В процесі розробки програмного забезпечення постійно виникають питання, пов'язані з виявленням помилок в коді, відслідковуванні роботи служб чи консольних програм, особливо якщо ці помилки важко відтворити чи вони мають нерегулярний характер. Відразу приходить на думку ведення логів програмою. Можна спробувати черговий раз вигадати черговий велосипед.
Модуль 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;
Немає коментарів :
Дописати коментар