Skip to content
/ CFLog Public

CFLog C# Logger Windows .NET 6 以降のためのログライブラリ

License

Notifications You must be signed in to change notification settings

explasm/CFLog

Repository files navigation

CFLog C# Windows .NET 6以降のためのログライブラリ

目次

◆1.特徴

日本の開発者向けOSS
    日本人開発者向けにソースコードのコメントなどはすべて日本語です。
    実行時は日本語か英語のテキストリソースが選択されます。(テキストリソースは CFLog 自身がログファイルに書出すメッセージや例外メッセージに使用されます)
把握しやすい仕様
    商用のアプリケーション開発でOSSを採用することは容易ではありません。
    多くの OSS は仕様についてのドキュメントが不十分で、唯一のドキュメントであるソースコードの解析にかかる工数も非現実的であると判断されがちです。
    CFLog は実用性を重視した設計とした上でソースコードには日本語でコメントを、また本ドキュメントで仕様の詳細を明らかにします。
xUnit での自動テストプロジェクトを同梱
    OSS の採用問題でもう1つ大きな要素に信頼性があります。
    CFLog は xUnit テストプロジェクトを同梱しています。自動実行で容易に再現可能な試験項目を確認すれば安心材料になるはずです。 また、これは CFLog 自体のカスタマイズにも役立ちます。
    ★ xUnit による試験は自動のものと人による確認を必要とするものがあります。そのため、全ての試験項目を一気に実行してもうまくいきません。後述の[4.xUnit による試験実施方法]を確認してください。
試験項目ドキュメントを別途提供可能
    必要であれば、試験項目と実施エビデンスを含むドキュメントのご提供が可能です(Excel、日本語)。試験項目には xUnit を使った自動試験、半自動試験が含まれます。項目数は詳細が78で確認数は126となっています。
    プログラムによる結果確認が困難なテストケースは人の目による確認が必要です。そういった項目についての試験結果はエビデンスシートに掲載しています。
    こちらのドキュメントについては有償でのご提供になります。[お問い合わせはこちら]

◆2.仕様・コンセプト

  • Visual Studio 2022 ソリューション、.NET 6 & C# Ver.10にて作成(ターゲットは .NET 6.0 ~ 8.0 Windows)
  • 1プロセスにつき1種類のログファイル
  • 同一アプリを複数起動するマルチプロセスには自動サブフォルダ振分けで対応
  • ログファイルの容量制限はなく、1日1ログファイルで指定保管日数を超えたファイルを削除していく方式
  • アプリケーションクラッシュ時のログ消失率を下げるため、ログ出力ごとに Flush
  • ログ種別のフィルタリングはユーザ指定のラムダ式(デリゲート)
  • スレッドセーフ
  • シングルトン
  • using staticディレクティブとの組合わせでシンプルなコード表記
  • デバッガへのログメッセージ同時出力
  • 実行は日本語と日本語以外(英語)に対応
  • 実行環境とは異なるタイムゾーン指定が可能

◆3.使用方法

★ 同梱の CFLogSampleForm プロジェクトも参考にしてください。

導入

GitHub での公開後に遅れて NuGet パッケージを公開しました。(nuget.org)
ライブラリを使用するだけであれば、ご存じのようにバージョンアップ時にも容易に反映できるため、NuGet がお勧めです。

NuGet

NuGetで "cflog" をキーに検索するとすぐに見つかります。インストールは NuGet の通常の方法で行ってください。
ただし、こちらには使用方法を示すサンプルプロジェクトや xUnit による試験プロジェクト等は含まれません。 必要な場合は GitHub からソースコードを取得してください。

ソースコード利用

GitHub で公開しているのは CFLog を使用するプロジェクトをメインとする Visual Stuidoソリューションです。
ここには CFLog のソースコードや xUnit のテストプロジェクトが含まれます。以下の手順は CFLog だけを使用する場合のものです。
(サンプルプログラムを実行したり、xUnit のテストを実行したりする場合は GitHub で公開されているソリューションをそのままビルドして使用できます。)

ソースコード利用手順 1.CFLog プロジェクトの追加

まず、CFLog プロジェクトフォルダ(ソリューションフォルダではなく)を使用するソリューション(ユーザソリューション)にコピーします。
ユーザソリューションで、既存のプロジェクトの追加を行い、CFLog プロジェクト(CFLog.csprojファイル)を選択して追加します。

ソースコード利用手順 2.プロジェクト参照の追加

使用するプロジェクトで CFLog への参照を追加してください。

