NLogを使ってAzure Table Storageにログを書きこむ

NuGetを検索するとAzure Table Storage用のTargetをいくつか見かけますが、.NET Coreへの対応状況や依存ライブラリの最新化状況などの制約があって微妙です。
また、Azure Table StorageまわりのSDKはCosmosDBに切り出されたりしてややこしいので、たかがEntityの追加のみで依存関係を増やしたくありません。
そんなわけで、NLog標準の`WebServiceTarget`を使った書き込みを試したら結構ハマりどころがあったので共有します。

# 事前準備

## コンソールアプリのプロジェクトを作成

ターゲットフレームワークはNLogがサポートするものであれば何でも良いですが、.NET Core 2.0〜3.0までで動作確認をしました。

## NLogのインストール

NuGetからインストールします。この記事では4.6.8の利用を前提としていますが、多少前後しても問題ないと思います。

## Azure StorageのSAS URLの作成

SASとはShared Access Signatureの略で、あらかじめリソースやそれに対するアクション（参照、書き込みなど）、利用可能期間を制限したトークンです。これを含むURLを使うことで、リクエスト都度署名をするといった手間も省けるためWebServiceTargetで利用する上では好都合というわけです。

作成は簡単で、Azure Portalで対象ストレージアカウントを選択し、メニューの「Shared Access Signature」に移動します。対象リソースなどを選んで「SASと接続文字列を作成する」ボタンを押下後、表示された「Table service の SAS URL」を控えておいてください。
<img width="1440" alt="azureportal.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53296/c81859a2-13e1-7c85-b613-de18cf9f2c3d.png">

# NLogの設定

まずは以下の通り`WebServiceTarget`を設定します。名前は何でもいいですが、ここでは`AZTBLTarget`としてみました。

```xml:NLog.config
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      throwExceptions="true">

    <targets>
        <target name="AZTBLTarget" xsi:type="WebService" protocol="JsonPost" url="<SAS URL>">
            <parameter name="PartitionKey" layout="${callsite}" />
            <parameter name="RowKey" layout="${ticks}.${sequenceid:padCharacter=0:padding=6}" />
            <parameter name="LocalTimestamp" layout="${date:format=yyyy-MM-ddTHH\:mm\:ss.fffffff}" />
            <parameter name="Level" layout="${level:uppercase=true}" />
            <parameter name="Callsite" layout="${callsite}" />
            <parameter name="Message" layout="${message}" />
            <parameter name="Error" layout="${exception:format=Message, ToString:separator=\n}" />
        </target>
    </targets>

    <rules>
        <logger name="*" minlevel="Trace" writeTo="AZTBLTarget" />
    </rules>
</nlog>
```

簡単に説明すると、ターゲットの種類として`WebService`のものを定義していて、その通信手続きとしてJSONデータのPOST、そのJSONのパラメータとして`PartitionKey`や`RowKey`を含む7項目を送信するようにしています。なお`PartitionKey`と`RowKey`はTable Storageとして必須の項目です。

さて、説明の中で`url`アトリビュートの説明を飛ばしていましたが、ここがハマりポイントです。コピペして`url`に貼り付けるだけではうまくいきません。先の手順で取得したSAS URLを、以下の通り修正する必要があります。

```:生成されたもの
https://accountname.table.core.windows.net/?&sv=2019-02-02&ss=t&srt=sco&sp=wau&se=2030-01-04T08:21:42Z&st=2020-01-04T00:21:42Z&spr=https&sig=xxxxxxxxx%3D
```

```:修正後
https://accountname.table.core.windows.net/nlogtable?$format=application/json&amp;sv=2019-02-02&amp;ss=t&amp;srt=sco&amp;sp=wau&amp;se=2030-01-04T08:21:42Z&amp;st=2020-01-04T00:21:42Z&amp;spr=https&amp;sig=xxxxxxxxx%3D
```

修正内容は以下の通り。

