C#のログ保存方法(TraceListener使用)

WPFアプリ(C#)でのログ保存方法を書いておきます。
Visual Studio 2017 を使用します。


.NETの標準機能でログを出力する方法はDebugクラスを使うか、Traceクラスを使うかがあります。
ソフトをリリース後でも有効にするためにはTraceクラスを使用する必要があります。
Release BuildのデフォルトではDebugクラスは無視されます。
TraceListenerを使って、ログの出力先、フォーマットを変更する事もでき、TraceListenerをコードではなくアプリケーション構成ファイルapp.configに書いておくことで、Build後でもテキストエディターで出力先を変更できたりします。

Default TraceListener の種類

以下のようなトレースリスナーが標準で用意されています。
System.Diagnostics.DefaultTraceListener
 出力先:トレースの既定の出力
System.Diagnostics.TextWriterTraceListener
 出力先:TextWriter または Stream
System.Diagnostics.EventLogTraceListener
 出力先:EventLog
System.Diagnostics.ConsoleTraceListener
 出力先:標準出力または標準エラーストリーム
System.Diagnostics.XmlWriterTraceListener
 出力先:XML エンコードしてTextWriter または Stream

TextWriterTraceListener の使用方法

一番よくつかわれるのが、ログファイルへの保存用にTextWriterTraceListenerだと思います。
下記の様にアプリケーション構成ファイルapp,configに書くことにより、
Trace.Writeline("ログデータ");
でログファイル(LogData.log)に1行追加する事ができます。
autoflushを"true"に設定しないと、
Trace.Flash();
で意識的に書き込まないといけなくなります。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.diagnostics>
    <trace autoflush="true" indentsize="4">
      <listeners>
        <add name="myListener"
                  type="System.Diagnostics.TextWriterTraceListener"
                  initializeData=".\LogData.log" />
        <remove name="Default" />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

アプリケーション構成ファイルApp.configは、PropertiesのSettings.settingsをクリックして表示された画面を変更した振りをすれば自動生成されます。
Buildすると実行フォルダーに "アプリ名.exe.config"と言う名前でXMLファイルができます。
なので、このXMLファイルを書き換えると、ToraceListenerを入れ替えたり、保存先を変えたりできる事になります。

Traceを使う為には、Build時にビルドオプションで"TRACE定数の定義"がされていることが大前提となります。デフォルトでチェックが入っているとは思いますがチェックポイントです。

カスタムトレースリスナーの使用方法

次のサイトのものを使わせていただきました。
www.pine4.net

アプリケーション構成ファイルで、保存フォルダーと日付付きファイル名、保存ファイルサイズを超えた時の分割ファイルのサフィックスフォーマット、保存文字エンコードが指定できます。
カスタムトレースリスナーであるDateTimeTraceListenerでは、書き込む前に次のように日時を加えています。
Write(DateTime.Now + "," + message + Environment.NewLine);
このカスタムトレースリスナーで書き込んだログは
ファイル名:2019032100.log
2019/03/21 22:32:05,テスト

の様になります。

アプリケーション構成ファイル

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!-- トレース設定 -->
  <system.diagnostics>
    <sources>
      <source name="LogSource" switchName="TraceSwitch"
        switchType="System.Diagnostics.SourceSwitch" >
        <listeners>
          <add name="datetime" />
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="TraceSwitch" value="Information"/>
    </switches>
    <sharedListeners>
      <add name="datetime" type="TraceListeners.DateTimeTraceListener, TraceListeners"
           initializeData=".\Trace\%YYYYMMDD%%SUFFIX%.log" 
           MaxSize="100000" 
           SuffixFormat="D2"
           Encoding="utf-8"
           DateFormat="yyyyMMdd"/>
    </sharedListeners>
    <trace autoflush="true" indentsize="4">
      <listeners>
        <add name="datetime" />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

Visual Studio の IntelliSense で、MaxSize等の箇所で「MaxSize属性は使用できません」等出てきますが、問題なく機能します。
MaxSizeの値を小さくすると、ファイルが分割されることを確認できます。

カスタムトレースリスナー

using System;
using System.Text;
using System.Diagnostics;
using System.IO;

namespace TraceListeners
{
    //下記をapp.configに追加するとTraceListenerが使えるようになる。
    //app.configはBuild後 projectname.exe.config になるのでEditorで編集可能
    //保存ファイルパス:     initializeData=".\Trace\%YYYYMMDD%%SUFFIX%.log" 
    //保存ファイルサイズ:    MaxSize="100000" 
    //%SUFFIX%のフォーマット:  SuffixFormat="D2"
    //保存エンコード:          Encoding="utf-8" or "Shift-JIS"
    //%YYYYMMDD%のフォーマット: DateFormat="yyyyMMdd"
    //
    //<system.diagnostics>
    //  <sources>
    //    <source name = "LogSource" switchName="TraceSwitch"
    //      switchType="System.Diagnostics.SourceSwitch" >
    //      <listeners>
    //        <add name = "datetime" />
    //      </ listeners >
    //    </ source >
    //  </ sources >
    //  < switches >
    //    < add name="TraceSwitch" value="Information"/>
    //  </switches>
    //  <sharedListeners>
    //    <add name = "datetime" 
    //         type="TraceListeners.DateTimeTraceListener, TraceListeners"
    //         initializeData=".\Trace\%YYYYMMDD%%SUFFIX%.log" 
    //         MaxSize="100000" 
    //         SuffixFormat="D2"
    //         Encoding="utf-8"
    //         DateFormat="yyyyMMdd"/>
    //  </sharedListeners>
    //  <trace autoflush = "true" indentsize="4">
    //    <listeners>
    //      <add name = "datetime" />
    //    </ listeners >
    //  </ trace >
    //</ system.diagnostics >

    /// <summary>
    /// DateTimeを足しこんだログを書き込む
    /// ログファイルが最大を超えていたらsuffixをインクリメントする
    /// </summary>
    public class DateTimeTraceListener : TraceListener
    {
        private string _fileNameTemplate = null;
        /// <summary>
        /// ファイル名のテンプレート
        /// </summary>
        private string FileNameTemplate
        {
            get { return _fileNameTemplate; }
        }

        private string _dateFormat = "yyyyMMdd";
        /// <summary>
        /// 日付部分のテンプレート
        /// </summary>
        private string DateFormat
        {
            get { LoadAttribute(); return _dateFormat; }
        }
        private string _suffixFormat = "";
        /// <summary>
        /// ファイルナンバー部分のテンプレート
        /// </summary>
        private string SuffixFormat
        {
            get { LoadAttribute(); return _suffixFormat; }
        }
        private string _datePlaceHolder = "%YYYYMMDD%";
        /// <summary>
        /// ファイル名テンプレートに含まれる日付のプレースホルダ
        /// </summary>
        private string DatePlaceHolder
        {
            get { LoadAttribute(); return _datePlaceHolder; }
        }
        private string _suffixPlaceHolder = "%SUFFIX%";
        /// <summary>
        /// ファイル名テンプレートに含まれるバージョンのプレースフォルダ
        /// </summary>
        private string SuffixPlaceHolder
        {
            get { LoadAttribute(); return _suffixPlaceHolder; }
        }
        private long _maxSize = 10 * 1024 * 1024;
        /// <summary>
        /// トレースファイルの最大バイト数
        /// </summary>
        private long MaxSize
        {
            get { LoadAttribute(); return _maxSize; }
        }
        private Encoding _encoding = Encoding.GetEncoding("utf-8");
        /// <summary>
        /// 出力ファイルのエンコーディング
        /// </summary>
        private Encoding Encoding
        {
            get { LoadAttribute(); return _encoding; }
        }

        #region 内部使用フィールド
        /// <summary>
        /// 出力バッファストリーム
        /// </summary>
        private TextWriter _stream = null;
        /// <summary>
        /// 実際に出力されるストリーム
        /// </summary>
        private Stream _baseStream = null;
        /// <summary>
        /// 現在のログ日付
        /// </summary>
        private DateTime _logDate = DateTime.MinValue;
        /// <summary>
        /// バッファサイズ
        /// </summary>
        private int _bufferSize = 4096;
        /// <summary>
        /// ロックオブジェクト
        /// </summary>
        private object _lockObj = new Object();
        /// <summary>
        /// カスタム属性読み込みフラグ
        /// </summary>
        private bool _attributeLoaded = false;
        #endregion

        /// <summary>
        /// スレッドセーフ
        /// </summary>
        public override bool IsThreadSafe
        {
            get
            {
                return true;
            }
        }
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="fileNameTemplate">ファイル名のテンプレート</param>
        public DateTimeTraceListener(string fileNameTemplate)
        {
            _fileNameTemplate = fileNameTemplate;
        }
        /// <summary>
        /// メッセージを出力します
        /// </summary>
        /// <param name="message"></param>
        public override void Write(string message)
        {
            lock (_lockObj)
            {
                if (EnsureTextWriter())
                {
                    if (NeedIndent)
                    {
                        WriteIndent();
                    }
                    _stream.Write(message);
                }
            }
        }
        public override void WriteLine(string message)
        {
            Write(DateTime.Now + "," + message + Environment.NewLine);
        }
        public override void Close()
        {
            lock (_lockObj)
            {
                if (_stream != null)
                {
                    _stream.Close();
                }
                _stream = null;
                _baseStream = null;
            }
        }
        public override void Flush()
        {
            lock (_lockObj)
            {
                if (_stream != null)
                {
                    _stream.Flush();
                }
            }
        }
        /// <summary>
        /// Dispose処理
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                Close();
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// 出力ストリームを準備する
        /// </summary>
        /// <returns></returns>
        private bool EnsureTextWriter()
        {
            if (string.IsNullOrEmpty(FileNameTemplate)) return false;

            DateTime now = DateTime.Now;
            if (_logDate.Date != now.Date)
            {
                Close();
            }
            if (_stream != null && _baseStream.Length > MaxSize)
            {
                Close();
            }
            if (_stream == null)
            {
                string filepath = NextFileName(now);
                // フルパスを求めると同時にファイル名に不正文字がないことの検証
                string fullpath = Path.GetFullPath(filepath);

                StreamWriter writer = 
                    new StreamWriter(fullpath,true,Encoding,_bufferSize);
                _stream = writer;
                _baseStream = writer.BaseStream;
                _logDate = now;
            }

            return true;
        }

        /// <summary>
        /// パスで指定されたディレクトリが存在しなければ
        /// 作成します。
        /// </summary>
        /// <param name="dirpath">ディレクトリのパス</param>
        /// <returns>作成した場合はtrue</returns>
        private bool CreateDirectoryIfNotExists(string dirpath)
        {
            if (!Directory.Exists(dirpath))
            {
                // 同時に作成してもエラーにならないため例外処理をしない
                Directory.CreateDirectory(dirpath);
                return true;
            }
            return false;
        }
        /// <summary>
        /// 指定されたファイルがログファイルとして使用できるかの判定を行う
        /// </summary>
        /// <param name="filepath"></param>
        /// <returns></returns>
        private bool IsValidLogFile(string filepath)
        {
            if (File.Exists(filepath))
            {
                FileInfo fi = new FileInfo(filepath);
                // 最大サイズより小さければ追記書き込みできるので OK
                if (fi.Length < MaxSize)
                {
                    return true;
                }
                // 最大サイズ以上でもサフィックスをサポートをしていない場合はOK
                if (!FileNameTemplate.Contains(SuffixPlaceHolder))
                {
                    return true;
                }
                // そうでない場合はNG
                return false;
            }
            return true;
        }
        /// <summary>
        /// 日付に基づくサフィックスつきのログファイルのパスを作成する。
        /// </summary>
        /// <param name="logDateTime">ログ日付</param>
        /// <returns></returns>
        private string NextFileName(DateTime logDateTime)
        {
            int suffix = 0;
            string filepath = ResolveFileName(logDateTime, suffix);
            string dir = Path.GetDirectoryName(filepath);
            CreateDirectoryIfNotExists(dir);

            while (!IsValidLogFile(filepath))
            {
                ++suffix;
                filepath = ResolveFileName(logDateTime, suffix);
            }

            return filepath;
        }
        /// <summary>
        /// ファイル名のテンプレートから日付バージョンを置き換えるヘルパ
        /// </summary>
        /// <param name="logDateTime"></param>
        /// <param name="version"></param>
        /// <returns></returns>
        private string ResolveFileName(DateTime logDateTime, int suffix)
        {
            string t = FileNameTemplate;
            if (t.Contains(DatePlaceHolder))
            {
                t = t.Replace(DatePlaceHolder,logDateTime.ToString(DateFormat));
            }
            if (t.Contains(SuffixPlaceHolder))
            {
                t = t.Replace(SuffixPlaceHolder,suffix.ToString(SuffixFormat));
            }
            return t;
        }

        #region カスタム属性用
        /// <summary>
        /// サポートされているカスタム属性
        /// MaxSize : logfileの最大size
        /// Encoding: 文字code
        /// DateFormat:logfile名の日付部分のformat文字列
        /// VersionFormat: logfileのSuffix部分のformat字列
        /// DatePlaceHolder: file名templateの日付部分のPlaceHolder文字列
        /// VersionPlaceHolder: file名templateのSuffix部分のPlaceHolder文字列
        /// </summary>
        /// <returns></returns>
        protected override string[] GetSupportedAttributes()
        {
            return new string[] { "MaxSize", "Encoding", "DateFormat",
                "SuffixFormat","DatePlaceHolder","SuffixPlaceHolder" };
        }
        /// <summary>
        /// カスタム属性
        /// </summary>
        private void LoadAttribute()
        {
            if (!_attributeLoaded)
            {
                // 最大バイト数
                if (Attributes.ContainsKey("MaxSize"))
                    { _maxSize = long.Parse(Attributes["MaxSize"]); }
                // エンコーディング
                if (Attributes.ContainsKey("Encoding"))
                    { _encoding = Encoding.GetEncoding(Attributes["Encoding"]); }
                // 日付のフォーマット
                if (Attributes.ContainsKey("DateFormat"))
                    { _dateFormat = Attributes["DateFormat"]; }
                // バージョンのフォーマット
                if (Attributes.ContainsKey("SuffixFormat"))
                    { _suffixFormat = Attributes["SuffixFormat"]; }
                // 日付のプレースホルダ
                if (Attributes.ContainsKey("DatePlaceHolder"))
                    { _datePlaceHolder = Attributes["DatePlaceHolder"]; }
                // ナンバーのプレースホルダ
                if (Attributes.ContainsKey("SuffixPlaceHolder"))
                    { _suffixPlaceHolder = Attributes["SuffixPlaceHolder"]; }

                _attributeLoaded = true;
            }
        }
        #endregion
    }
}


以上の内容のサンプルソフトを下記に置いておきます。
ViewModelのコンストラクターでログに書き込みを行っているので、アプリの起動のみでログファイルへの書き込みが確認できると思います。

        public MainWindowViewModel()
        {
            string st = "";
            for(int i = 0;i < 10; i++)
            {
                st = "テスト:" + i.ToString("D2");
                Trace.WriteLine(st);
            }
            
        }

github.com