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で公開しています。
エントリ中で紹介できなかった、実際にログを出力する処理も公開しています。
本エントリと合せて、参考にしていただけたら幸いです。
ではっ!
ディスカッション
コメント一覧
まだ、コメントがありません