.NET で開発する場合、 TraceLister を継承した トレースリスナ や log4net 、Enterprise Library に含まれるログライブラリを使用できると思います。残念ながらお客さんによっては .NET Framework で標準に付属するライブラリ以外は使用できないケースもあると思います。 24時間稼動するシステムにとっては TextWriterTraceListener の機能はあまりに貧弱です。やはり日毎に記録でき、 一定のサイズになったら枝番などをつけてログファイルの切り替えを行う機能が欲しいところです。

日別やサイズごとの切り替えは VisualBasic の名前空間に含まれるカスタムトレースリスナ FileLogTraceListener で実現できます。しかもスレッドセーフです。残念ながら今回は 日毎に分けたフォルダにトレースログを出力できる必要があったので、FileLogTraceListener を採用できませんでした。 FileLogTraceListener はファイル名に対して 年月日を 含めることができますが、日付ごとフォルダに分けて出力する機能はないです(私の知る限り)。

というわけで次の機能をもつカスタムトレースリスナを作成してみました。

機能 説明
日別ログ出力機能 日別で出力先を変更できるようにしています。年月日はフォルダ、ファイル名いづれにも設定可能で、書式を指定できるようにしています。なので、月別にログ出力を変えるなんてこともできます。
バージョニング機能

ログファイルが特定のサイズを超過した場合、枝番をつけて別ファイルにログを出力できるようにしています。今回は、書式指定をすることで枝番の書式を変更できるようにしました。

スレッドセーフ ASP.NET などマルチスレッド環境でトレースを出力しても問題がないようにしました。(つもり。あんま検証していないですが)
 

カスタムトレースリスナはは次の環境で作成しています。

  • Visual Studio 2008 Professional
  • .NET 2.0

1. カスタムトレースリスナの実装

カスタムとレースリスナを実装してみます。カスタムトレースリスナは TraceListener を継承し、 WriteLine, Write メソッドを実装すれば作成できます。今回は トレースリスナを構成ファイルで定義するときにカスタム属性をしようできるように、GetSupportedAttributes もオーバーライドしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;

namespace ComponentGeek.Log
{
    /// <summary>
    /// 日別サイズ別に出力先を変更できるカスタムトレースリスナ
    /// </summary>
    public class DailyTraceListener : 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 _versionFormat = "";
        /// <summary>
        /// ファイルバージョン部分のテンプレート
        /// </summary>
        private string VersionFormat
        {
            get { LoadAttribute(); return _versionFormat; }
        }

        private string _datePlaceHolder = "%YYYYMMDD%";
        /// <summary>
        /// ファイル名テンプレートに含まれる日付のプレースホルダ
        /// </summary>
        private string DatePlaceHolder
        {
            get { LoadAttribute(); return _datePlaceHolder; }
        }

        private string _versionPlaceHolder = "%VERSION%";
        /// <summary>
        /// ファイル名テンプレートに含まれるバージョンのプレースフォルダ
        /// </summary>
        private string VersionPlaceHolder
        {
            get { LoadAttribute(); return _versionPlaceHolder; }
        }

        private long _maxSize = 10 * 1024 * 1024;
        /// <summary>
        /// トレースファイルの最大バイト数
        /// </summary>
        private long MaxSize
        {
            get { LoadAttribute(); return _maxSize; }
        }
        private Encoding _encoding = Encoding.GetEncoding("Shift_JIS");
        /// <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 DailyTraceListener(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(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>
        /// 廃棄処理
        /// </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(VersionPlaceHolder))
                {
                    return true;
                }
                // そうでない場合はNG
                return false;
            }
            return true;
        }
        /// <summary>
        /// 日付に基づくバージョンつきのログファイルのパスを作成する。
        /// </summary>
        /// <param name="logDateTime">ログ日付</param>
        /// <returns></returns>
        private string NextFileName(DateTime logDateTime)
        {
            int version = 0;
            string filepath = ResolveFileName(logDateTime, version);
            string dir = Path.GetDirectoryName(filepath);
            CreateDirectoryIfNotExists(dir);

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

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

        #region カスタム属性用
        /// <summary>
        /// サポートされているカスタム属性
        /// MaxSize : ログファイルの最大サイズ
        /// Encoding: 文字コード
        /// DateFormat:ログファイル名の日付部分のフォーマット文字列
        /// VersionFormat: ログファイルのバージョン部分のフォーマット文字列
        /// DatePlaceHolder: ファイル名テンプレートの日付部分のプレースホルダ文字列
        /// VersionPlaceHolder: ファイル名テンプレートのバージョブ部分のプレースホルダ文字列
        /// </summary>
        /// <returns></returns>
        protected override string[] GetSupportedAttributes()
        {
            return new string[] { "MaxSize", "Encoding", "DateFormat", "VersionFormat"
                , "DatePlaceHolder", "VersionPlaceHolder" };
        }
        /// <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("VersionFormat")) { _versionFormat = Attributes["VersionFormat"]; }
                // 日付のプレースホルダ
                if (Attributes.ContainsKey("DatePlaceHolder")) { _datePlaceHolder = Attributes["DatePlaceHolder"]; }
                // バージョンのプレースホルダ
                if (Attributes.ContainsKey("VersionPlaceHolder")) { _versionPlaceHolder = Attributes["VersionPlaceHolder"]; }

                _attributeLoaded = true;
            }
        }
        #endregion
    }
}

2. 構成ファイルの定義例

作成したトレースリスナを使う場合のアプリケーション構成ファイルのサンプルです。カスタム属性に MaxSize="4096" と指定してファイルサイズが4KBを超えたら、ファイルを切り替えるように指定しています。トレースソース名に LogSource と指定しているので、 プログラム内で TraceSource クラスのインスタンスを作成してログの出力を行ったり Trace クラスを使用して出力を行ったりできるようになります。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!-- トレース設定 -->
  <system.diagnostics>
    <sources>
      <source name="LogSource" switchName="TraceSwitch"
        switchType="System.Diagnostics.SourceSwitch" >
        <listeners>
          <add name="daily" />
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="TraceSwitch" value="Information"/>
    </switches>
    <sharedListeners>
      <add name="daily" type="ComponentGeek.Log.DailyTraceListener, ComponentGeek.Log" 
           initializeData="C:\\Trace\%YYYYMMDD%\CustomTrace_%VERSION%.log" MaxSize="4096" />
    </sharedListeners>
    <trace autoflush="true" indentsize="4">
      <listeners>
        <add name="daily" />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

3. まとめ

今回の説明は以上です。今回のカスタムとレースリスナの作成には次のページを参考にさせていただきました。

カスタム トレース リスナの技術サンプル
http://msdn.microsoft.com/ja-jp/library/ms180974.aspx

あんまり詳細に動作確認していないですが、間違い指摘点などありましたらご連絡ください。