プログラムを作るときに、プロジェクトで(ログ出力をコントールするために)NLog を採用することがあります。
そこまではよいのですが、個人的なライブラリのプロジェクトの中では通常どおり System.Diagnostics
の Debug
クラスを利用して WriteLine(...)
を使って例外やデバッグ用のメッセージを出力している、というケースはよくあると思います。
こうなると、あっちは NLog の出力、こっちではデフォルトの出力……となってしまって、イマイチよい形でログ出力をコントロールできていない感じになっていまします。
そこで NLog に限らず、Debug メッセージをカスタマイズしたいときは TraceListener
を利用するのが一番シンプルな方法になると思います。今回のパターンでは「コンソールに出力されるメッセージは全部 NLog から出力してほしいんだけどなぁ」という問題に対応する例になります。この内容をメモ。
NLog の定義ファイル (nlog.config)
NLog の基本的な使い方は過去の記事で書いたので割愛します。
今回の NLog 設定の定義ファイル (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"><!-- throwExceptions:NLog の(主に設定に関する)例外をスローするので、本番環境では false にすること --><targets><target name="console"xsi:type="Console" /><target name="debugger"xsi:type="Debugger"layout="[${uppercase:${level:padding=-5}}]${date:format=HH\:mm\:ss.ffff} ${message}${exception:format=tostring} (${callsite})" /><target name="file"xsi:type="File"fileName="logs/log.txt"layout="${level},"${message}","${exception:format=tostring}",${longdate},${callsite:className=true:methodName=true}" /></targets><rules><logger name="*"minLevel="Trace"writeTo="debugger" /><logger name="*"minlevel="Debug"writeTo="file" /></rules></nlog>
TraceListener を定義
次に TraceListener
クラスを定義します。TraceListener
は System.Diagnostics
名前空間にあるクラスなので、NLog 側で定義するクラスではありません。
publicclassTestTraceListener : TraceListener { privatestaticreadonly Logger _Logger = LogManager.GetCurrentClassLogger(); publicoverridevoid Write(string? message) { _Logger.Debug(message ??""); } publicoverridevoid WriteLine(string? message) { _Logger.Debug(message ??""); } }
一般的には Write
と WriteLine
メソッドをここで工夫することで、コンソール出力等を工夫することになります。自作のファイル出力にすることもできると思いますが NLog を使っているなら、わざわざ自分でやることもない。
ただ、上記のコードだと NLog の便利な部分である Debug がどこで呼び出されたのか、という ${callsite}
の部分が TestTraceListener
クラスになってしまいます。加えて、Nlog の呼び出しも Debug
など一定のレベル呼び出しになってしまいます。(とはいえ、それでも通常の Debug
出力と比べると機能が低下したりすることは無いはずですが)
本当なら、try-catch の中での Debug
呼び出しなら出力のレベルを上げたいところです。無理に凡化して対応をすると StackTrace
などを利用して元メソッドの情報を読み取るといった形で複雑になったり、パフォーマンスに悪影響を与えないように注意が必要になったりもします。
このあたりは message にルールを設定して、特定のキーワード(例えば "Warning")を含むなら~といった条件で、レベルを切り替えれるようにすると簡単化しやすいと思います。後述しましたが、レベルを設定部で切り替え可能なようにしておくと便利かも。
テスト
Trace.Listeners.Clear()
は実行しないと、デフォルトのログ出力と NLog の出力で、ログを二重にコンソール出力することになります。注意点はそれくらいのもので、単純な設定変更でログ出力をカスタマイズできることがわかります。これくらいなら、多少カスタマイズをしてもプリプロセッサディレクティブ #if DEBUG
を使っておけば、パフォーマンスへの影響を簡単にコントールできそうです。
コードの出力例は、Sample に記載しています。
internalclassProgram { privatestaticreadonly Logger _Logger = LogManager.GetCurrentClassLogger(); staticvoid Main(string[] args) { var program =new Program(); program.Run(args); } publicvoid Run(string[] args) { Trace.Listeners.Clear(); // デフォルトの出力をクリア Trace.Listeners.Add(new TestTraceListener()); Debug.WriteLine("test 1."); var sample =new Sample(); sample.WriteLine("test 2."); Sample.WriteLineStatic("test 3."); } }
Sample
クラスは以下のようなコードにしました。別ライブラリのプロジェクトを作成して Sample
を追加します。実行用プロジェクトは、ライブラリを参照して Sample
のメソッドを実行します。
これが、一番最初に書いた条件の「別プロジェクトのログ出力」の統一、ということになります。
public class Sample { public void WriteLine(string text) { System.Diagnostics.Debug.WriteLine(text); } public static void WriteLineStatic(string text) { System.Diagnostics.Debug.WriteLine(text); } }
応用編
こんな感じで Debug
出力をデフォルトにしたり Info
出力にしたりを切り替えできるようにしておくのもよいかも。ライブラリの完成度が高ければ、ファイル出力に対応する以上のレベルにする必要があるので。
public void Run(string[] args) { Trace.Listeners.Clear(); // デフォルトの出力をクリア Trace.Listeners.Add(new TestTraceListener(Debug)); }
たとえば、こんな感じ:
publicclassLevelTraceListener : TraceListener { privatestaticreadonly Logger _Logger = LogManager.GetCurrentClassLogger(); private Action<string> _Action; public LogLevel Level { get; } publicoverridevoid Write(string? message) { _Action.Invoke(message ??""); } publicoverridevoid WriteLine(string? message) { _Action.Invoke(message ??""); } /// <summary>/// <seecref="LevelTraceListener"/>クラスの新しいインスタンスを初期化します。/// </summary>/// <paramname="level">ログの出力レベル。</param>public LevelTraceListener(NLog.LogLevel level) { Level = level; Action<string> action = level.Name switch { "Trace"=> Trace, "Debug"=> Debug, "Info"=> Info, "Warn"=> Warn, "Error"=> Error, "Fatal"=> Fatal, "Off"=> Off, _=> Debug, }; _Action = action; } privatevoid Trace(string message) => _Logger.Trace(message); privatevoid Debug(string message) => _Logger.Debug(message); privatevoid Info(string message) => _Logger.Info(message); privatevoid Warn(string message) => _Logger.Warn(message); privatevoid Error(string message) => _Logger.Error(message); privatevoid Fatal(string message) => _Logger.Fatal(message); privatevoid Off(string message) { // NOP } }
Sample
出力例を示します。
[DEBUG]11:05:55.5514 test 1. (NLogTraceTest.TestTraceListener.WriteLine) [DEBUG]11:05:55.6050 test 2. (NLogTraceTest.TestTraceListener.WriteLine) [DEBUG]11:05:55.6074 test 3. (NLogTraceTest.TestTraceListener.WriteLine)
GitHubにサンプルを公開しています。