メディア アセットのライフサイクルの監査 – パート 1
このポストは、7 月 23 日に投稿した Auditing Media Assets Lifecycle – Part 1 の翻訳です。
付加価値の高いコンテンツを扱うメディア アプリケーションでは、通常、MPAA (英語) や CDSA (英語)、またはそれと同等のコンプライアンス要件に従う必要があります。また、その監査手順の一環として、メディア アセットのライフサイクルを明示する監査レポートを作成し、アプリケーションおよびサービスの全体を説明するように求められることがあります。このブログでは、Media Services 全体を通してメディア アセットの監査レポートを生成する方法を、シリーズとして複数回に分けて説明します。第 1 回は、メディア アセットの作成と削除を示すアセット監査レポートの作成方法を扱います。
Media Services のメディア アセット
メディア アセットを作成すると、Media Services では GUID が生成され、この GUID に基づいて “nb:cid:UUID:” というプレフィックスの後に GUID が続く形でメディア アセットの ID が作成されます。これらの ID は URN 形式です。“nb” は Media Services のコードネーム (Nimbus) のイニシャルを、“cid” はコンテンツ ID (Content ID) を表しています。つまり、メディア アセットの ID は “nb:cid:UUID:<GUID>” となります。Media Services では次に、アセットのレコードが作成され、内部に格納されます。さらに、指定されたストレージ アカウントに “asset-<GUID>” という名前のコンテナーが作成されます。アセットの作成が完了したら、そのストレージ コンテナーにメディア ファイルをアップロードできます。メディア アセットを削除すると、Media Services では内部データベースからアセットのレコードが削除され、さらにストレージ コンテナーが削除されます。このため、アセットが削除されない限り、Media Services API を使用して作成時刻を特定することはできますが、アセットの削除時刻は、メディア アプリケーションで追跡しない限り特定することはできません。
Storage のログでメディア アセットの作成と削除を追跡する
メディア アセットは Storage のコンテナーとして表されるため、Storage のログからメディア アセットの作成時刻と削除時刻を特定することができます。ただしこの場合、使用しているストレージ アカウントで Storage のログ記録が有効になっている必要があります。詳細については、ログ記録の構成 (英語) を参照してください。監査レポートをいつまでさかのぼることができるかは、保持ポリシーの設定により決まります。ポリシーを 0 に設定した場合、ログは消去されず、ログ記録が有効化された時点までさかのぼることができます。Azure Storage のログは、ストレージ アカウント内の $logs というコンテナーに保存されます。ログの保存方法および命名規則の詳細については、MSDN の「Storage Analytics Logging について」のページをお読みください。
サンプル コード
次のサンプル コードは、Media Services のアセットのコレクションと Storage のログの両方を使用して AssetAudit という名前の Azure Storage Tables を作成するものです。この AssetAudit テーブルは、アセットの作成時刻と削除時刻を示すアセットの監査レポートを生成する際に使用できます。ロジックの内容は、主に次のようになっています。
- Media Services API を使用してすべてのアセットのリストを作成します。
- リストに挙げられたそれぞれのアセットについて、Asset.Created プロパティを使用して AssetAudit テーブルにエントリを作成します。
- $logs/blob にあるすべての BLOB を処理ます。
- それぞれの BLOB ファイルを取得し、さらに MSDN の「Storage Analytics のログの形式」のページに書かれているログ エントリの形式に従って解析します。
- “asset-” で始まるオブジェクトに対する操作を検出します。
- CreateContainer および DeleteContainer の操作を検出し、それぞれのエントリを AssetAudit テーブルに作成します。
サンプル コードの App.Config ファイルは次のようになります。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<appSettings>
<add key="MediaServicesAccountName" value="<MediaAccountName>" />
<add key="MediaServicesAccountKey" value="<MediaAccountKey>" />
<add key="StorageConnectionString" value="DefaultEndpointsProtocol=https;AccountName=<StorageAccountName>;AccountKey=<StorageAccountKey>"/>
</appSettings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.WindowsAzure.Storage" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
上の App.Config 内の <MediaAccountName> および <MediaAccountKey> の部分は、お客様の Media Services アカウントの名前とキーに置き換えてください。また、<StorageAccountName> および <StorageAccountKey> の部分は Media Services アカウントに関連付けられているストレージ アカウントの名前とキーに置き換えます。
コード本体は次のようになります。
using System;
using System.Linq;
using System.Configuration;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.MediaServices.Client;
namespace AssetAuditing
{
/// <summary>
///
/// </summary>
public class AssetAuditEntity : TableEntity
{
public string OperationType { get; set; }
}
/// <summary>
///
/// </summary>
class Program
{
// App.config ファイルから値を読み込む
private static readonly string _mediaServicesAccountName = ConfigurationManager.AppSettings["MediaServicesAccountName"];
private static readonly string _mediaServicesAccountKey = ConfigurationManager.AppSettings["MediaServicesAccountKey"];
private static readonly string _storageConnectionString = ConfigurationManager.AppSettings["StorageConnectionString"];
private static string _lastLogFile = ConfigurationManager.AppSettings["LastLogFile"];
// サービスのコンテキストのフィールド
private static CloudMediaContext _context = null;
private static MediaServicesCredentials _cachedCredentials = null;
private static CloudStorageAccount _cloudStorage = null;
private static CloudBlobClient _blobClient = null;
private static CloudTableClient _tableClient = null;
private static CloudTable _assetAuditTable = null;
/// <summary>
///
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
try
{
// Media Services の認証情報を静的クラス変数の形で作成しキャッシュする
_cachedCredentials = new MediaServicesCredentials(_mediaServicesAccountName, _mediaServicesAccountKey);
// キャッシュされた認証情報から CloudMediaContext を作成する
_context = new CloudMediaContext(_cachedCredentials);
_cloudStorage = CloudStorageAccount.Parse(_storageConnectionString);
_blobClient = _cloudStorage.CreateCloudBlobClient();
_tableClient = _cloudStorage.CreateCloudTableClient();
_assetAuditTable = _tableClient.GetTableReference("AssetAudit");
_assetAuditTable.CreateIfNotExists();
ProcessAssetData();
ParseStorageLogs();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.InnerException.StackTrace);
}
}
/// <summary>
/// この関数では、$logs コンテナーに格納されている Storage のすべてのログ ファイルを解析する
/// ただし app.config のエントリに基づいて、直近の実行時に既に解析済みのファイルは飛ばして進む
/// </summary>
static void ParseStorageLogs()
{
try
{
// $logs/blob にあるすべての BLOB のリストを作成する
foreach (CloudBlockBlob _blobItem in _blobClient.ListBlobs("$logs/blob", true))
{
// BLOB のリストは昇順で作成される
// ログは時系列順に整理されるため、直近の処理済みのログ ファイルと BLOB 名を比較すれば、再び処理しないようにできる
if (String.Compare(_blobItem.Name, _lastLogFile) > 0)
{
try
{
Console.WriteLine("Processing " + _blobItem.Name);
string _logs = GetBlobData(_blobItem); // BLOB を取得
// 新しい行の区切り記号を検出してログの各行を取得
List<string> _logLines = ParseDelimitedString(_logs, "\n");
for (int i = 0; i < _logLines.Count; i++)
{
// 区切り記号の ; を検出してログの各項目を分割
List<string> _logLineItems = ParseDelimitedString(_logLines[i], ";");
if (_logLineItems.Count > 0)
{
// ログの各行を解析
ParseLogLine(_logLineItems);
}
}
// BLOB 名を直近の処理済みのログ ファイルとして保存
_lastLogFile = _blobItem.Name;
SaveLastLogFileInConfig();
}
catch (Exception x)
{
Console.WriteLine(x.Message + x.InnerException.StackTrace);
}
}
else
{
Console.WriteLine("Skipping " + _blobItem.Name);
}
}
SaveLastLogFileInConfig();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.InnerException.StackTrace);
}
}
/// <summary>
/// この関数では、Media Services アカウントのすべてのアセットをループ処理 (1 回あたり 1000 個) し、AssetAudit テーブルにアセットの作成時刻を記録する
/// </summary>
static void ProcessAssetData()
{
try
{
int skipSize = 0;
int batchSize = 1000;
int currentSkipSize = 0;
while (true)
{
// すべてのアセットのリストを作成する (1 回あたり 1000 個)
foreach (IAsset asset in _context.Assets.Skip(skipSize).Take(batchSize))
{
currentSkipSize++;
Console.WriteLine("Processing Asset " + asset.Id);
// AssetAudit テーブルにアセットの作成時刻を入力する
InsertAssetData(asset.Id, asset.Created.ToString("o"), "Create");
}
if (currentSkipSize == batchSize)
{
skipSize += batchSize;
currentSkipSize = 0;
}
else
{
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
/// <summary>
/// この関数では、解析された最終のログ ファイルを app.config に保存する
/// </summary>
static void SaveLastLogFileInConfig()
{
var configFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var settings = configFile.AppSettings.Settings;
if (settings["LastLogFile"] == null)
{
settings.Add("LastLogFile", _lastLogFile);
}
else
{
settings["LastLogFile"].Value = _lastLogFile;
}
configFile.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection(configFile.AppSettings.SectionInformation.Name);
}
/// <summary>
/// この関数では BLOB を取得して、その中のデータを文字列として読み込む
/// </summary>
/// <param name="_blobItem"></param>
/// <returns></returns>
static string GetBlobData(CloudBlockBlob _blobItem)
{
MemoryStream ms = new MemoryStream();
_blobItem.DownloadToStream(ms);
byte[] buffer = new byte[ms.Length];
ms.Seek(0, SeekOrigin.Begin);
ms.Read(buffer, 0, (int)ms.Length);
string _logs = Encoding.UTF8.GetString(buffer);
ms.Dispose();
return _logs;
}
/// <summary>
/// この関数では文字列を解析し、指定された区切り記号で分割されたサブ文字列のリストを生成する
/// 引用符の内側にある区切り記号は無視する
/// </summary>
/// <param name="_stringToParse"></param>
/// <param name="strDelimiter"></param>
/// <returns></returns>
public static List<string> ParseDelimitedString(string _stringToParse, string strDelimiter)
{
List<string> _parsedStrings = new List<string>();
if (!String.IsNullOrEmpty(_stringToParse))
{
int j = 0;
int i = _stringToParse.IndexOf(strDelimiter);
while (i >= 0)
{
if (_stringToParse.Length > 0)
{
// この部分では、サブ文字列の先頭が引用符かどうかを確認する
// 引用符である場合、一致するペアを検索し、その後ろにある区切り記号を検出する
if (_stringToParse[j] == '\"')
{
i = _stringToParse.IndexOf("\"", j + 1);
if (i > 0)
{
i = _stringToParse.IndexOf(strDelimiter, i);
}
}
}
string _str = _stringToParse.Substring(j, i - j);
_parsedStrings.Add(_str);
j = i + strDelimiter.Length;
i = _stringToParse.IndexOf(strDelimiter, j);
}
_parsedStrings.Add(_stringToParse.Substring(j, _stringToParse.Length - j));
}
return _parsedStrings;
}
/// <summary>
/// この関数では、ログの各行を解析する
/// </summary>
/// <param name="_logLineItems"></param>
static void ParseLogLine(List<string> _logLineItems)
{
try
{
// バージョン 1.0 のログを処理していることと、すべてのログ項目が正常に分割されたことを確認する
if ((_logLineItems[0] == "1.0") && (_logLineItems.Count == 30))
{
// 必要なログ項目を解析する (このサンプルではすべてのアイテムを解析する必要はない)
string _requestedObjectKey = _logLineItems[12];
string _assetPrefix = "\"/" + _cloudStorage.Credentials.AccountName + "/asset-";
int _assetIdIndex = _requestedObjectKey.IndexOf(_assetPrefix);
if (_assetIdIndex == 0)
{
Console.WriteLine("Processing ObjectKey=" + _requestedObjectKey);
_assetIdIndex += _assetPrefix.Length;
int j = _requestedObjectKey.IndexOf("/", _assetIdIndex);
if (j < 0)
{
j = _requestedObjectKey.Length - 1;
}
string _assetId = _requestedObjectKey.Substring(_assetIdIndex, j - _assetIdIndex);
_assetId = "nb:cid:UUID:" + _assetId;
string _timeStamp = _logLineItems[1];
string _operationType = _logLineItems[2];
string _requestStatus = _logLineItems[3];
string _authType = _logLineItems[7];
string _requesterIpAddress = _logLineItems[15];
Console.WriteLine("Processing Asset Id:" + _assetId + " TimeStamp:" + _timeStamp + " OperationType:" + _operationType);
switch (_operationType)
{
case "CreateContainer":
_operationType = "Create";
InsertAssetData(_assetId, _timeStamp, _operationType);
break;
case "DeleteContainer":
_operationType = "Delete";
InsertAssetData(_assetId, _timeStamp, _operationType);
break;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.InnerException.StackTrace);
}
}
/// <summary>
/// この関数では、AssetAudit テーブルにエントリを追加する
/// Create 操作の場合はデータに 2 つのソースがあるので、エントリが重複しないように、エントリが既に存在しているかどうかを確認する
/// Azure のロール インスタンス間で時計がずれるために、アセットのコレクションと Storage のログのタイムスタンプは多少異なる場合がある
/// </summary>
/// <param name="_assetId"></param>
/// <param name="_timeStamp"></param>
/// <param name="_operationType"></param>
static void InsertAssetData(string _assetId, string _timeStamp, string _operationType)
{
try
{
bool _insert = true;
if (_operationType == "Create")
{
// operationType (操作の種類) が "Create" の場合、そのアセットの ID でエントリが存在するかどうかを確認する
TableQuery<AssetAuditEntity> query = new TableQuery<AssetAuditEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, _assetId));
query.Take(1);
TableQuerySegment<AssetAuditEntity> tqs = _assetAuditTable.ExecuteQuerySegmented(query, null);
if ((tqs != null) && (tqs.Results != null))
{
if (tqs.Results.Count > 0)
{
if (tqs.Results[0].OperationType == "Create")
{
_insert = false;
}
}
}
}
if (_insert)
{
AssetAuditEntity _asset = new AssetAuditEntity();
_asset.PartitionKey = _assetId;
_asset.RowKey = _timeStamp;
_asset.OperationType = _operationType;
TableOperation op = TableOperation.Insert(_asset);
_assetAuditTable.Execute(op);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
このコードで使用されている関数について簡単に説明します。
ProcessAssetData
この関数は、指定された Media Services アカウントのすべてのアセットをループ処理します。Media Services は、アセットのコレクションで 1,000 個のアセットを返します。この関数では Skip および Take を使用して、アカウント内に 1,000 個以上のアセットが存在する場合にも確実にすべてのアセットをリストに含めます。
ParseStorageLogs
この関数は、 $logs/blob に格納されているすべての BLOB のリストを作成し、処理が済んだ BLOB を直近の処理済みのログ ファイルとして保存して、コードが再実行されても処理が重複しないようにします。
SaveLastLogFileInConfig
この関数は、直近の処理済みのログ ファイルの名前を App.Config に保存して、プログラムが再実行されたときに取得できるようにします。
GetBlobData
この関数では、Storage から BLOB を取得して、その中のデータを文字列として読み込みます。
ParseDelimitedString
この関数は、指定された区切り記号に基づいて文字列を分割します。分割されたデータは文字列のコレクションとして返されます。
ParseLogLine
この関数は、ログの各行を解析して “asset-” で始まるコンテナーの CreateContainer および DeleteContainer の操作を抽出します。
InsertAssetData
この関数は、AssetAudit テーブルにエントリを追加します。
アセットの監査データ
上記のコードを実行すると、AssetAudit テーブルが作成されます。テスト アカウントで作成されたこのテーブルの内容のスクリーンショットを次に示します。赤枠で囲まれているのは、あるアセットの Create と Delete の組み合わせです。Media Services にはこのアセットが存在しないため、これらのエントリは、上記のコードを実行して Storage ログから作成するしかありません。
また、このデータは Excel の Power Query を使用して Excel に読み込むことができます。Excel を利用すると、さらに高度なフィルタリングを行ったり、ピボット テーブルに読み込んで詳細な分析を行ったりすることができます。Excel の Power Query を使用したことのないお客様は、Microsoft Power Query for Excel のダウンロード ページ (英語) からダウンロードしてご利用ください。インストール完了後に Excel を起動すると、[POWER QUERY] というタブが表示されます。このタブをクリックし、続いて [From Other Sources] ボタンをクリックすると、次のスクリーンショットのように [From Windows Azure Table Storage] というメニュー項目が表示されます。
AssetAudit テーブルからデータをインポートするには、上記のメニュー項目を選択して、画面の指示に従います。Azure の Table が右側の [Navigator] ウィンドウに読み込まれた後、AssetAudit テーブルをダブル クリックすると、次のスクリーンショットのように新しいウィンドウが開きます。
[Content] 列の隣のボタンをクリックして、さらに [OK]、画面上部の [Apply & Close] の順にクリックします。画面が閉じて、データが Excel に読み込まれます。これで、Excel を使用して自由にデータを分析できるようになります。
考慮事項
最後に、サンプル コードを使用する際の注意事項について説明します。
- この記事でご紹介したサンプル コードは、すべてのアセットが 1 つのストレージ アカウントに含まれている Media Services アカウントで使用することを前提としていますが、簡単な変更で複数のストレージ アカウントでも動作するようにできます。
- 監査は Storage のログに関連付けられている保持ポリシーによる制限を受けます。
- デバッガーでサンプル コードを実行する場合は、直近に処理されたログの BLOB 名が App.Config ファイルに更新されることはありません。更新はデバッガー以外でサンプル コードを実行した場合にのみ行われます。
- 例外はコンソールにのみ出力されます。ただし、必要に応じて Azure Tables やローカル ファイルに書き出すようにすることもできます。