ASP.NET CoreでSystem.Diagnostic.TraceをNLogに流し込む方法

やりたいこと
--
`ASP.NET Core 2.x`を使った環境において、`Trace.TraceError`や`Trace.TraceInformation`で書き出される内容を`NLog`で記録したい。

なにがうれしい？
--
標準の`System.Diagnostic.Trace`でログを吐けるので、各RazorPages／ControllerでLoggerを宣言・DIしなくて済む。
NLogで物理ファイルに吐き出せるので Azure Application Insights ではそのうち消えてしまう生ログが永続化できる。
NLog以外のロガーパッケージを導入する際に変更が容易（未検証）。


環境等
--
###### ターゲットフレームワーク
- .NET Core 2.0

###### 使用パッケージ
- Microsoft.AspNetCore.All 2.0.5
- NLog 4.5.7
- NLog.Web.AspNetCore 4.5.4

###### 開発環境
- Windows 10 Pro
- Visual Studio Enterprise 2017 v15.7.5

実装
--
[公式ページ](https://github.com/NLog/NLog.Web/wiki/Getting-started-with-ASP.NET-Core-2)の 0,1,2,3 を参考にパッケージの導入＆設定ファイルの追加まで行なってください。
参考にオレオレ設定も添付します。

```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"
      autoReload="true"
      internalLogLevel="info"
      internalLogFile="c:\temp\internal-nlog.txt">

  <!-- enable asp.net core layout renderers -->
  <extensions>
    <add assembly="NLog.Web.AspNetCore"/>
  </extensions>

  <variable name="standardLayout" value="${date:universalTime=false:format=yyyy-MM-dd HH\:mm\:ss.fffK:culture=ja-JP} ${uppercase:${level}} ${message}"/>
  <!-- よく使うcallsiteは今回においてはうまく機能しなかった。後述。 -->

  <targets>
    <!--
      これ↓らの記述で出力できる。Info/Warn/Error/Debug
      Trace.TraceInformation("Trace.TraceInformation");
      Trace.TraceWarning("Trace.TraceWarning");
      Trace.TraceError("Trace.TraceError");
      Trace.WriteLine("Trace.WriteLine");
    -->

    <target xsi:type="File" name="logfileDebug" fileName="${basedir}/logs/${shortdate}_Debug.log"
            layout="${standardLayout}" />

    <target xsi:type="File" name="logfileInfo" fileName="${basedir}/logs/${shortdate}_Info.log"
            layout="${standardLayout}" />

    <target xsi:type="File" name="logfileWarn" fileName="${basedir}/logs/${shortdate}_Warn.log"
            layout="${standardLayout}" />

    <target xsi:type="File" name="logfileError" fileName="${basedir}/logs/${shortdate}_Error.log"
            layout="${standardLayout} ${exception:format=ToString}" />

  </targets>

  <rules>
    <logger name="*" minlevel="Debug" maxlevel="Debug" writeTo="logfileDebug" />
    <logger name="*" minlevel="Warn" maxlevel="Warn"  writeTo="logfileWarn" />
    <logger name="*" minlevel="Error" writeTo="logfileError" />
    <logger name="Microsoft.*" minlevel="Info" maxlevel="Info" final="true" />
    <logger name="*" minlevel="Info" maxlevel="Info" writeTo="logfileInfo" />
  </rules>
</nlog>
```

次に**公式ページの手順4は無視**し、以下の実装を行なってください。

```Startup.cs
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            //～略～

            //NLogを使う設定を宣言し
            env.ConfigureNLog("nlog.config");
            loggerFactory.AddNLog();
            //さらにNLogに流し込むListenerを追加することをでTrace.Xxxxでの出力がNLog設定で書き出される
            Trace.Listeners.Add(new NLogTraceListener()
            {
                IndentSize = 4,
                Name = "MyNLogTraceListener",
            });

            //本来もっと前に記録したいところだけど記述準的にここで初めてTraceをNLogに流し込めるようになるのでここでAppStartログ
            Trace.TraceInformation("Appcation Start!");
        }
```

そして公式ページの手順5の通り設定を変更し、書き出されるログに情報を詰めるためのサポートクラスを追加します。
これは本来`NLog`の`callsite`で賄える呼び出し情報等がうまく取れなかったための措置です。
※どこからログを書いても`callsite`は常に`System.Diagnostics.TraceInternal.WriteLine`になってしまっていた

```MyLogger.cs
    public static class MyLogger
    {
        private static string Format(string sourceFilePath, string memberName, int sourceLineNumber, string message)
        {
            var fileName = sourceFilePath.Substring(sourceFilePath.LastIndexOf("\\") + 1);
            return $"{fileName} {memberName} {sourceLineNumber} {message}";
        }
        public static void Info(string message,
            [CallerFilePath] string sourceFilePath = "",
            [CallerMemberName] string memberName = "",
            [CallerLineNumber] int sourceLineNumber = 0)
        {
            Trace.TraceInformation(Format(sourceFilePath, memberName, sourceLineNumber, message));
        }

        public static void Warn(string message,
            [CallerFilePath] string sourceFilePath = "",
            [CallerMemberName] string memberName = "",
            [CallerLineNumber] int sourceLineNumber = 0)
        {
            Trace.TraceWarning(Format(sourceFilePath, memberName, sourceLineNumber, message));
        }

        public static void Error(string message,
            [CallerFilePath] string sourceFilePath = "",
            [CallerMemberName] string memberName = "",
            [CallerLineNumber] int sourceLineNumber = 0,
            Exception ex = null)
        {
            if (ex == null)
            {
                Trace.TraceError(Format(sourceFilePath, memberName, sourceLineNumber, message));
            }
            else
            {
                Trace.TraceError(Format(sourceFilePath, memberName, sourceLineNumber, message) + Environment.NewLine + ex.ExtractException());
            }
        }

        public static void Debug(string message,
            [CallerFilePath] string sourceFilePath = "",
            [CallerMemberName] string memberName = "",
            [CallerLineNumber] int sourceLineNumber = 0)
        {
            Trace.WriteLine(Format(sourceFilePath, memberName, sourceLineNumber, message));
        }
    }
```
```ExceptionHelper.cs
    // https://teratail.com/questions/24669
    public static class ExceptionHelper
    {
        public static string ExtractException(this Exception ex, int indent = 2)
        {
            var indentStr = new String(' ',indent);
            StringBuilder traceLog = new StringBuilder();
            StackTrace trace = new StackTrace(ex, true);
            foreach (var frame in trace.GetFrames())
            {
                traceLog.AppendLine($"{indentStr}File Name : {frame.GetFileName()}");
                traceLog.AppendLine($"{indentStr}Class Name : {frame.GetMethod().ReflectedType.Name}");
                traceLog.AppendLine($"{indentStr}Method Name : {frame.GetMethod()}");
                traceLog.AppendLine($"{indentStr}Line Number : {frame.GetFileLineNumber()}");
                traceLog.AppendLine($"=======================================================");
            }

            return traceLog.ToString();
        }
    }
```
あとは好きに、ログを残したい場所に`MyLogger.Info`なり`MyLogger.Error`なり書くことで、所定のフォルダの`logs`配下に書き出されていきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/78880/1776a55a-6d6c-dfc4-0392-de0a5e172d75.png)


