пʼятницю, 14 лютого 2014 р.

Клас для ведення логів роботи програми


В процесі розробки програмного забезпечення постійно виникають питання, пов'язані з виявленням помилок в коді, відслідковуванні роботи служб чи консольних програм, особливо якщо ці помилки важко відтворити чи вони мають нерегулярний характер. Відразу приходить на думку ведення логів програмою. Можна спробувати черговий раз вигадати черговий велосипед.

Модуль 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 &#8470;',
                                                            '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 №TimeUnit nameClass nameMethod nameDescription
00000114.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

00000214.02.2014 15:43:17.190DemoLib_fu_main.pasTfmDemoLibFuMain
00000314.02.2014 15:43:17.190TfmDemoLibFuMainbtnSaveErrorClick
00000414.02.2014 15:43:17.190btnSaveErrorClickSome text 01
00000514.02.2014 15:43:17.190DemoLib_fu_main.pasbtnSaveErrorClickSome text 02
00000614.02.2014 15:43:17.190TfmDemoLibFuMainbtnSaveErrorClickSome text 03
00000714.02.2014 15:43:17.190Some text 04
00000814.02.2014 15:43:17.190DemoLib_fu_main.pasbtnSaveErrorClickЯкась дуже страшна помилка 01
Division by zero
00000914.02.2014 15:43:17.190TfmDemoLibFuMainbtnSaveErrorClickЯкась дуже страшна помилка 02
''ABC'' is not a valid integer value
00001014.02.2014 15:43:17.190TfmDemoLibFuMainbtnSaveErrorClick
00001114.02.2014 15:43:17.190DemoLib_fu_main.pasTfmDemoLibFuMain
00001214.02.2014 15:43:18.543Log 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

Скачати файл vmsDebugWriter.pas
Приклад використання:

 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;

Немає коментарів :

Дописати коментар