C#でログ機能を実装してみた

どもです。

今回は、C#でログ機能(オリジナル)を実装してみたので、紹介します。

なお、開発環境は、以下の通りです。

項目 内容
OS Windows10 Pro(21H1)
CPU i7-8700
メモリ 16GB
IDE VisualStudioCommnuity 2019
version 16.11.11
.NET Framework 4.7.2
.NET Core 3.1
言語 C#

紹介するログ機能は、.NET Core/.NET Frameworkの両方で使用できるよう実装しました。

1. ログ機能の課題

アプリケーションを作成する際に、常に(個人的に)課題になるのが

ログ機能

です。

実行ログは、よくあるのはコマンドプロンプトに出力する、という方法です。
しかしGUIアプリでは、ファイルに出力したり、イベントとして登録したりします。
また、これらすべて(コマンドプロンプト/ファイル/イベント/etc…)に対して出力したい場合もあります。

このように、ログの出力先/出力方法は、アプリケーションの種類によって異なります。

この「ログの出力先/出力方法の違い」が、ログ機能の課題です。

2. 課題の解決

先述の課題解決のために考えた設計、および実装です。

2.1. 設計(クラス図)

ログ機能の課題を解決するために、以下のような設計(クラス図)を考えました。



クラス図中の右下に配置された2つのクラスが、実際にログを出力するクラスになります。
クラスと出力先の対応は、以下のようになります。

クラス名 出力
Console.Log 標準出力
File.Log ファイル

この設計では、「ログを出力したい」機能と「ログを出力する」機能を別クラスに分けています。
更に、このクラスの間に「Log」というクラスを実装しています。
これはシングルトンオブジェクトであり、全ての「ログを出力したいクラス」がこのオブジェクトを使用します。
これにより、ログの出力先の変更(追加や削除も含む)があった場合でも「ログを出力したい」クラスと「ログを出力するクラス」を変更することなく対応が可能です。

2.2. 実装

次に、実装を紹介します。
紹介するのは、前章のクラス図中の「Log」と「CommonBaseObject」クラスです。

2.2.1. Logクラス

まず、「Log」クラスです。
このクラスは「シングルトン」ですので、シングルトンオブジェクトを返すメソッドと、各ログ情報に対応したメソッドを持ちます。
今回は簡単のために、DEBUGレベルのメソッドのみを記載します。

class Log
{
	protected static Log _instance;
	protected Log() {}

	public Log GetCurrentLogger()
	{
		if (null == Log._instance) {
			Log,_instance = new Log();
		}
		return _instance;
	}

	public delegate void LogEventHandler(object sender, EventArgs e);

	public event LogEventHandler debugEventHandler;
	void DEBUG(string message)
	{
		var eventArg = new LogEventHandler(message);
		debugEventHandler?.Invoke(this, eventArg);
	}
}

このクラスの各ログ出力のメソッド(例では「DEBUG」メソッド)は、それぞれのレベルに対応したイベントハンドラ(debugEventHandler)を実行するのみです。
イベントハンドラに登録が無かった場合には、何も起こりません。

またシングルトンなので、コンストラクタは外部からアクセス不可にしており、staticなメソッドを介してインスタンスを取得します。

2.2.2. CommonBaseObjectクラス

次に、CommonBaseObjectクラスです。
このクラスは、Logクラスのオブジェクトを取得し、対応するメソッドを呼び出します。

class CommonBaseObject
{
	public void DEBUG(string message)
	{
		var logger = Log.GetCurrentLogger()
		logger.DEBUG(message);
	}
}

CommonBaseObjectクラスのログ出力メソッド(例では「DEBUG」メソッド)は、Logクラスのシングルトンオブジェクトを取得し、あとは対応するメソッドを呼び出すのみです。
これにより、このCommonBaseObjectクラスを継承したクラスは、対応するメソッドを実行するだけで、出力先を意識することなくログを出力ができます。
出力先が複数(コマンドプロンプトとファイル、とか)になったとしても、このコードは変更する必要がありません。

3. 使ってみる

次に、これまでに紹介したコードを使って、実際にログ出力をしてみます。

で。
サンプルプログラムは、以下!

