NLogをプログラマブルに初期化し動的に構成変更する
少し前に、TAC(Talend Administration Center)のRPCインターフェースを突っつくためのC#ライブラリを作成しました(リポジトリはこちら)。そして直近これに手を加える中でNLogを使う機会を得ました。
Log4nなどと同様NLogもXMLファイルで設定を記述するのが一般的ですが、C#コードからロガーを初期化し、かつまた、アプリケーションの稼働中に動的にその構成変更を行うことも可能です。ロギングがアプリケーションの主関心ときっちり分離できるときはXMLファイルによる構成のほうが好ましいのは明らかですが、その反対であるようなケース(ロギングがアプリケーションの提供しようとする機能と密接に関係する場合)や何かしら高度なカスタマイズを加えたいというケースではむしろC#コードによる制御を行ったほうが好ましいのではないかと考えています。
手順
何はともあれ例を示すためXamarin Studioでソリューションを新規作成しました。
続いてソリューション・パッドでプロジェクト(今回は"NLogSample"プロジェクト)を右クリック→[追加]→[NuGetパッケージの追加]をクリックして、「パッケージを追加」ウィンドウで「NuGet」を検索します。結構な数のパッケージがヒットしますが必要なのはその名も「NLog」というパッケージだけです。これを選択して[Add Package]をクリック。
コーディングの前に整理。NLogの主要な概念は以下の通り:
概念 | 説明 |
---|---|
Logger | 文字通り。ログを出力する際に直接使用されるオブジェクトで、Log() 、Info() 、Warn() 、Log() などのメソッドを持ちます。 |
Target | おおよそLog4jやLog4nにおけるAppender に相当。ログ出力先のリソースと出力時のフォーマットを管理するためのオブジェクトです。ファイル、コンソール、イベントログなど各種のリソース向けにレディメイドが提供されています(詳しくはこちらを参照)。 |
LoggingRule | Logger の名前とログレベルを条件に、Logger とTarget を結びつける概念です。 |
Layout | ログ出力時のレイアウトのほかログファイル名(FileTarget#FileName )などもこのオブジェクトで表現されており、ようするにNLogもしくはその拡張が提供する各種文字列置換ロジックの対象となるテンプレートとでも言うべきものです。これもレディメイドがいくつか提供されています(詳しくはこちらを参照)。 |
これらを組み合わせてロガーの構成(設定)を行います。
まずはusing
でNLog
とNLog.Config
とNLog.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(); } } }
続いて要件にあわせてターゲットを作成します。今回はコンソール出力とファイル出力をさせるのでConsoleTarget
とFileTarget
を登場させています:
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を読むともっとたくさんのオプションが提供されていることがわかります。
締めくくりにLoggingConfiguration
をLogManager.Configuration
プロパティに設定します。LoggingConfiguration
のコンストラクタによる初期化からはじめず、既成の同プロパティから既成のLoggingConfiguration
を取得して設定変更をしている場合も、このプロパティへの再設定は必要のようです。これによりはじめて変更後の設定がロードされます:
LogManager.Configuration = conf;
これで準備はできたのでロガーを取得して実際にログ出力してみます。以下のコードを記述したら、メニューバーから[実行]→[デバッグなしで開始]をクリックして実行してみます:
var logger = LogManager.GetCurrentClassLogger(); logger.Info("いんふぉ"); logger.Warn("わーん");
コンソール出力には2行のメッセージが出力され:
ログファイルには1行のメッセージが出力されています:
ところでこのファイル名には年月日が刻印されていますが、これは先程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
そのものの拡張に踏み出せばさらに広範なカスタマイズが可能になるはずです。しかしまあ今回はこのへんで終わりにします。