ソースコード利用手順 3.追加した CFLog プロジェクトのプロパティ

CFLogプロジェクトはマルチターゲット設定にしています。不都合があれば変更してください。

CFLog.csproj(部分)

<PropertyGroup>
   <TargetFrameworks>net6.0-windows;net7.0-windows;net8.0-windows</TargetFrameworks>
   ...
</PropertyGroup>
...

CFlogを使用するコード

CFLog を使用するプロジェクトの global using に定義を追加

using static CFLog.Logger;
using static CFLog.Logger.LogType;

(Visual Studio 2022では、以上の設定をプロジェクトのプロパティ画面で行うことができます。)

初期化

CFLog の設定はすべて LoggerDef クラスに行い、本体の Logger クラスに渡します。
設定内容については後述の[5.仕様詳細]を参照してください。

Form プロジェクトの場合の開始(終了を含む)コード例

static void Main()
{
    ApplicationConfiguration.Initialize();
    try
    {
        // ログ機能設定
        var logDef = new LoggerDef()
        {
            FILE_PREFIX = "SampleAPP",  // ログファイル名プレフィックスの設定
        };
        // ログ開始
        using(CreateLogger(logDef))
        {
            Application.Run(new SampleForm());
        }
    } catch(LoggerException ex)
    {
        MessageBox.Show(ex.Message);
    }
}

ログ出力

Write()メソッドの1番目のパラメータはログ種別でTDIWEFの6種類から選択します。
2番目はログテキストメッセージ、
3番目のテキストパラメータはオプションで、改行コードが入っても適切にインデントされます。
Write()には例外オブジェクトをパラメータとして受け取るオーバーロードもあります([5.仕様詳細]参照)。

ログ出力コード例

LOG.Write(I, "情報メッセージ");
LOG.Write(E, "エラーメッセージ","追加情報\n何行でもOK");

ログテキスト

ログは開始や終了時にメッセージを書き出します(オプション)。
また、Write()メソッドの呼び出し単位に以下の行ヘッダが付加されます。

  • 日時(ミリ秒まで)
  • プロセスID
  • スレッドID(.NET由来)

また、行ヘッダの直後にはログ種別の記号と、出力したソースファイル名、および行番号も出力されます。

ログ出力例

2024-04-30 16:25:15.422 <25956:01> === ログを開始します ===  [Tokyo Standard Time]
2024-04-30 16:25:17.683 <25956:01> I [Sample01Form.cs(L.12)]: 情報メッセージ
2024-04-30 16:25:17.684 <25956:01> E [Sample01Form.cs(L.13)]: エラーメッセージ
                                   追加情報
                                   何行でもOK
2024-04-30 16:25:19.379 <25956:01> === ログを終了します ===

◆4.xUnitによる試験実施方法

環境

xUnit の試験をすべて成功させるには最低限次の環境条件が必要です。

  • Visual Studio 2022 がインストールされている
  • ネットワーク共有フォルダ\\localhost\CFLogTestLog\Logが定義され、テスト実行者に必要な権限(ファイル一覧取得、ファイル読み・書き・作成、フォルダ作成)が設定されていること
  • Cドライブに一時的にC:\CFLogtest2024フォルダを作成可能なこと(削除も可能なこと)
  • Aドライブが存在しないこと
  • ネットワーク共有フォルダ\\ErrorServer\が存在しないこと
  • ソリューションがデバッグモード(コンパイルシンボルに"DEBUG"があること)でビルドされていること

OSは日本語でなくても実施可能ですが、一部の試験が意味的に形骸化します。
デバッグモードでのビルド(あるいは"DEBUG"シンボルが定義されていること)が必須なのは、 CFLog 本体に試験補助コードがあり、DEBUG シンボルが定義されているときのみ有効にしているからです。

実施

xUnit の試験項目は自動のものと半自動のものがあります。半自動のものは結果を人の目で確認する必要があり、メモ帳が自動的に開きます。 これを閉じるまで待機状態になるため、すべての試験を一度に実行することは避けてください。

  • 自動試験は xUnit のプレイリスト、Auto.playlist にまとめてあります。これを開いて実行すればすべての自動試験が可能です。あるいは、それぞれのテストケースメソッドに設定してある属性(特徴)[Trait("FullAuto", "true")]または[Trait("FullAuto", "false")]により2つのグループに分け、true のグループだけを実行することもできます。
  • 半自動試験は1項目ずつ実行することが前提で、出力されたログファイルをメモ帳で自動的に開きます。それぞれに試験の意味、内容については別途提供する試験項目ドキュメントに記載があります。