public class Program
{
	static void SetupLogger(ref ILogEvent logEvent)
	{
		var logger = Log.GetInstance();
		logger.TraceLogEventHandler += logEvent.TRACE;
		logger.DebugLogEventHandler += logEvent.DEBUG;
		logger.InfoLogEventHandler += logEvent.INFO;
		logger.WarnLogEventHandler += logEvent.WARN;
		logger.ErrorLogEventHandler += logEvent.ERROR;
		logger.FatalLogEventHandler += logEvent.FATAL;
	}

	static void Main(string[] args)
	{
		ILogEvent consoleLog = new CSEngineer.Logger.Console.Log();
		ILogEvent fileLog = new CSEngineer.Logger.File.Log();

		SetupLogger(ref consoleLog);
		SetupLogger(ref fileLog);

		var sampleClass = new LoggerSampleClass();
		sampleClass.Sample();

		return;
	}
}

public class LoggerSampleClass : CommonBaseObject
{
	public LoggerSampleClass() { }

	public void Sample()
	{
		base.TRACE("Sample TRACE message");
		base.DEBUG("Sample DEBUG message");
		base.INFO("Sample INFO message");
		base.WARN("Sample WARN message");
		base.ERROR("Sample ERROR message");
		base.FATAL("Sample FATAL message");
	}
}

public class CommonBaseObject : ILogger
{
	public void DEBUG(string message)
	{
		var logger = Log.GetInstance();
		logger.DEBUG(message);
	}

	public void ERROR(string message)
	{
		var logger = Log.GetInstance();
		logger.ERROR(message);
	}

	public void FATAL(string message)
	{
		var logger = Log.GetInstance();
		logger.FATAL(message);
	}

	public void INFO(string message)
	{
		var logger = Log.GetInstance();
		logger.INFO(message);
	}

	public void TRACE(string message)
	{
		var logger = Log.GetInstance();
		logger.TRACE(message);
	}

	public void WARN(string message)
	{
		var logger = Log.GetInstance();
		logger.WARN(message);
	}
}

interface ILogger
{
	void TRACE(string message);
	void DEBUG(string message);
	void INFO(string message);
	void WARN(string message);
	void ERROR(string message);
	void FATAL(string message);
}

ちょっと長いですね。

で、このプログラムを実行すると、コンソールとファイルに、それぞれ以下のように出力されます。





上の画像がコンソール、下の画像がファイルに出力した結果です。

よくよく見ると、タイムスタンプにほんの少しだけずれがあります。
これは、ログレベルの文字列、およびタイムスタンプの生成を各Logクラスで実施しているためです。
しかし、それ以外は全て同じ文字列が出力されていることが分かります。

また、ログを出力する各クラス/メソッドでは、出力先を考慮していません。
Logオブジェクトのメソッドを呼び出しているだけです。
仮に出力先をコンソールだけにしたい、あるいはファイルだけにしたい、といった場合には、上述のMain関数内の「SetupLogger」関数の呼び出し(ひいては、イベントハンドラのセットアップ)を解除すれば対応可能です。

加えて、仮に特定のレベル以上、あるいは以下、または特定のレベルのみ出力するようにしたい場合は、「SetupLogger」でのイベントハンドラの設定を変更する飲みで対応可能です。

4. まとめ

ログ機能は、アプリケーションの種類(GUI/CUI)によって出力先を切り替える、変更する必要があります。
今回は、これを少しでも簡単にするために、出力先を気にせず、出力先の変更も簡単で、かつ色々出力先が選択/指定可能なログ機能を作成してみました。
作成したログ機能では、シングルトンオブジェクトを介してログを出力することで、ログを「出力したい機能」と「出力する機能」を分離することで、これらを実現しています。

言語によっては、超絶便利なログ機能が作成、オープンソースで公開されたりしています。
なので、今更自分でログ機能を実装する、なんてことはないと思います。
でも。
それでも。
今回のエントリの内容が、誰かの助けになれば幸いです。

ex. 公開しています

今回のエントリで作成したログ機能は、GitHubで公開しています。
エントリ中で紹介できなかった、実際にログを出力する処理も公開しています。
本エントリと合せて、参考にしていただけたら幸いです。

ではっ!