[コピペでOK] C#でログを出力する

# はじめに

C#でログを出力するLoggerを作成しました。
C#では、便利なライブラリがありますが、
ここでは、コピペですぐに使えるものを目指しています。

機能としては下記の通りです。

- ログレベル
- ログローテート
- 出力先指定

下記にC#でログ出力を行うライブラリを紹介しているリンクを記載します。

- [NLogでログを記録する](https://qiita.com/hsagae/items/9f09e6ee204745fd2481)
- [Log4Net を利用してログを記録する](https://qiita.com/rohinomiya/items/2b86c4e8d5afd5c2fb39)

# 実行環境

今回実行した環境は下記の通りです。

```Markdown:実行環境
Microsoft .NET Framework 3.5
```

C#の古いバージョンでも動作するように実装しています。

# 使い方

使い方は下記の通りです。

```C#
Logger log = Logger.GetInstance();
log.Error("error log.");
log.Warn("warn log.");
log.Info("info log.");
log.Debug("debug log.");
```

出力されるログは下記の通りです。

```log
[2020-04-08 22:38:56.081][1][DEBUG] debug log.
[2020-04-08 22:38:56.082][1][ERROR] error log.
[2020-04-08 22:38:56.082][1][WARN] warn log.
[2020-04-08 22:38:56.082][1][INFO] info log.
```

「1」この数字は、プロセスIDを表示しています。

# Loggerの実装

configでLoggerに関する設定を行います。

```xml:app.config
<setting name="IS_LOGFILE" serializeAs="String">
    <value>True</value>
</setting>
<setting name="LOG_LEVEL" serializeAs="String">
    <value>3</value>
</setting>
<setting name="LOGDIR_PATH" serializeAs="String">
    <value>./logs/</value>
</setting>
<setting name="LOGFILE_NAME" serializeAs="String">
    <value>console</value>
</setting>
<setting name="LOGFILE_MAXSIZE" serializeAs="String">
    <value>10485760</value>
</setting>
<setting name="LOGFILE_PERIOD" serializeAs="String">
    <value>30</value>
</setting>
``` 

- 「IS_LOGFILE」はログファイル出力フラグです。
- 「LOG_LEVEL」はログレベルです。
- 「LOGDIR_PATH」はログファイル出力ディレクトリです。
- 「LOGFILE_NAME」はログファイル名です。
- 「LOGFILE_MAXSIZE」はログファイル最大サイズ（Byte）です。
- 「LOGFILE_PERIOD」はログ保存期間（日）です。

Loggerクラスの実装は下記の通りです。

```C#:Logger.cs
class Logger
{
    /// <summary>
    /// ログレベル
    /// </summary>
    private enum LogLevel
    {
        ERROR,
        WARN,
        INFO,
        DEBUG
    }

    private static Logger singleton = null;
    private readonly string logFilePath = null;
    private readonly object lockObj = new object();
    private StreamWriter stream = null;

    /// <summary>
    /// インスタンスを生成する
    /// </summary>
    public static Logger GetInstance()
    {
        if (singleton == null)
        {
            singleton = new Logger();
        }
        return singleton;
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    private Logger()
    {
        this.logFilePath = Settings.Default.LOGDIR_PATH + Settings.Default.LOGFILE_NAME + ".log";

        // ログファイルを生成する
        CreateLogfile(new FileInfo(logFilePath));
    }

    /// <summary>
    /// ERRORレベルのログを出力する
    /// </summary>
    /// <param name="msg">メッセージ</param>
    public void Error(string msg)
    {
        if ((int)LogLevel.ERROR <= Settings.Default.LOG_LEVEL)
        {
            Out(LogLevel.ERROR, msg);
        }
    }

    /// <summary>
    /// ERRORレベルのスタックトレースログを出力する
    /// </summary>
    /// <param name="ex">例外オブジェクト</param>
    public void Error(Exception ex)
    {
        if ((int)LogLevel.ERROR <= Settings.Default.LOG_LEVEL)
        {
            Out(LogLevel.ERROR, ex.Message + Environment.NewLine + ex.StackTrace);
        }
    }

    /// <summary>
    /// WARNレベルのログを出力する
    /// </summary>
    /// <param name="msg">メッセージ</param>
    public void Warn(string msg)
    {
        if ((int)LogLevel.WARN <= Settings.Default.LOG_LEVEL)
        {
            Out(LogLevel.WARN, msg);
        }
    }

    /// <summary>
    /// INFOレベルのログを出力する
    /// </summary>
    /// <param name="msg">メッセージ</param>
    public void Info(string msg)
    {
        if ((int)LogLevel.INFO <= Settings.Default.LOG_LEVEL)
        {
            Out(LogLevel.INFO, msg);
        }
    }

    /// <summary>
    /// DEBUGレベルのログを出力する
    /// </summary>
    /// <param name="msg">メッセージ</param>
    public void Debug(string msg)
    {
        if ((int)LogLevel.DEBUG <= Properties.Settings.Default.LOG_LEVEL)
        {
            Out(LogLevel.DEBUG, msg);
        }
    }

    /// <summary>
    /// ログを出力する
    /// </summary>
    /// <param name="level">ログレベル</param>
    /// <param name="msg">メッセージ</param>
    private void Out(LogLevel level, string msg)
    {
        if (Settings.Default.IS_LOGFILE)
        {
            int tid = System.Threading.Thread.CurrentThread.ManagedThreadId;
            string fullMsg = string.Format("[{0}][{1}][{2}] {3}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), tid, level.ToString(), msg);

            lock (this.lockObj)
            {
                this.stream.WriteLine(fullMsg);

                FileInfo logFile = new FileInfo(this.logFilePath);
                if (Settings.Default.LOGFILE_MAXSIZE < logFile.Length)
                {
                    // ログファイルを圧縮する
                    CompressLogFile();

                    // 古いログファイルを削除する
                    DeleteOldLogFile();
                }
            }
        }
    }

    /// <summary>
    /// ログファイルを生成する
    /// </summary>
    /// <param name="logFile">ファイル情報</param>
    private void CreateLogfile(FileInfo logFile)
    {
        if (!Directory.Exists(logFile.DirectoryName))
        {
            Directory.CreateDirectory(logFile.DirectoryName);
        }

        this.stream = new StreamWriter(logFile.FullName, true, Encoding.UTF8)
        {
            AutoFlush = true
        };
    }

    /// <summary>
    /// ログファイルを圧縮する
    /// </summary>
    private void CompressLogFile()
    {
        this.stream.Close();
        string oldFilePath = Settings.Default.LOGDIR_PATH + Settings.Default.LOGFILE_NAME + "_" + DateTime.Now.ToString("yyyyMMddHHmmss");
        File.Move(this.logFilePath, oldFilePath + ".log");

        FileStream inStream = new FileStream(oldFilePath + ".log", FileMode.Open, FileAccess.Read);
        FileStream outStream = new FileStream(oldFilePath + ".gz", FileMode.Create, FileAccess.Write);
        GZipStream gzStream = new GZipStream(outStream, CompressionMode.Compress);

        int size = 0;
        byte[] buffer = new byte[Settings.Default.LOGFILE_MAXSIZE + 1000];
        while (0 < (size = inStream.Read(buffer, 0, buffer.Length)))
        {
            gzStream.Write(buffer, 0, size);
        }

        inStream.Close();
        gzStream.Close();
        outStream.Close();

        File.Delete(oldFilePath + ".log");
        CreateLogfile(new FileInfo(this.logFilePath));
    }

    /// <summary>
    /// 古いログファイルを削除する
    /// </summary>
    private void DeleteOldLogFile()
    {
        Regex regex = new Regex(Settings.Default.LOGFILE_NAME + @"_(\d{14}).*\.gz");
        DateTime retentionDate = DateTime.Today.AddDays(-Settings.Default.LOGFILE_PERIOD);
        string[] filePathList = Directory.GetFiles(Settings.Default.LOGDIR_PATH, Settings.Default.LOGFILE_NAME + "_*.gz", SearchOption.TopDirectoryOnly);
        foreach (string filePath in filePathList)
        {
            Match match = regex.Match(filePath);
            if (match.Success)
            {
                DateTime logCreatedDate = DateTime.ParseExact(match.Groups[1].Value.ToString(), "yyyyMMddHHmmss", null);
                if (logCreatedDate < retentionDate)
                {
                    File.Delete(filePath);
                }
            }
        }
    }
}
```

スレッドセーフにするために、lockを使用しています。

# さいごに

ソースコードをGitHubに公開しています。

[ソースファイルはこちら](https://github.com/yunbow/CSharp-logger)

以上です。