- URIの末尾に`/<テーブル名>`をつける。当たり前ですが、コピペでは動かず一瞬「ウッ」てなります。修正後の例は、テーブル名を`nlogtable`のケースです。
- `&`を`&amp;`に置換。これもXMLに一般的な話だと思いますが、エラーになるので小心者の私は「えっえっ」てなりました。
- `$format=application/json`をつける。[Insert Entityの仕様](https://docs.microsoft.com/ja-jp/rest/api/storageservices/insert-entity)を見る限り`Accept`ヘッダーは任意なのですが、なぜかこれを渡さないと`415`エラーとなります。しかしながらNLogのWebServiceTargetではなぜかHTTPヘッダーに`Accept`を設定することができませんでした。諦めかけていたところ、[Payload format for Table service operations](https://docs.microsoft.com/en-us/rest/api/storageservices/payload-format-for-table-service-operations)を読んだら`$format`で指定できることを発見したため、これをURLに含んでやります。

# ログの書き込み

それでは実際に書き込んでみましょう。本記事のポイントは設定方法ですので、書き込み自体は一般的な手順に従えばOKかと思います。ちょっとハマるかもしれないポイントとしては、最終行の`LogManager.Shutdown();`を実行しないと、ログ送信前にプロセスが終了してしまいTable Storgeに書き込まれない場合があるところでしょうか。

```csharp
using NLog;

namespace NLogToAzureTable
{
    class Program
    {
        static void Main(string[] args)
        {
            // NLog.configを出力ディレクトリにコピーするように設定してください。
            // もしくは、以下の通り明示的にパスを指定することもできます。2つめの引数falseはエラーを無視するか否かの設定です（false=無視しない）
            // LogManager.Configuration = new XmlLoggingConfiguration("/path/to/NLog.config", false);

            // ロガーの初期化・取得
            var logger = LogManager.GetCurrentClassLogger();

            // ログの書き込み
            logger.Info("test message for info");
            logger.Warn("test message for warn");

            // ロガーの終了。プロセスを終了する前にログを送信するために必要
            LogManager.Shutdown();
        }
    }
}
```

# 非同期書き込みの利用

NLogには便利な非同期処理の仕組み`AsyncWrapper`ターゲットがあります。ログメッセージをキューに貯めてバッチ的に別スレッドで処理してくれるといった便利なものです。
使い方は超簡単で、`targets`の`async`アトリビュートに`true`を設定するだけで全てのターゲットへの書き込みが非同期に行われます。

```xml:NLog.config
：中略
<targets async="true">
：中略
```

一部の書き込みのみを非同期にしたい場合は、ラッパーの名の通り非同期にしたいターゲットを`AsyncWrapper`で囲みます。このとき、`rule`に設定する`name`もラッパーのものにすることに注意です。

```xml:NLog.config
    <targets>
        <target name="AZTBLTargetAsync" xsi:type="AsyncWrapper">
            <target name="AZTBLTarget" xsi:type="WebService" protocol="JsonPost" url="<SAS URL>">
                <parameter name="PartitionKey" layout="${callsite}" />
                 ：中略
            </target>
        </target>
    </targets>

    <rules>
        <logger name="*" minlevel="Trace" writeTo="AZTBLTargetAsync" />
    </rules>
```

詳細は公式Wikiを読んでみてください。
https://github.com/nlog/NLog/wiki/AsyncWrapper-target


# 補足

- `PartitionKey`に設定する値。記事では便宜上出力元メソッドの名前を指定していますが、ユーザーとかデバイスとかを一意に特定できるものを設定した方が調査しやすいと思います・
- パフォーマンスや負荷は未知数。`AsyncWrapper`を使うことでメイン処理への影響はないと思いますが、書き込み速度や負荷は計測していません。WebServiceTargetに一般的な議論かと思います。詳しい方アドバイスください！！
- 出力レベル。そもそもリモートでログを取る時点で常にデバッグ情報が欲しいわけではないと思いますので、`minlevel`をエラーに設定するなど最適化が必要です。
- Logging Frameworkについて。実装ライブラリを自由に変更できるようにDIなフレームワークが.NET Coreでは用意されていますが、今回は未考慮です。


# おまけ：UnityでNLogを使ってAzure Table Storageに書き込み

そもそも何でAzure Table Storageにログを書き込もうと思ったかというと、[Gatebox](https://www.gatebox.ai/)というデバイス向けのアプリ開発をはじめたことがきっかけです。実機デバッグする際にコンソール出力やファイルファイルを選択できないことから、リモートにログ出力をする方法を探ることにしました。ログのためにサーバーを用意するのもアレなのでAzureに・・・という経緯です。

UnityのConsoleへのログ出力用ターゲットも同梱したファクトリーぽいものを作りましたのでよかったら使ってみてください。

NLogFactoryForUnity
https://github.com/uezo/NLogFactoryForUnity