Логирование в Delphi

Логирование (logging) — это ведение записей(как правило сохранение в файл) в хронологическом порядке.

Ведение логов зачастую очень удобная, облегчающая жизнь разработчику, штука, а для больших серьёзных проектов это вообще неотъемлемая вещь.

Логирование помогает как на этапе отладки, так и на этапе внедрения, например я прицеплял утилиту к проектам, которая отправляла файл лога куда мне удобно(сайт, мыло, сервер), таким образом избегал процесса вымогательства у пользователя лог-файла.

Я использую логирование почти во всех своих проектах от средне до велика.

В этой статье я рассмотрю свою реализацию модуля ведения логов, которым пользуюсь и время от времени грейжу его.

Данный модуль можно так же применять в многопоточных приложениях.

Былина

За свою практику я переписал не мало разновидностей процессов логирования и каждый раз меня чутка тревожило ощущение, что «что-то тут осталось не таким удобным как мне хотелось бы» и от этого постоянно что-то дописывал/переписывал.

Так я пришёл от модуля, содержащего глобальные переменные — параметры лога и глобальную функцию типа WriteToLog(aMsg: string): boolean, к модулю с классом — менеджером логирования и методами записи на WinApi. Время дало мне возможность, так сказать, выбрать самый удобный вариант.

Во время улучшения менеджера логирования я встречал одну интересную реализацию в плане процесса записи:

  • единое открытие файла на всё время работы приложения;
  • запись по таймеру накопившейся очереди сообщений.

При этом для работы с файлом использовался какой-нить стрим, зачастую TFileStream. Но при таких раскладах у нас есть довольно большой риск «просеять» в логе сообщение именно про ту ошибку, которая и привела к краху проги из-за которой не дописался лог…

Парадоксальная неприятность)

Последнее важное изменение я внёс когда обнаружил, что мой менеджер пишет строку поверх другой строки при могопоточном использовании, хоть и допускал возможность такого  использования.

Реализация

Я реализовал следующие элементы удобства:

  • единый класс логирования для всего приложения посредством шаблона Singleton;
  • типы сообщений (информация, предупреждение, ошибка);
  • уровень приоритета сообщений (максимальный, информативный, отладочный);
  • событийные типы как для объектных методов так и для обычных процедур;
  • настройка менеджера через свойства;
  • формирование заголовка из текущей даты и типа сообщения.

Код всего модуля uLogManager отображён в следующем листинге (Delphi 2007):

unit uLogManager;
  
{ 
  Автор: Monax (x-monax[at]mail[dot]ru)  
}  
  
interface  
  
uses  
  Windows, SysUtils, SyncObjs,  
  uSingletonTemplate;  
  
type  
  TLogMsgType = (lmtInfo, lmtWarning, lmtError);  
  
  TLogMsgLevel = (lmlMax, lmlInfo, lmlDebug);  
  
  TLogManagerEvent = procedure(const aLogMsgText: string; aLogMsgType: TLogMsgType);  
  TLogManagerEventObj = procedure(const aLogMsgText: string; aLogMsgType: TLogMsgType) of object;  
  
  TLogManager = class(TSingleton)   
  private  
    fcs   : TCriticalSection;  
    fFileName     : string; // файл лога  
    fMaxLogLevel  : TLogMsgLevel; // уровень, который опредеяет записывать сообщение или нет  
    fLogEvent     : boolean; // вызывать свойства?  
    fOnLogEvent   : TLogManagerEvent;  
    fOnLogEventObj  : TLogManagerEventObj;  
    function FormatLogTime(aDT: TDateTime): string;  
    function FormatLogMsgType(aLogMsgType: TLogMsgType): string;  
    function FormatLogMsgText(aText: string; aLogMsgType: TLogMsgType): string;  
    procedure DoLogEvent(aLogMsgText: string; aLogMsgType: TLogMsgType; aWriteToEvent: boolean);  
  protected  
    constructor Create; override;  
  public  
    procedure WriteToLog(aText: string; aLogMsgType: TLogMsgType = lmtInfo; aLvl: TLogMsgLevel = Low(TLogMsgLevel);   
      aWriteToEvent: boolean = True);  
  public  
    destructor Destroy; override;  
    property FileName: string read fFileName write fFileName; // куда писать  
    property MaxLogLevel: TLogMsgLevel read fMaxLogLevel write fMaxLogLevel; // максимальный доступный уровень  
    property LogEvent: boolean read fLogEvent write fLogEvent; // вызывать события лога?  
    property OnLogEvent: TLogManagerEvent read fOnLogEvent write fOnLogEvent; // событие лога  
    property OnLogEventObj: TLogManagerEventObj read fOnLogEventObj write fOnLogEventObj; // событие лога для объектов  
  end;  
  
// funcs  
  function LogMng: TLogManager;  
  
implementation  
  
function LogMng: TLogManager;  
begin  
  Result := TLogManager.GetInstance;  
end;  
  
{ TLogManager }  
  
constructor TLogManager.Create;  
begin  
  inherited;  
  fcs := TCriticalSection.Create;  
  fFileName := '';  
  fMaxLogLevel := High(TLogMsgLevel);  
  fLogEvent := False;  
end;  
  
destructor TLogManager.Destroy;  
begin  
  fcs.Enter;  
  try  
  finally  
    fcs.Leave;  
  end;  
  fcs.Free;  
  inherited;  
end;  
  
