В процесі розробки програмного забезпечення постійно виникають питання, пов'язані з виявленням помилок в коді, відслідковуванні роботи служб чи консольних програм, особливо якщо ці помилки важко відтворити чи вони мають нерегулярний характер. Відразу приходить на думку ведення логів програмою. Можна спробувати черговий раз вигадати черговий велосипед.
Модуль vmsVerInfo.pas описано в статті "Клас, який повертає інформацію про файл", модуль vmsLocalInformation.pas - в статті "Бібліотека, що містить процедури та функції, які повертають локальну системну інформацію", модулі vmsCharConsts.pas та vmsHtmlConsts - в статті "Дві допоміжні бібліотеки з константами"
Розглянемо модуль детальніше. Він складається з двох класів: TFileWriter та TDebugWriter. Клас TFileWriter займається безпосереднім записом інформації у файл, без аналізу вмісту.
TFileWriter = class
private
FCriticalSection : TRTLCriticalSection;
FFileStream : TFileStream;
FFileName : string;
FLogPath : string;
FStarted : Boolean;
procedure SetLogPath(const Value: string);
function GetSize: Int64;
public
constructor Create; virtual;
destructor Destroy; override;
procedure Finish;
procedure Start; virtual;
procedure Write(AText: AnsiString);
property FileName : string read FFileName write FFileName;
property LogPath : string read FLogPath write SetLogPath;
property Size : Int64 read GetSize;
property Started : Boolean read FStarted;
end;
implementation
...
constructor TFileWriter.Create;
begin
inherited;
FStarted := False;
end;
destructor TFileWriter.Destroy;
begin
if Assigned(FFileStream) then
FreeAndNil(FFileStream);
inherited;
end;
procedure TFileWriter.Finish;
begin
FStarted := False;
DeleteCriticalSection(FCriticalSection);
if Assigned(FFileStream) then
FreeAndNil(FFileStream);
inherited;
end;
function TFileWriter.GetSize: Int64;
begin
if Assigned(FFileStream) then
Result := FFileStream.Size
else
Result := 0;
end;
procedure TFileWriter.SetLogPath(const Value: string);
begin
if (Value <> '') then
FLogPath := IncludeTrailingPathDelimiter(Value)
else
FLogPath := ExtractFilePath(Application.ExeName);
if not DirectoryExists(Value) then
if not CreateDir(Value) then
FLogPath := '';
end;
procedure TFileWriter.Start;
var
sFileName : string;
begin
InitializeCriticalSection(FCriticalSection);
FStarted := True;
sFileName := Concat(LogPath, FileName);
if not Assigned(FFileStream) then
begin
if FileExists(sFileName) then
FFileStream := TFileStream.Create(sFileName, fmOpenWrite or fmShareDenyWrite)
else
FFileStream := TFileStream.Create(sFileName, fmCreate or fmShareDenyWrite);
end;
end;
procedure TFileWriter.Write(AText: AnsiString);
begin
if FStarted then
begin
EnterCriticalSection(FCriticalSection);
try
FFileStream.WriteBuffer(PAnsiChar(AText)^, Length(AText));
finally
LeaveCriticalSection(FCriticalSection);
end;
end;
end;
...
Звичайно, можна було б скористатися одним зі стандартних класів, як то TWriter, але потрібно спланувати роботу модуля з різних потоків і з середини dll. Взагалі, цей клас почав писатися для отримання логів саме з динамічних бібліотек, тому що інколи важко прослідкувати послідовність проходження алгоритму. Особливо складно для нетипових важковловимих помилок, що невідомо коли і як беруться.
Ініціалізація екземпляра класу відбувається в розділі initialization, що дозволяє проводити логування об'єктів ще в момент конструктора Create. Є як прихильники, так і критики такого методу створення об'єктів. В даному випадку цей варіант виправданий.
Виходячи з цього, потрібно реалізувати механізм автоматичного увімкнення логування. Це можна зробити, як варіант, через реєстр або конфігураційні файли: .ini або .xml. В подальшому буде описано клас по роботі з xml, поки можна це реалізувати в ini-файлі.
Приклад конфігураційного ini-файлу:
[Debug] FormatOfLogFile = html MaxSizeOfLogFile = 1000 IsStartDebug = 1
- IsStartDebug = 1 - параметр, що вмикає або вимикає логування (0, 1)
- MaxSizeOfLogFile - максимальний розмір лог-файлу
- FormatOfLogFile - формат лог-файлу: html або text
Метод TDebugWriter.IsStartDebug перевіряє, чи встановлено у конфігураційному файлі параметр IsStartDebug. Метод TDebugWriter.RestoreStartParams вичитує формат лог-файлу, а також максимальний розмір лог-файлу:
function TDebugWriter.IsStartDebug: Boolean;
var
loFile : TIniFile;
begin
Result := False;
loFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
Result := loFile.ReadBool(C_CFG_SECTION_DEBUG, C_CFG_KEY_IS_START, False);
finally
FreeAndNil(loFile);
end;
end;
procedure TDebugWriter.RestoreStartParams;
var
loFile : TIniFile;
sResultFormat : string;
begin
loFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
sResultFormat := loFile.ReadString(C_CFG_SECTION_DEBUG, C_CFG_KEY_FORMAT, 'htm').ToLower;
if (sResultFormat.Contains('htm')) or (sResultFormat.Contains('html')) then
FResultFormat := rfHtm
else if (sResultFormat.Contains('txt')) or (sResultFormat.Contains('text')) then
FResultFormat := rfText;
FMaxSize := loFile.ReadInteger(C_CFG_SECTION_DEBUG, C_CFG_KEY_MAX_SIZE, 0) * 1024;
finally
FreeAndNil(loFile);
end;
end;
Метод Start безпосередньо вмикає логування. В ньому відбувається створення об'єкта TFileWriter, а також відбувається формування заголовків таблиці, якщо формат - html.
procedure TDebugWriter.Start;
var
sText : string;
begin
if not Assigned(FLogFile) then
begin
FLogFile := TFileWriter.Create;
FLogFile.FileName := GetDebugFileName;
end;
if (not FLogFile.Started) then
begin
FLogFile.Start;
case FResultFormat of
rfText :
sText := 'Log session already started' + C_VMS_LINE_BREAK;
rfHtm :
if not FIsExistHtmlOpen then
begin
FIsExistHtmlOpen := True;
sText := Concat(C_VMS_HTML_OPEN, //<!DOCTYPE HTML><HTML>
C_VMS_HTML_HEAD_OPEN, //<HEAD><meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
C_VMS_HTML_STYLE_OPEN, //<STYLE>
C_VMS_HTML_STYLE_TABLE,
C_VMS_HTML_STYLE_IMG_ENTER,
C_VMS_HTML_STYLE_IMG_EXIT,
C_VMS_HTML_STYLE_IMG_ERR,
C_VMS_HTML_STYLE_CLOSE, //</STYLE>
C_VMS_HTML_HEAD_CLOSE, //</HEAD>
TvmsHtmlLib.GetTableTag(VarArrayOf([C_VMS_HTML_NBSP,
'Line №',
'Time',
'Unit name',
'Class name',
'Method name',
'Description'])));
end
else
Write(TvmsHtmlLib.GetColorTag(TvmsHtmlLib.GetBoldText('Log session already started'), clNavy));
end;
if (sText <> '') then
WriteOnlyText(sText);
end;
end;
По аналогії, метод Finish вимикає процес логування, закриваючи відсутні теги. Прапор FIsExistHtmlClose визначає, чи лог-файл буде закритий повністю, чи лише призупинений.
procedure TDebugWriter.Finish;
var
sText : string;
begin
if Assigned(FLogFile) and FLogFile.Started then
begin
case FResultFormat of
rfText :
sText := 'Log session finished';
rfHtm :
begin
Write(TvmsHtmlLib.GetColorTag(TvmsHtmlLib.GetBoldText('Log session finished'), clNavy));
if FIsExistHtmlClose then
sText := Concat(sText, C_VMS_HTML_TABLE_CLOSE, C_VMS_HTML_CLOSE);
end;
end;
if (sText <> '') then
WriteOnlyText(sText);
FLogFile.Finish;
end;
end;
procedure TDebugWriter.SetActive(AValue: Boolean);
begin
if AValue then
Start
else
Finish;
end;
Потрібно також визначитися з назвою та шляхом збереження лог-файлів. В даному випадку, файли зберігаються в каталог програми. Але ніщо не забороняє, навіть потрібно, створити окремий каталог Log, де будуть складатися файли. Параметр конфігураційого файла MaxSizeOfLogFile вказує на те, що лог-файл не може бути більшого розміру. Тобто це означає, що потрібно створювати новий лог-файл з іншою назвою. Поле FCountFiles - лічильник кількість таких файлів. Якщо обмежень у розмірі немає, все писатиметься в один файл.
function TDebugWriter.GetDebugFileName: string;
var
sDebugFileExt : string;
begin
case FResultFormat of
rfText : sDebugFileExt := '.log';
rfHtm : sDebugFileExt := '.htm';
end;
if (FCountFiles > 0) then
Result := LowerCase(Concat(ChangeFileExt(ExtractFileName(Application.ExeName), ''),
FormatFloat('_00000', GetCurrentProcessId),
FormatDateTime('_zzz', Now), '.',
IntToStr(FCountFiles),
sDebugFileExt))
else
Result := LowerCase(Concat(ChangeFileExt(ExtractFileName(Application.ExeName), ''),
FormatFloat('_00000', GetCurrentProcessId),
FormatDateTime('_zzz', Now),
sDebugFileExt));
end;
----delphi
----delphi
----delphi
----delphi
Приклад лог-файлу у вигляді html:
| Line № | Time | Unit name | Class name | Method name | Description | |
|---|---|---|---|---|---|---|
| 000001 | 14.02.2014 15:43:14.533 | Module : C:\Temporary\ut_DemoLib\ut_DemoLib.exe Module version : 1.0.5158.45803 Module date : 14.02.2014 Module size : 3 358 208 bytes Local IP-address : 192.168.0.1 (SomeHost) Windows version : Windows Win7 6.01.7601 Service Pack 1 Windows user : User | ||||
| 000002 | 14.02.2014 15:43:17.190 | DemoLib_fu_main.pas | TfmDemoLibFuMain | |||
| 000003 | 14.02.2014 15:43:17.190 | TfmDemoLibFuMain | btnSaveErrorClick | |||
| 000004 | 14.02.2014 15:43:17.190 | btnSaveErrorClick | Some text 01 | |||
| 000005 | 14.02.2014 15:43:17.190 | DemoLib_fu_main.pas | btnSaveErrorClick | Some text 02 | ||
| 000006 | 14.02.2014 15:43:17.190 | TfmDemoLibFuMain | btnSaveErrorClick | Some text 03 | ||
| 000007 | 14.02.2014 15:43:17.190 | Some text 04 | ||||
| 000008 | 14.02.2014 15:43:17.190 | DemoLib_fu_main.pas | btnSaveErrorClick | Якась дуже страшна помилка 01 Division by zero | ||
| 000009 | 14.02.2014 15:43:17.190 | TfmDemoLibFuMain | btnSaveErrorClick | Якась дуже страшна помилка 02 ''ABC'' is not a valid integer value | ||
| 000010 | 14.02.2014 15:43:17.190 | TfmDemoLibFuMain | btnSaveErrorClick | |||
| 000011 | 14.02.2014 15:43:17.190 | DemoLib_fu_main.pas | TfmDemoLibFuMain | |||
| 000012 | 14.02.2014 15:43:18.543 | Log session finished |
Приклад лог-файлу у вигляді тексту:
Log session already started
000001 14.02.2014 15:40:27.152
*************************************************************************************
Module : C:\Temporary\ut_DemoLib\ut_DemoLib.exe
Module version : 1.0.5158.45803
Module date : 14.02.2014
Module size : 3 358 208 bytes
Local IP-address : 192.168.0.1 (SomeHost)
Windows version : Windows Win7 6.01.7601 Service Pack 1
Windows user : User
*************************************************************************************
000002 14.02.2014 15:40:29.262 ->{Unit Name: DemoLib_fu_main.pas} (TfmDemoLibFuMain)
000003 14.02.2014 15:40:29.262 ->(TfmDemoLibFuMain) [btnSaveErrorClick]
000004 14.02.2014 15:40:29.262 [btnSaveErrorClick] Some text 01
000005 14.02.2014 15:40:29.262 {Unit Name: DemoLib_fu_main.pas.pas} [btnSaveErrorClick] Some text 02
000006 14.02.2014 15:40:29.262 (TfmDemoLibFuMain) [btnSaveErrorClick] Some text 03
000007 14.02.2014 15:40:29.262 Some text 04
000008 14.02.2014 15:40:29.262 [Error! {Unit Name: DemoLib_fu_main.pas} [btnSaveErrorClick] ] Якась дуже страшна помилка 01 Division by zero
000009 14.02.2014 15:40:29.262 [Error! (TfmDemoLibFuMain) [btnSaveErrorClick] ] Якась дуже страшна помилка 02 ''ABC'' is not a valid integer value
000010 14.02.2014 15:40:29.262 <-(TfmDemoLibFuMain) [btnSaveErrorClick]
000011 14.02.2014 15:40:29.262 <-{Unit Name: DemoLib_fu_main.pas} (TfmDemoLibFuMain)
Log session finished
Приклад використання:
procedure TfmDemoLibFuMain.FormCreate(Sender: TObject);
begin
DebugFile.EnterObject(Self);
end;
procedure TfmDemoLibFuMain.btnSaveErrorClick(Sender: TObject);
var
i: byte;
begin
DebugFile.EnterMethod(Self, 'btnSaveErrorClick');
DebugFile.Write('btnSaveErrorClick', 'Some text 01');
DebugFile.Write('btnSaveErrorClick', Self.UnitName, 'Some text 02');
DebugFile.Write(Self, 'btnSaveErrorClick', 'Some text 03');
DebugFile.Write('Some text 04');
i := 0;
try
i := 1 div i;
except
on Err:Exception do
DebugFile.WriteError('btnSaveErrorClick', 'DemoLib_fu_main', 'Якась дуже страшна помилка 01' + C_VMS_LINE_BREAK + Err.Message);
end;
try
i := StrToInt('ABC');
except
on Era:Exception do
DebugFile.WriteError('btnSaveErrorClick', 'DemoLib_fu_main', 'Якась дуже страшна помилка 02' + C_VMS_LINE_BREAK + Era.Message);
end;
DebugFile.ExitMethod(Self, 'btnSaveErrorClick');
end;
procedure TfmDemoLibFuMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
DebugFile.ExitObject(Self);
end;
Немає коментарів :
Дописати коментар