◆5.仕様詳細

CFLog の設定

CFLog のメインクラスである Logger クラスへの設定は、すべて LoggerDef クラスを通じて行います。 設定した LoggerDef クラスのインスタンスを Logger クラスの初期化時に渡します。
ここでは各設定値を説明します。
以下は LoggerDef クラスのメンバです。

■ログ出力フォルダ

// 定義と既定値
public string LOG_DIRPATH { get; init; } = @".\Log";

ログファイルを書出すフォルダを指定します。
既定値はアプリケーション実行時フォルダの Log サブフォルダを意味します。
絶対パスの指定も可能ですが、ルートフォルダ(例:@"D:\")を指定することはできません。
ネットワーク共有フォルダの指定は可能ですが、ネットワークの安定性がアプリケーションのボトルネックになることは避けなければなりませんのでお勧めはできません。
ALLOW_MULTIPLE_PROCESSES([同一アプリケーションの複数起動許可]を true に設定した場合は指定フォルダの下にサブフォルダが作られて使用されます。
指定されたフォルダが存在しない場合、CFLog がフォルダを作成します。

■ログファイル名プレフィックス

// 定義と既定値
public string FILE_PREFIX { get; init; } = "CFLog";

CFLog を使用するアプリケーション名にすることが一般的と思われます。
このプレフィックスと日付の数値8桁、後述のFILE_SUFFIX([ログファイル名サフィックス])と合わせてログファイル名になります。

■ログファイル名サフィックス

// 定義と既定値
public string FILE_SUFFIX { get; init; } = "Log.txt";

ログファイル名の拡張子を含む末尾文字列を指定します。
前述のFILE_PREFIX([ログファイル名プレフィックス])に日付の数値8桁が加えられ、最後にこのサフィックスが足されてログファイル名になります。

■ログ保存日数

// 定義と既定値
public int STORAGE_DAYS { get; init; } = 7;

1以上でログ保管日数を示します。0以下にすると無期限になります。
期限を超えたファイルは CFLog 初期化時、および日付変更に伴うログファイル切替時に削除されます。
1を指定した場合、現在出力中のファイルと合わせて最大2日分のファイルが存在することになります。

■ログ出力先フォルダの権限追加設定対象ユーザまたはグループ

// 定義と既定値
public System.Security.Principal.NTAccount? DIR_RIGHTS_TARGET { get; init; } = new (@"BUILTIN\Users");

CFLog では指定されたログ出力フォルダが存在しないときはフォルダを作成します。 その際にフォルダに対して権限の設定を行います。
フォルダが既に存在する場合、権限の設定処理は行われません。
権限の設定は Windows のユーザ、またはユーザグループが対象になりますが、ここではそれを指定します。この処理が不要な場合は null を設定してください。
既定値はビルトインのユーザグループで、通常の Windows アカウントの場合はこのグループに所属します。
このユーザグループに対してログ書き出しの権限(次項DIR_RIGHTS)を設定することで、 同一のログ出力フォルダを使用する、システム管理権限のない他のユーザによりアプリが実行された場合でもログの書出しが可能になります。

ちなみに、ログ出力フォルダの権限設定はインストーラ等の役割になることが一般的と考えられます。 しかし、CFLog は特に後述のALLOW_MULTIPLE_PROCESSES([同一アプリケーションの複数起動許可])を true に設定した場合、 起動されたプロセスごとにサブフォルダを作成する仕組みがあるので、CFLog 自身で権限を設定する機能を備えています。

■ログ出力フォルダ作成時に設定する権限

// 定義と既定値
public System.Security.AccessControl.FileSystemRights DIR_RIGHTS { get; init; } =
    FileSystemRights.FullControl & ~FileSystemRights.ExecuteFile;

前項DIR_RIGHTS_TARGETで指定されたユーザ、またはユーザグループに対し、作成するログ出力フォルダに設定する権限の内容です。
既定値はフルアクセスから実行権限のみを取り除いたものです。

■同一アプリケーションの複数起動許可

// 定義と既定値
public bool ALLOW_MULTIPLE_PROCESSES { get; init; } = false;

同時に複数の起動を行うアプリケーションで CFLog を使用する場合は true を設定します。 ただし、起動するプロセスごとにログ出力フォルダやファイル名が異なる場合はその必要はありません。
もし、同一ログ出力フォルダ、同一ファイル名を使用するアプリケーションを false のまま使用すると、2番目に起動したアプリケーションがログファイルのオープンに失敗します。
CFLog はこれが true に設定されている場合、ログ出力フォルダにの下にサブフォルダを作成してプロセスごとに異なるログファイルを使用します。

この機能は簡易的実装方法により実現されており、同時に起動されるプロセス数が多くなるほど CFLog 使用開始時のパフォーマンスが悪くなっていきます。
処理はプロセスカウント数そのものをサブフォルダ名とし、"1"から順にログファイルのオープンを試みます。既に起動されているプロセスによって使用されているとオープンに失敗します。 この試行をサブフォルダ名をインクリメントしながら繰り返します。
ログファイルがオープンできるまでの間、競合が発生しないようにミューテックスによる排他制御が行われます。 もし、同時に複数のアプリケーションを起動するようなものであれば(アプリケーション起動直後に CFlog の使用を開始する前提)、最後のアプリケーションの起動が完了するまで予想以上の時間がかかるかもしれません。
そういったことが問題になる場合は本設定を false とし、アプリケーション側で各プロセスのログ出力フォルダやファイル名を異なるものにするといった方法を検討してください。

■複数起動の最大許可数

// 定義と既定値
public int MAX_PROCESS_COUNT { get; init; } = 16;

前項ALLOW_MULTIPLE_PROCESSESを true にした場合の最大プロセス起動数。
前項の説明の通り、簡易的な実装のため試行回数の上限を設けています。 既にこの設定値のプロセス数に達している場合、追加のプロセスでは CFLog の初期化は失敗します。

■ログ種別フィルタ

// 定義と既定値
public LogTypeFilter LOG_TYPE_FILTER { get; init; } = (lt) => lt <= LogType.I;

// LogTypeFilterの定義
delegate bool LogTypeFilter(Logger.LogType logType);

世の中の多くのロガーではログ種別をログレベルと定義づけし、レベルの上下判定でフィルタリングします。
しかし、現実的にはログレベルという考え方にそぐわないこともあり、種別ごとにOn/Offを判断したいこともあります。
CFLog は両方の要望を実現するため、種別定義をビットフィールドとしてもレベルとしても判定できるものとし、 判定そのものを行うフィルタ処理をユーザがラムダ式で指定できるようにしています。
ここで示している既定値は DEBUG シンボルが定義されていない場合のもので、致命的エラー(F)~情報(I)を出力するものです。
(DEBUG シンボルが定義されている場合の既定値は(lt) => lt <= LogType.Tとしています)

ログ種別は次のように定義されています。

public enum LogType : uint
{
    T = 0b00100000,		// トレース用(実装時用)
    D = 0b00010000,		// デバッグ用(試験時用)
    I = 0b00001000,		// 情報
    W = 0b00000100,		// 警告
    E = 0b00000010,		// 復帰可能エラー
    F = 0b00000001,		// 致命的エラー

    // リリースモードフィルタ
    FILTER_RELEASE = (F | E | W | I),

    // デバッグモードフィルタ
    FILTER_DEBUG = (F | E | W | I | D),
}

最後の2つは種別ではなく、フィルタに使うための定義です。例えば次のように使用します。

LOG_TYPE_FILTER = (lt) => (LogType.FILTER_DEBUG & lt) != 0;

■デバッグ出力フラグ

// 定義と既定値
public bool DEUBG_WRITE { get; init; } = true;

ログファイルだけでなく、Visual Studio のデバッグ用にも内容を出力する場合は true にします。
内部ではSystem.Diagnostics.Write()を使用していて、ビルド時に DEBUG シンボルが定義されている場合にのみ機能します。 リリース時にあえて false に設定する必要はありません。

■開始終了ログメッセージ

// 定義と既定値
public bool WRITE_START_AND_STOP_MESSAGE { get; init; } = true;

CFLog 自身がログファイルに出力するメッセージを制御します。
CFLog 自身が出力するログメッセージには以下の種類があり、このうち5を除く1~4のOn/Offを制御します。

  1. ログ開始
  2. ログ終了
  3. ログ引継ぎ開始
  4. ログ引継ぎ終了
  5. 保管期限切れログファイル削除に関するメッセージ

■言語

// 定義と既定値
public System.Globalization.CultureInfo? CULTURE_INFO { get; init; } = CultureInfo.CurrentCulture;

既定値の場合実行環境が反映されます。
主としてこの設定が影響するのは CFLog 自身が出力するログメッセージと、CFLog 独自の例外メッセージです。
これらのメッセージはテキストリソースの内容が出力されます。CFLog では既定の言語として英語、それに加えて日本語のテキストリソースを定義しています。
Windows の環境によらず、日本語に固定したい場合はnew("ja-JP")、英語に固定する場合はそれ以外(new("en-US") 等)を設定してください。
テキストリソース以外に、行ヘッダの日時やプロセスID,スレッドIDのToString()メソッドの引数にも使用していますが、 後述のDATETIME_FORMAT日時データフォーマット])、 PROCESS_ID_FORMAT([プロセスIDフォーマット])、 THREAD_ID_FORMAT([スレッドIDフォーマット])をカルチャー依存のフォーマットにしない限りは影響はないと考えられます。