参考
--
[Automatically log System.diagnostics.trace messages to an Nlog target](https://stackoverflow.com/questions/13260672/automatically-log-system-diagnostics-trace-messages-to-an-nlog-target)
[Routing System.Diagnostics.Trace and System.Diagnostics.TraceSource logs through NLog](http://nlog-project.org/2010/09/02/routing-system-diagnostics-trace-and-system-diagnostics-tracesource-logs-through-nlog.html)
[Getting started with ASP.NET Core 2](https://github.com/NLog/NLog.Web/wiki/Getting-started-with-ASP.NET-Core-2)
[Azureの診断モニタにエラーログのソース行数を表示する方法](https://teratail.com/questions/24669)
[呼び出し元情報 (C#)](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/caller-information)
[方法 : トレース リスナーを作成し初期化する](https://docs.microsoft.com/ja-jp/dotnet/framework/debug-trace-profile/how-to-create-and-initialize-trace-listeners)
[how to route System.Diagnostics.TraceSource logs through NLog in .NET Core](https://github.com/NLog/NLog/issues/1711)

最後に
--
「nlog.config」が小文字じゃないと動かなかったり、`Startup.cs`ではなく`Program.cs`で設定するサンプルがあったりで割と素直にいきませんでした。
結局ログは`Trace.TraceXxxx`ではなく`MyLogger.Xxxx`で書き出していますがｼｶﾀﾅｲﾈ。

誰かの助けになれば！