Логирование (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 я рассматривал вот в этой статье, где так же приведён исходный код модулей его реализации.