■ログに表示する日時やファイル管理に使用する時間地域情報

// 定義と既定値
public System.TimeZoneInfo TIME_ZONE_INFO { get; init; } = System.TimeZoneInfo.Local;

ログの行ヘッダに含まれる日時情報や、ログファイル名に含まれる日付情報、 また、保管期限切れファイルの削除処理など CFLog 内部で使用する時計のタイムゾーンです。
タイムゾーンID(文字列)は開始メッセージと共にログファイルへ書出されます。

開始メッセージとともに出力されるタイムゾーンIDの例

2024-04-30 16:25:15.422 <25956:01> === ログを開始します ===  [Tokyo Standard Time]

■ログテキストのエンコード

// 定義と既定値
public System.Text.Encoding LOG_TEXT_ENCODING { get; init; } = System.Text.Encoding.UTF8;

出力されるログテキストファイルの文字コードの指定です。
utf-16(Encoding.Unicode)やShift-JIS(CodePagesEncodingProvider.Instance.GetEncoding(932))等の指定が可能です。
同一ログファイルに対して途中で変更した場合、テキストファイルとして不正なものになります。

■行ヘッダの日時データフォーマット

// 定義と既定値
public string DATETIME_FORMAT { get; init; } = "yyyy-MM-dd HH:mm:ss.fff";

