読者です 読者をやめる 読者になる 読者になる

M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

NLogをプログラマブルに初期化し動的に構成変更する

C# macOS

少し前に、TAC(Talend Administration Center)のRPCインターフェースを突っつくためのC#ライブラリを作成しました(リポジトリこちら)。そして直近これに手を加える中でNLogを使う機会を得ました。

Log4nなどと同様NLogもXMLファイルで設定を記述するのが一般的ですが、C#コードからロガーを初期化し、かつまた、アプリケーションの稼働中に動的にその構成変更を行うことも可能です。ロギングがアプリケーションの主関心ときっちり分離できるときはXMLファイルによる構成のほうが好ましいのは明らかですが、その反対であるようなケース(ロギングがアプリケーションの提供しようとする機能と密接に関係する場合)や何かしら高度なカスタマイズを加えたいというケースではむしろC#コードによる制御を行ったほうが好ましいのではないかと考えています。

手順

何はともあれ例を示すためXamarin Studioでソリューションを新規作成しました。

f:id:m12i:20161126202432p:plain

続いてソリューション・パッドでプロジェクト(今回は"NLogSample"プロジェクト)を右クリック→[追加]→[NuGetパッケージの追加]をクリックして、「パッケージを追加」ウィンドウで「NuGet」を検索します。結構な数のパッケージがヒットしますが必要なのはその名も「NLog」というパッケージだけです。これを選択して[Add Package]をクリック。

f:id:m12i:20161126202810p:plain

コーディングの前に整理。NLogの主要な概念は以下の通り:

概念 説明
Logger 文字通り。ログを出力する際に直接使用されるオブジェクトで、Log()Info()Warn()Log()などのメソッドを持ちます。
Target おおよそLog4jやLog4nにおけるAppenderに相当。ログ出力先のリソースと出力時のフォーマットを管理するためのオブジェクトです。ファイル、コンソール、イベントログなど各種のリソース向けにレディメイドが提供されています(詳しくはこちらを参照)。
LoggingRule Loggerの名前とログレベルを条件に、LoggerTargetを結びつける概念です。
Layout ログ出力時のレイアウトのほかログファイル名(FileTarget#FileName)などもこのオブジェクトで表現されており、ようするにNLogもしくはその拡張が提供する各種文字列置換ロジックの対象となるテンプレートとでも言うべきものです。これもレディメイドがいくつか提供されています(詳しくはこちらを参照)。

これらを組み合わせてロガーの構成(設定)を行います。

まずはusingNLogNLog.ConfigNLog.Targetsの3つの名前空間のメンバーを参照できるようにします。

追加したら、LoggingConfigurationのコンストラクタを呼び出しインスタンスを生成します。もしXMLファイルでベースとなる設定をしていて、C#コードからは追加の動的な変更を行うだけだという場合にはLogManager.Configurationプロパティから既成のインスタンスを手に入れます(まだXMLファイルなどで構成を済ませていない場合このプロパティはnullを返します)

using System;
using NLog;
using NLog.Config;
using NLog.Targets;

namespace NLogSample
{
	class MainClass
	{
		public static void Main(string[] args)
		{
			var conf = new LoggingConfiguration();
		}
	}
}

続いて要件にあわせてターゲットを作成します。今回はコンソール出力とファイル出力をさせるのでConsoleTargetFileTargetを登場させています:

var console = new ConsoleTarget("console");
var file = new FileTarget("file");
file.Encoding = Encoding.UTF8;
file.FileName = "logs/sample-${date:format=yyyyMMdd}.log";

前述の通りNLogではファイル出力時のファイル名なども「レイアウト」として表現されています。上記のコードではFileTarge#FileNameプロパティはLayout型です。開発者が暗黙型変換を定義することを可能にするC#の黒魔術的な便利機能のおかげで、string"logs/sample-${date:format=yyyyMMdd}.log"という値は暗黙的にLayout型に変換されています。

Targetの初期化・カスタマイズが終わったらLoggingConfigurationに登録を行います。そしてLoggingRuleでロガーの名前とログレベル(例では下限のみの指定だが上限も指定可能)とTargetをひも付けます:

conf.AddTarget(console);
conf.AddTarget(file);
conf.LoggingRules.Add(new LoggingRule("*", LogLevel.Info, console));
conf.LoggingRules.Add(new LoggingRule("*", LogLevel.Warn, file));

上記のコードではほんのわずかなカスタマイズしかしていませんが、NLogのGitHubリポジトリWikiを読むともっとたくさんのオプションが提供されていることがわかります。

締めくくりにLoggingConfigurationLogManager.Configurationプロパティに設定します。LoggingConfigurationのコンストラクタによる初期化からはじめず、既成の同プロパティから既成のLoggingConfigurationを取得して設定変更をしている場合も、このプロパティへの再設定は必要のようです。これによりはじめて変更後の設定がロードされます:

LogManager.Configuration = conf;

これで準備はできたのでロガーを取得して実際にログ出力してみます。以下のコードを記述したら、メニューバーから[実行]→[デバッグなしで開始]をクリックして実行してみます:

var logger = LogManager.GetCurrentClassLogger();
logger.Info("いんふぉ");
logger.Warn("わーん");

コンソール出力には2行のメッセージが出力され:

f:id:m12i:20161126212605p:plain

ログファイルには1行のメッセージが出力されています:

f:id:m12i:20161126212659p:plain

ところでこのファイル名には年月日が刻印されていますが、これは先程FileTarge#FileNameプロパティに"logs/sample-${date:format=yyyyMMdd}.log"というレイアウト(に暗黙型変換される文字列)を指定したからでした。この${...}書式で指定できる変数にはレイアウトごとにいろいろあるのですが、カスタムメイドの変数を定義して使用することもできます。

それには以下のようにLoggingConfiguration#Variablesプロパティが参照するコレクションに名前とレイアウト(サンプルでは文字列が指定されているが例によって暗黙型変換が裏ではたらいている)を指定します:

conf.Variables.Add("fooDate", DateTime.Now.ToString("yyyy-MM-dd"));

こうするとた例えばFileTarge#FileNameプロパティに"logs/sample-${var:fooDate}.log"という値を設定することで任意の文字列をログファイル名に埋め込むことができるようになります。

上記の例はいかにも無意味──そもそも${date:format=yyyy-MM-dd}でこと足りるのにわざわざカスタムメイドしている──に見えますが、実はそうでもありません。"${date:format=yyyy-MM-dd}"の場合、ログファイル名はログのバッファがフラッシュされるたびに再計算されるので、プログラムの実行中にファイル名が変化する可能性があります。一方、上記の例ではAdd(string, Layout)メソッドを呼び出す時点で日付を文字列化しているので、"${var:fooDate}"が表わす値が変動することはありませんから、ファイル名も変化しません。

これはメリットにもデメリットにもなるでしょう。Layoutそのものの拡張に踏み出せばさらに広範なカスタマイズが可能になるはずです。しかしまあ今回はこのへんで終わりにします。