function TLogManager.FormatLogTime(aDT: TDateTime): string;  
begin  
  DateTimeToString(Result, '[dd.mm.yyyy] [hh:mm:ss]', aDT);  
end;  
  
function TLogManager.FormatLogMsgType(aLogMsgType: TLogMsgType): string;  
begin  
  case aLogMsgType of  
    lmtInfo: Result := '[Info]';  
    lmtWarning: Result := '[Warning]';  
    lmtError: Result := '[Error]';  
  end;  
end;  
  
function TLogManager.FormatLogMsgText(aText: string; aLogMsgType: TLogMsgType): string;  
begin  
  Result := format('%s %s: %s', [FormatLogTime(Now), FormatLogMsgType(aLogMsgType), aText]);  
end;  
  
procedure TLogManager.DoLogEvent(aLogMsgText: string; aLogMsgType: TLogMsgType; aWriteToEvent: boolean);  
begin  
  if not fLogEvent or not aWriteToEvent then  
    Exit;  
  if Assigned(fOnLogEvent) then  
    fOnLogEvent(aLogMsgText, aLogMsgType);  
  if Assigned(fOnLogEventObj) then  
    fOnLogEventObj(aLogMsgText, aLogMsgType);  
end;  
  
procedure TLogManager.WriteToLog(aText: string; aLogMsgType: TLogMsgType; aLvl: TLogMsgLevel; aWriteToEvent: boolean);  
var  
  fh  : THandle;  
  lmsg  : string;  
  card  : cardinal;  
begin  
  if fFileName = '' then  
  begin  
    lmsg := FormatLogMsgText(aText, aLogMsgType);  
    lmsg := Format('%s: %s', [FormatLogMsgType(lmtError), 'Не определён файл логирования! LogMsg: ' + lmsg]);  
    DoLogEvent(lmsg, lmtError, True);  
    Exit;  
  end;  
  if aLvl > fMaxLogLevel then  
    Exit;  
  fh := CreateFile(PChar(fFileName), GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE,  
    nil, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);  
  if fh = INVALID_HANDLE_VALUE then  
  begin  
    lmsg := FormatLogMsgText(aText, aLogMsgType);  
    lmsg := Format('%s: %s', [FormatLogMsgType(lmtError), 'Ошибка лог-файла: INVALID_HANDLE_VALUE. LogMsg: ' + lmsg]);  
    DoLogEvent(lmsg, lmtError, True);  
    Exit;  
  end;  
  fcs.Enter;  
  try  
    // формируем сообщение  
    lmsg := FormatLogMsgText(aText, aLogMsgType);  
    DoLogEvent(lmsg, aLogMsgType, aWriteToEvent);  
    lmsg := lmsg + #13#10;  
    try  
      SetFilePointer(fh, 0, nil, FILE_END);  
      WriteFile(fh, lmsg[1], sizeof(lmsg[1]) * length(lmsg), card, nil);  
    except  
      on E: Exception do  
      begin  
        lmsg := Format('%s: %s', [FormatLogMsgType(lmtError), 'Ошибка записи в файл лога: ' +  
          E.ClassName + ': ' + E.Message]);  
        DoLogEvent(lmsg, lmtError, True);  
      end;  
      Else  
      begin  
        lmsg := Format('%s: %s', [FormatLogMsgType(lmtError), 'Неизвестная ошибка в TLogManager.WriteToLog!']);  
        DoLogEvent(lmsg, lmtError, True);  
      end;  
    end;  
  finally  
    CloseHandle(fh);  
    fcs.Leave;  
  end;  
end;  
  
end.

Как видно уровней важности сообщений и типов сообщений может быть сколько угодно. Потом можно взять какой-нить хороший текстовый редактор(например я юзаю PSPad) и настроить подсветку строк относительно типов сообщений или их уровней. Красота да и только)

Пример использования

// организация лог-менеджера  
procedure TFormMain.FormActivate(Sender: TObject);  
begin  
  // ... some code ...  
  {$IFDEF Release}  
  LogMng.MaxLogLevel := lmlInfo;  
  {$ENDIF}  
  LogMng.FileName := ExtractFilePath(Application.ExeName) + 'general.log';  
  LogMng.OnLogEventObj := LogMngInfoEvent;  
  LogMng.LogEvent := True;  
  LogMng.WriteToLog('Программа запустилась!');  
end;  
  
// отображение логирования в реальном времени  
procedure TFormMain.LogMngInfoEvent(const aLogMsg: string; aLogMsgType: TLogMsgType);  
begin  
  // выводим текст разного цвета в зависимости от типа лог-сообщения  
  case aLogMsgType of  
    lmtInfo: AddLogStr(aLogMsg, clWhite, False);   
    lmtWarning: AddLogStr(aLogMsg, clYellow, False);  
    lmtError: AddLogStr(aLogMsg, clRed, False);  
  end;  
end;  
  
procedure TFormMain.AddLogStr(aLogMsg: string; aTextColor: TColor;   
  aAddTime: boolean);  
var  
  lm  : string;  
begin  
  if aAddTime then  
    DateTimeToString(lm, '[dd.mm.yyyy] [hh:mm:ss]: ', Now)  
  else  
    lm := '';  
  reLog.SelAttributes.Color := aTextColor; // вывод в TRichEdit  
  reLog.Lines.Add(lm + aLogMsg);  
end;

Заключение

Реализация проходила в Delphi 2007.

Напомню, что шаблон Singleton я рассматривал вот в этой статье, где так же приведён исходный код модулей его реализации.