System.DateTime.ToString()のパラメータです。固定長となるフォーマットをお勧めします。
設定は後述の INDENT_SPACE([インデントスペース])の調整も併せて行ってください。

■行ヘッダのプロセスIDフォーマット

// 定義と既定値
public string PROCESS_ID_FORMAT { get; init; } = "00000";

Int32.ToString()のパラメータです。プロセスIDは通常5桁で十分です。
設定は後述の INDENT_SPACE([インデントスペース])の調整も併せて行ってください。

■行ヘッダのスレッドIDフォーマット

// 定義と既定値
public string THREAD_ID_FORMAT { get; init; } = "00";

Int32.ToString()のパラメータです。このスレッドIDはシステムで管理されるスレッドIDではなく、.NET独自の内部管理番号です。
スレッド数が2桁で足りないアプリケーションの場合は桁ずれを起こさないように "000" 等と設定してください。
設定は後述の INDENT_SPACE([インデントスペース])の調整も併せて行ってください。

■インデントスペース

// 定義と既定値
public string INDENT_SPACE { get; init; } = string.Empty.PadRight(35);

ログに2行目以降の出力がある場合、ここで指定されたスペースをインデントに使用します。


Logger クラス

CFLog の機能を使うには Logger クラスを使用します。シングルトンでインスタンスも内部で管理されます。

  • プロパティ

    • Logger クラスインスタンス(LOG)
  • メソッド

    • 初期化(Logger クラスインスタンス生成)
    • 終了(Dispose())
    • ログ出力(Write()×2)

次にすべてのプロパティとメソッド(ただしWrite()のオーバーロードを除く)を使用する同内容の2つの最小コードを示します。

最小コード1

using(CFLog.Logger.CreateLogger())
{
    CFLog.Logger.LOG.Write(I,"最小コード");
}

最小コード2

using static CFLog.Logger;
using static CFLog.Logger.LogType;

var logger = CreateLogger();
LOG.Write(I,"最小コード");
logger.Dispose();

■ Logger.LOG プロパティ

《定義》

Logger クラスインスタンス
シングルトンの Logger クラスインスタンスを保持します。ユーザはこのプロパティを通じてメソッドを呼び出します。

static public Logger LOG { get; }
《例外》
LoggerInitException
    未初期化でアクセスしました。
《解説》

プロパティではありますが、未初期化でアクセスした場合は例外を発生させます。これは null チェックをユーザ側の責務にしないためです。
このプロパティの名称は一般的な規則では「Log」とすべきところですが、あえて全部大文字にしています。 これは、本来のプログラムの流れとは異なる呼び出しであることや、実質的にグローバルなメソッドを呼び出していることを判別しやすくするための工夫です。

