
プログラムを作るときに、プロジェクトで(ログ出力をコントールするために)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 側で定義するクラスではありません。
public class TestTraceListener : TraceListener { private static readonly Logger _Logger = LogManager.GetCurrentClassLogger(); public override void Write(string? message) { _Logger.Debug(message ?? ""); } public override void 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 に記載しています。
internal class Program { private static readonly Logger _Logger = LogManager.GetCurrentClassLogger(); static void Main(string[] args) { var program = new Program(); program.Run(args); } public void 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));
}
たとえば、こんな感じ:
public class LevelTraceListener : TraceListener { private static readonly Logger _Logger = LogManager.GetCurrentClassLogger(); private Action<string> _Action; public LogLevel Level { get; } public override void Write(string? message) { _Action.Invoke(message ?? ""); } public override void WriteLine(string? message) { _Action.Invoke(message ?? ""); } /// <summary> /// <see cref="LevelTraceListener"/> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="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; } private void Trace(string message) => _Logger.Trace(message); private void Debug(string message) => _Logger.Debug(message); private void Info(string message) => _Logger.Info(message); private void Warn(string message) => _Logger.Warn(message); private void Error(string message) => _Logger.Error(message); private void Fatal(string message) => _Logger.Fatal(message); private void 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 にサンプルを公開しています。