👨‍💻

C#定石 - ログファイル出力

2024/12/27に公開

はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8
  • WPF - .NET Framework 4.8
  • WPF - .NET 8

RDP評価は、Windows Server 2025 評価版を利用しました。
Windows Server では「管理用リモートデスクトップ」として同時2セッションまでのリモートデスクトップ接続が許可されています。
上記は、RDS (Remote Desktop Services) を構成しなくても利用できます。

ログファイル出力

テスト工程などでの不具合確認として、トレースログをファイル出力しておくことは有効な手段となります。
該当機能は、有名どころとして NuGet Gallery | log4net で公開されている log4net がありますが、単純機能で設定いらずのクラスを自作して利用していました。
トレースログの実装は、グローバルインスタンスを利用することが手軽なので、下記のようなクラスとしていました。

https://github.com/chaichai0917/SampleCode

  • トレースログ ファイル出力
    • TraceLogging.cs (.NET Framework)
    • TraceLogging-Net.cs (.NET - Null許容参照型を明示)

上記のプロジェクトへの追加、アプリ設定、ログファイル出力、TraceLogging クラス説明を以降で記載します。

プロジェクトへの追加

.NET Fremework と .NET でサンプルソースが異なるので、それぞれについて以降で記載します。

.NET Framework

前述 TraceLogging.cs をダウンロードして、namespece Hoge となっている部分を、対象プロジェクトの namespece に変更します。

TraceLoggin.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Hoge    // ← ここを変更

変更した TraceLogging.cs を対象プロジェクトのフォルダに複写して、ソリューションエクスプローラ 追加 - 既存の項目 で追加します。

.NET

前述 TraceLogging-Net.cs をダウンロードして、TraceLogging.cs にリネームします。
次に namespece Hoge となっている部分を、対象プロジェクトの namespece に変更します。

TraceLoggin.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Hoge    // ← ここを変更

変更した TraceLogging.cs を対象プロジェクトのフォルダに複写して、ソリューションエクスプローラ 追加 - 既存の項目 で追加します。

アプリ設定

メインフォーム(メインウィンドウ)実行の前後で、トレースログの前処理/後処理を呼び出します。
Windows Forms / WPF、.NET Framework / .NET ともに同一記述ですが、記述箇所とその周辺に差異があるので、それぞれについて以降で記載します。

Windows Forms - .NET Framework

Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインフォーム
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());

    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}

Windows Forms - .NET

Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインフォーム
    ApplicationConfiguration.Initialize();
    Application.Run(new Form1());

    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}

WPF - .NET Framework / .NET

.NET Framework / .NET ともに同一ソースコードです。
WPF定石 - スタートアップルーチン 記載内容を実施済みのコードをベースとします。

App.xaml.cs
public partial class App : Application
{
  private void App_Startup(object sender, StartupEventArgs e)
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインウィンドウ
    var mainWindow = new MainWindow();
    mainWindow.Show();
  }
  private void App_Exit(object sender, ExitEventArgs e)
  {
    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}

ログファイル出力

グローバルインスタンスとしているので、同一 namespace のコードから下記記述でログファイル出力が可能です。

TraceLogging.LogObj?.WriteLine("Hoge Hoge");

ファイル出力は、バッファリングされるので、適時 DataFlush を呼び出すことで、バッファリングされている情報を強制的にファイル出力させることができます。

TraceLogging.LogObj?.WriteLine("Hoge Hoge");
TraceLogging.LogObj?.DataFlush();

TraceLogging クラス説明

.NET Framework 向けのソースをベースとして、いくつかのポイントについて説明します。

グローバルインスタンス化

グローバルインスタンスとするために public static class としています。

TraceLogging.cs
public static class TraceLogging
{
  // TODO
}

ログファイル作成フォルダ

RDP 運用時、GetTempPath は C:\Users\[username]\AppData\Local\Temp\2 のように序数サブフォルダを付与したパスが取得されます。
これは、同一アカウントで複数 RDP 接続した場合を考慮して、ワークフォルダをそれぞれ別フォルダとするためです。
このように序数サブフォルダが付与されたフォルダは、RDPセッション終了で削除されてしまいます。
このため、GetFolderPath を用いて C:\Users\[username]\AppData\Local\Temp を取得するようにしています。

TraceLogging.cs - OpenWorkFile
string tmpdir = JoinFilePath(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
    "Temp");
string workpath = JoinFilePath(tmpdir, filename);

GetFolderPath の引数は下記を参照してください。

https://learn.microsoft.com/ja-jp/dotnet/api/system.environment.specialfolder

JoinFilePath は、本クラス内のメソッドで、フォルダとして指定されたパス末尾にパス区切り文字があったら削除してから、ファイル名と結合をしています。

ログファイル名

前述ログファイル作成フォルダを RDP 運用時でも序数サブフォルダ無しとしたので、ログファイル名に端末名を付与して、同一アカウントでの RDP 複数接続時に別ファイルとなるようにしています。

TraceLogging.cs - TraceLogObject
string progname = Process.GetCurrentProcess().ProcessName;
string terminal = GetTerminalName();

// RDP運用を考慮して端末名を付与
string filename = string.Format("{0}-{1}.log", progname, terminal);

progname は、該当アプリ名称です。
GetTerminalName は、本クラス内のメソッドで、RDP運用時は RDP 接続元の端末名、RDP運用でない場合は自端末名を取得します。

ログファイル排他オープン

ログファイル出力が複数アプリで競合した場合、ログ出力がぐちゃぐちゃになってしまいます。
このため、ログファイルを排他オープンして、同一ファイルを同時利用できなくしています。
具体的には、まず FileStream で排他オープンした後、StreamWriter として生成しています。

TraceLogging.cs - OpenWorkFile
// 排他オープンのため、まずは FileStream 生成
stream = new FileStream(workpath, FileMode.Create, FileAccess.Write, FileShare.None);
StreamWriter writer = new StreamWriter(stream, encoding);

Exception 握り潰し

「ログ出力機能で、何らかの不足の事態により例外を発生させて、アプリ動作を止めてしまうのは望ましくない」という判断で、TraceLooging.cs 内では、下記記述で Exception を握り潰しています。

TraceLogging.cs - TraceLogObject
try
{
  // TODO
}
catch { /* NOP */ }

補足

リリースしたソフトでも不具合時の情報収集として、トレースログは有効です。
トレースログの有効/無効を設定ファイルなどによって制御する場合には、前処理 PreProcessing を下記のように書き換えてください。

TraceLogging.cs
public static void PreProcessing()
{
  // ログ出力有効/無効をアプリ設定から取得
  bool bEnable = IsLoggingEnable();   // IsLoggingEnable はそれぞれで実装してください。 
  if (bEnable)
  {
    // トレースログ - 初期化
    LogObj = new TraceLogObject();
  }
  else
  {
    LobObj = null;
  }
}

出典

本記事は、2024/12/25 Qiita 投稿記事の転載です。

C#定石 - ログファイル出力

Discussion