■ Logger.CreateLogger メソッド

《定義》

Logger インスタンス生成と初期化

static public Logger CreateLogger(in LoggerDef? loggerDef = null)
《パラメータ》
loggerDef
    null の場合は既定値の LoggerDef が使用されます。
    通常はユーザにより定義された LoggerDef インスタンスを指定します。
《戻り値》

生成された Logger クラスのインスタンスを返しますが、これはDispose()の呼び出しだけを目的とするものです。

《例外》
LoggerInitException
    初期化失敗を意味します。
LoggerWriteException
    初期化時に Logger 自身がログファイル書出しを行う際にエラーが発生したことを意味します。
《解説》

シングルトンの Logger クラスをインスタンス化し、static領域に保持します。
Logger.Dispose()を呼び出す前に多重に呼び出すと2回目以降は LoggerInitException 例外が発生します。

Logger クラスインスタンス化時の主な処理
  • ログ出力フォルダの作成
  • ログファイルオープン
  • 保管期限切れログファイルの削除

それぞれの処理については後述の[6.内部処理]をご覧ください。

■ Logger.Write メソッド

《定義》

ログ出力

オーバーロード
プロトタイプ 機能
Write(LogType,string,string?) 通常メッセージログ出力
Write(LogType,string,Exception) 例外情報ログ出力
public void Write(Logger.LogType logType,string text, string? data = null)
public void Write(Logger.LogType logType,string text, Exception exData,bool enableOtherExceptionData = true)
《パラメータ》
logType
    ログの種別で、ログ出力のフィルタリングの判定、およびログの行ヘッダへの記載に使用されます。
text
    ログに出力するテキストデータです。改行コードは含めないでください。
data
    ログ出力するオプションの追加テキストデータです。
    改行コードを含めることが可能で、インデント処理が行われます。
exData
    ログへ書出す例外情報です。
    例外情報が入れ子になっていても展開してすべての例外メッセージ(Exception.Message)をログに出力します。
enableOtherExceptionData
    例外情報をログに書出す際、Exception.ToString() を出力するかどうかを決めます。
    Exception.ToString() による出力はスタックトレース等の詳細情報が含まれます。

例外情報をログに出力する例

    try
    {
        try
        {
            throw new Exception("元例外メッセージ");
        } catch(Exception ex)
        {
            throw new Exception("例外メッセージ2", ex);
        }
    } catch(Exception ex)
    {
        // 入れ子になった例外情報
        LOG.Write(F, "ToString()なし", ex, false);
        LOG.Write(F, "ToString()あり", ex);
    }

ログ出力例

2024-05-05 22:59:03.726 <17928:01> F [Sample01Form.cs(L.27)]: ToString()なし
                                   [例外メッセージ2]<-[元例外メッセージ]
2024-05-05 22:59:03.753 <17928:01> F [Sample01Form.cs(L.28)]: ToString()あり
                                   [例外メッセージ2]<-[元例外メッセージ]
                                   System.Exception: 例外メッセージ2
                                    ---> System.Exception: 元例外メッセージ
                                      at CFLogSampleForDoc01.Sample01Form.Write01Button_Click(Object sender, EventArgs e) in C:\Users\MyName\source\repos\CFLogSampleForDoc\CFLogSampleForDoc01\Sample01Form.cs:line 20
                                      --- End of inner exception stack trace ---
                                      at CFLogSampleForDoc01.Sample01Form.Write01Button_Click(Object sender, EventArgs e) in C:\Users\MyName\source\repos\CFLogSampleForDoc\CFLogSampleForDoc01\Sample01Form.cs:line 23
《例外》
LoggerWriteException
    処理中、何らかの例外が発生した際に LoggerWriteException でラップして置き換えます。
《解説》

text データは行ヘッダ等の後に書出されます。
data の指定がある場合は、data 内の改行コードを認識し、それぞれの行にインデントスペースを付加した上で2行目以降に出力します。これは exData の場合も同様です。
ファイルへの出力は最後に Flush 処理されます。

通常は以上の処理だけですが、現在の日付が CFLog 初期化時から繰越されている場合は[日付繰越によるログファイル切替]が行われます。

処理の詳細については後述の[6.内部処理]をご覧ください。

■ Logger.Dispose メソッド

《定義》

インスタンス廃棄

《例外》
LoggerWriteException
    ログファイルクローズ直前にログ終了メッセージを書き出しますが、その際に発生する可能性があります。
《解説》

