[c#]プログラムのバグの解決-ログ出力機能の作成-
概要
プログラムの作成において不具合の発生個所の特定をする必要がある。
これまではVisualStudioのステップ実行を用いていたが、他にもやりやすい方法が欲しいと考えた。
そこで、以下の図のようなログをテキストファイルに出力する機能を作成した。
目次
- ログファイル出力プログラムの用途
- ログファイル出力プログラムの仕様
- ログファイルの構成
- ログデータ取得用関数
- データ削除用関数
- 目的プログラムへのログの取得関数の配置
- 実行結果
- 補足
- ダウンロード
ログファイル出力プログラムの用途
今回作成するログ出力プログラムでは、プログラム運用中に生じた不具合をその後のテスト中に再現する事を目的としている。
不具合を再現できれば、後はViualStudioの機能を使って不具合の発生理由を探すことが出来る。
そこで今回は以下のような方針で作成した。
- ユーザーの入力情報を記録する
- 接続先の機器の情報を取得する
- 入力情報の処理結果の記録は取得するが、テストがやりやすいように補助的に用いる程度。
ログファイル出力プログラムの仕様
今回のログ出力プログラムでは、ログを取りたいプログラム(以下:目的プログラム)中のログを取りたい箇所に、ログ取得用の関数を設置することでログを取得している。
取得したログデータはデータの取得と同時にテキストファイルに出力するようにしている。(下図)
これにより、予期しないプログラムの終了があってもログデータが残るようになっている。
またログ出力用の関数が属しているクラスは、シングルトンデザインを採用し、目的プログラムのどこから呼んでも同一のオブジェクトが呼ばれるようになっている。
ログファイルの構成
ログファイルの先頭には、起動しているPCに搭載されているOSのバージョン、ユーザー名、目的プログラムのディレクトリを書き込む。
それ以降は、目的プログラム中に配置したログ取得関数から得られたログを書き込んでいる。
また目的プログラムの起動時に文字列”---NEW—”を書き込むことで起動1回分のデータの判別に利用している。
ログデータ取得用関数
目的プログラム中に配置し使用する。ユーザーや外部機器から得たデータや処理の結果を引数とし、これを元にログ出力用の文字列を作成する。
この時同時に、ログ出力用関数の呼び出し目的、ログ取得関数の呼び出しもとの関数の名前、クラス名を取得するようにしている。(呼び出しもとの取得方法は補足参照)
ここで関数の呼び出し目的は、「ユーザーの入力を記録する」、「外部機器の入力を記録する」、「プログラムの処理を記録する」、「テスト中に一時的に追加したもの」の4つとした。
これにより後述するログデータをエクセルに転写した際に、フィルタ機能を使いユーザーの入力情報のみを抽出する等といった事が出来るようになる。
以上の事をまとめて出力されるファイルに書き込まれる情報は以下のようにした。
加えてある程度データが蓄積した場合、古いデータから削除するようにした。
今回は10回以上目的プログラムを起動した場合、一番古い、1回分のデータを削除するようにした。
データ削除用関数
目的プログラムの起動時に、一時ファイルを作成する。
その後必要なデータだけを一時ファイルに書き込み、その後元のログファイルにコピーする事で古いデータの削除を実装した。
削除すべきデータの判定には、前述した文字列”---NEW—”を利用し、ファイルの冒頭から"---NEW---"が現れるまでは一時ファイルに転写せず読み飛ばすことで行った。
目的プログラムへのログの取得関数の配置
今回は、以前に作成したDFT計算プログラムを目的プログラムとしてログを取得した。
計算の実行ボタンや計算結果のリセットボタンに呼び出し目的を「ユーザーの入力を記録する」とするログ取得用関を配置した。
外部機器の入力を記録する」を目的とするログ取得用関数は、設定ファイルの取得部分に配置した。
最後に、「プログラムの処理を記録する」を目的とするものは、DFT計算に必要なパラメータの作成部分と、計算終了部分、これに加え、ログファイル取得用クラスの生成部分に配置した。
実行結果
実行した結果、冒頭の図のようなログファイルが出来た。
ログ出力関数の初回呼び出し時に、コンストラクタが呼び出され、以降は同じオブジェクトにアクセスしていると分かる。
ユーザーの入力部分と外部ファイルの読み取り結果もログが取れている。このため、不具合が生じた場合の再現も可能と考える。
補足
今回作成したログ取得用のプログラムでは、関数の呼び出し元の関数とクラスを取得している。
呼び出しもと関数の取得は、呼ばれた関数の引数に
[CallerMemberName] string callerMethodName = ""
を与える事で取得可能。
またクラス名は、呼ばれた関数内に、
var caller = new System.Diagnostics.StackFrame(1, false);
string callerClassName = caller.GetMethod().DeclaringType.FullName;
を記述することで取得可能。
これにより、ログファイルにどのクラスのどの関数からログ出力関数が呼ばれたか分かるようになる。
ダウンロード
ログ取得用プログラム(.cs)
ログ取得コード(ファイルパスなどは適宜変更が必要)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace LogFile
{
internal class Log
{
public enum E_KINF_OF_OPERATE//ログの分類
{
USER,//ユーザーの操作
OUTSIDE,//外部機器からの操作
PROCESS,//得られたデータ、操作に対する処理結果
DEBUG
}
const ushort MinimamStringLenght = 25;//ログの各項目の最低文字数
const int LEFTNO = 10;//直近10回の起動についてのログを残す
const string startRow = " ---NEW---";
const string path = ".\\DFTlog.log";
const string tempPath = ".\\temp.log";//一時ファイル
int startUpNum = 0;//起動回数
static Log log;
StreamWriter sw;
private Log()
{
GetStartNo();
//--前回までのデータの転写---
SelectStoreData();
sw = new StreamWriter(path, true, Encoding.UTF8);
if (startUpNum != 1)
{//初回起動でない場合
sw.WriteLine("\n\n\n");
sw.WriteLine(startRow);
}
//----今回の記入分---
sw.WriteLine(System.Environment.CurrentDirectory);
sw.WriteLine(System.Environment.UserName);
sw.WriteLine(System.Environment.OSVersion);
writeLosStr("Logクラス生成",E_KINF_OF_OPERATE.PROCESS);
}
public static Log LogFile()
{
var caller = new System.Diagnostics.StackFrame(1, false);
string callerClassName = caller.GetMethod().DeclaringType.FullName;
if (log == null)
{
log = new Log();
}
return log;
}
public void writeLosStr(string buf, E_KINF_OF_OPERATE e_KINF_OF_OPERATE,[CallerMemberName] string callerMethodName = "")
{
DateTime dateTime = DateTime.Now;
string headBlank = "";
var caller = new System.Diagnostics.StackFrame(1, false);
string callerClassName = caller.GetMethod().DeclaringType.FullName;
string kind=e_KINF_OF_OPERATE.ToString();
sw.WriteLine(dateTime.ToString("F") +" "+ callerClassName +" "+ callerMethodName +" "+ buf+" "+kind);
sw.Flush();
}
///
/// 起動回数をカウントする
///
private void GetStartNo()
{
StreamReader sr;
try
{
sr= new StreamReader(path);
}
catch (FileNotFoundException) {
FileStream stream=File.Create(path);
stream.Close();
sr=new StreamReader(path);
}
if (!sr.EndOfStream)//最初の1行目を読む
{
string line = sr.ReadLine();
if (int.TryParse(line, out startUpNum))
{//1行目が数値に変換できるとき
startUpNum++;
}
}
else//ファイルが空白⇒初回起動時
{
startUpNum = 1;
}
sr.Close();
}
///
/// ログファイルのデータのうち保持しておくデータの選定
/// 一定回数(10回)起動したら古いデータから消していく
///
/// 一時ファイルを作成し、そこに転写する。その後、ログファイルにコピーする
///
private void SelectStoreData()
{
bool abandonData = false;//読み飛ばすかどうか
if (startUpNum >= LEFTNO)//古いデータを消すフラグ
{
abandonData = true;
}
StreamReader sr = new StreamReader(path);
StreamWriter sw = new StreamWriter(tempPath);
sw.WriteLine(startUpNum.ToString());//起動回数を記入
string line = sr.ReadLine();
while (!sr.EndOfStream)//ファイルの末端まで読む
{
line = sr.ReadLine();
if (abandonData == true)//読み飛ばす
{
if (line == startRow)//---NEW---が見つかるまで読み飛ばす
{
abandonData = false;
}
}
else// 一時ファイルに転写
{
sw.WriteLine(line);
}
}
//全て読んだらクローズしコピーする
sw.Close();
sr.Close();
File.Copy(tempPath, path,true);
File.Delete(tempPath);//一時ファイルの削除
}
}
}
呼び出し側の関数記述例
private void DFT_START_Click(object sender, EventArgs e)
{
Log.LogFile().writeLosStr("DFT計算スタートボタン押下:分割数"+devide.Text,Log.E_KINF_OF_OPERATE.USER);
int n2 = int.Parse(devide.Text);
DFT_Cal cal = new DFT_Cal(n2,ref Message);//数値に出来かつ偶数である事は事前に確認済み
cal.SetFunction();
cal.CalcCoefficient();
for (int k = 0; k <= n2/2; k++)
{
if(k == 0||k==(n2/2))//係数Bの値がない場合(行列Bの方がサイズが小さいため)
{
resultTable.Rows.Add(k, Math.Round(cal.getA(k), 5));
}
else
{
resultTable.Rows.Add(k,Math.Round(cal.getA(k),5) ,Math.Round(cal.getB(k),5) );
}
}
Log.LogFile().writeLosStr("DFT計算終了", Log.E_KINF_OF_OPERATE.PROCESS);//関数内に配置することでログを出力する
}