オープンされているログファイルストリームをクローズし、Logger クラスインスタンスを破棄します。
本処理実行後は再度CreateLogger()で初期化することが可能になります。

◆6.内部処理

■ Logger クラスコンストラクタ

Logger クラスのコンストラクタは private であり、CreateLogger()からのみ起動されます。
主に以下の処理を行います。

  • ログファイルオープン(ログ出力フォルダ作成を含む)
  • 保管期限切れログファイルの削除(後述します)

ログファイルオープン処理

同一アプリケーションの複数起動許可について

LoggerDef.ALLOW_MULTIPLE_PROCESSESの設定が影響を与える処理がログ出力フォルダの作成とログファイルオープンです。それぞれの処理の説明の前にこの点について説明します。
ALLOW_MULTIPLE_PROCESSES = falseの場合はLOG_DIR_PATHで指定されたフォルダに直接ログファイルを作成します。
ALLOW_MULTIPLE_PROCESSES = trueの場合はLOG_DIR_PATHのフォルダの下にサブフォルダを作成して使用します。

LOG_DIR = @"C:\ProgramData\YoureApp\Log",
ALLOW_MULTIPLE_PROCESSES = true,
/*
  この場合、実際に使用されるフォルダは番号サブフォルダが付加されたものです
  C:\ProgramData\youreApp\Log\1
  C:\ProgramData\youreApp\Log\2
*/

同一ログ出力フォルダ、かつ同一ログファイル名を使用するアプリケーション(つまり、通常は同一実行モジュール)が同時に起動される数に応じてサブフォルダが作られます。

単一プロセス起動を前提とする処理(ALLOW_MULTIPLE_PROCESSES = false

LOG_DIR_PATHで指定されたログ出力フォルダが存在しなければ作成します。
フォルダ作成とともにDIR_RIGHTS_TARGETDIR_RIGHTSで指定された権限をフォルダに追加します。 この権限追加処理は既にログ出力フォルダが存在する場合やDIR_RIGHTS_TARGET = nullの場合は行いません。

ログファイルは追加書込みでオープンします。まだなければ作成します。ほかのプロセスに対しては書込みを許可しないモードでオープンします。
ファイルの権限については特に何も設定しません。フォルダの設定が継承されることを前提としています。

複数プロセス起動を前提とする処理(ALLOW_MULTIPLE_PROCESSES = true

複数プロセスが前提の場合、プロセスごとにサブフォルダを作成します。フォルダの作成時に権限を設定するのは[単一プロセス起動を前提とする処理]の場合と同じです。

既に他のプロセスでログファイルが使用中でオープンに失敗した場合、サブフォルダ名に相当するプロセス番号をインクリメントして再試行します。 ファイルがオープンできるか、設定されたプロセス数の上限に達するまで繰り返されます。
必ずプロセス番号1からインクリメントしながらファイルオープンを試行するので、処理パフォーマンスはよくありません。 もし、同時に複数のプロセスを起動して CFLog を一斉に初期化し始めると、この処理の部分はミューテックスによりプロセス間排他制御を行っているため、最後のプロセスがログファイルをオープンできるまでより多くの時間がかかります。
CFLog でのマルチプロセス対応は飽くまで簡易的なものであると考えてください。もし、多数のプロセスを起動するようなアプリケーションであれば、アプリケーション側でプロセスごとにログ出力フォルダ名やログファイル名を変えるといった対応をしてください。

ちなみに、上記のような処理方式のため、途中終了したプロセスがあった場合、使用していたフォルダが再利用されます。
例えばプロセスが3つ立ち上がっていてサブフォルダ"1"、"2"、"3"を使用中だとします。 次に、"2"のプロセスが終了し、その後新たにプロセスを起動した場合、"4"を使用せず、空いた"2"を使います。

■ 保管期限切れログファイル削除

CFLog で唯一非同期処理が行われる部分です。
Logger クラスコンストラクタと、[日付繰越によるログファイル切替]処理時に行われます。

日付の繰越はWrite()メソッドが呼ばれたタイミングで感知します。 24時間以上Write()が呼ばれない場合は作られたログファイルが1日1ファイル未満になる可能性があります。 また、次のWrite()が呼ばれるまで、保管期限を過ぎたファイルが削除されずに残っているようにも見えます。
保管期限切れのログファイルが削除されないケースはほかにもあります。それは CFLog を使用するアプリケーションが24時間以上起動されないケースです。 これは当然のことではありますが、注意していただきたいのは同一アプリケーションの複数起動を許可(ALLOW_MULTIPLE_PROCESSES = true)している場合です。
例えば、初日だけ同時にプロセスを3つ起動し、その後は2つだけ起動するような場合、3つ目のプロセスが使用するサブフォルダ"3"の中のログファイルはいつまでも残ってしまいます。

保管期限切れログファイル削除機能の目的はディスク容量の節約が本来の目的です。仕様として、直接ログファイルの容量を指定する方法も考えられますが、 CFLog では保管期限日数を指定する方式としています。今日のPCの環境の多くは昔に比べてディスク容量に余裕があります。ただ、そうはいっても当然容量は無限ではありません。 そこで、保管期間を指定して期限が切れたファイルを削除していく方法を採りました。
この方法のメリットはログ出力の量ににかかわらず保管期限が保証され、かつディスク容量も無限には消費しないという点にあります。
デメリットはログファイルのディスク消費量が明確ではない点でしょうか。

ログファイルの保管期限切れ判定にはファイル名に含まれる日時文字列を使用します。ファイルシステムのタイムスタンプは使用しません。
フォルダに含まれるファイル名のリストを取得する際、ファイルシステムのワイルドカードによるリスト機能に依存しています。 得られたファイル名リストからそれぞれ保管期限最古のファイル名文字列と比較して削除対象の判断をしています。

// 削除ファイル判定部分
if(string.Compare(delFileName, oldistDateFilename, StringComparison.Ordinal) < 0)

このような処理のためにログファイル名の設定は日時文字列の前後(プレフィックスとサフィックス)を指定するようにしています。

以上の処理は比較的時間を要する処理のためTask.Run()により非同期に実行しています。
保管期限切れログファイル削除処理は初期化時と[日付繰越によるログファイル切替]時に起動されるので、初期化時に起動された Task が終了する前に次の Task が起動されてしまう可能性があります。 それを避けるため、前回のタスクが終了していない場合は[日付繰越によるログファイル切替]をキャンセルし、次に呼ばれるWrite()に機会を譲ります。

■ プロセス間排他制御

複数プロセス起動を前提とした設定(ALLOW_MULTIPLE_PROCESSES = true)の場合だけミューテックスによるプロセス間排他制御を行います。
既出の[複数プロセス起動を前提とする処理]でのログファイルオープン処理は、複数のプロセスが同時に行うと矛盾が生じて期待通りの動作ができないため、排他制御を行っています。

排他制御はミューテックス名が重要です。CFLog では他のすべてのアプリケーションと競合しないように GUID を含めています。 そして、排他制御の対象を絞るため、ログ出力フォルダ(フルパスに展開したもの)とファイルプレフィックスとサフィックスを繋いだデータを使用します。
ただし、ミューテックス名に使用できる文字列にはファイルシステムと同様の制限があると考えらるので、SHA-512によりハッシュ化したデータを使用しています。

■ スレッド間排他制御

Write()メソッドをどのスレッドからも使えるようにするため、lock ステートメントによるスレッド間排他制御を行っています。
lock の対象とするオブジェクトは、本質的にはログファイルへのアクセスを排他制御するのでストリームオブジェクト(ハンドル)を対象とすることが理にかなっています。しかし、残念ながら理想的にはいきません。 なぜなら、日付繰越によるログファイル切替が行われると前日のログファイルトリームはクローズしてDispose()され、ロック対象が消えてしまうことになるからです。
そのため、lock 対象専用の object を Logger クラス内で生成して使用しています。

ほかにも、RepeatableTask クラス内で同様の lock を行っています。

■ 日付繰越によるログファイル切替

Write()の際、ログファイルオープン時から日付に変更があった場合はそれまでのファイルをクローズし、新たに現在のログファイルをオープンします。
そのため、24時間以上の間隔でWrite()が呼ばれるようなアプリケーションではログファイルが連続しない可能性があります。 また、ログファイル切替が行われるまでログファイルはオープンされたままになりますが、それが昨日以前にオープンされたものである可能性もあります。
こういったことが問題になる場合は、日付が変わるタイミングでWrite()を呼ぶようにしてください。種別のフィルタリングで実際には書出されない場合でもログファイル切替処理は行われます。
ただし、CFLog 初期化直後に日付が変わった場合は[保管期限切れログファイル削除]処理が終了していない可能性があり、その場合はログファイル切替処理は行われません。

もう1つの制限事項としては、WRITE_START_AND_STOP_MESSAGE = trueの場合ログファイルクローズ直前にログ終了メッセージを書き込みますが、 このメッセージ自体のタイムスタンプが日付を超えたものになることです。