Логирование (logging) — это ведение записей(как правило сохранение в файл) в хронологическом порядке.
Ведение логов зачастую очень удобная, облегчающая жизнь разработчику, штука, а для больших серьёзных проектов это вообще неотъемлемая вещь.
Логирование помогает как на этапе отладки, так и на этапе внедрения, например я прицеплял утилиту к проектам, которая отправляла файл лога куда мне удобно(сайт, мыло, сервер), таким образом избегал процесса вымогательства у пользователя лог-файла.
Я использую логирование почти во всех своих проектах от средне до велика.
В этой статье я рассмотрю свою реализацию модуля ведения логов, которым пользуюсь и время от времени грейжу его.
Данный модуль можно так же применять в многопоточных приложениях.
Былина
За свою практику я переписал не мало разновидностей процессов логирования и каждый раз меня чутка тревожило ощущение, что «что-то тут осталось не таким удобным как мне хотелось бы» и от этого постоянно что-то дописывал/переписывал.
Так я пришёл от модуля, содержащего глобальные переменные — параметры лога и глобальную функцию типа WriteToLog(aMsg: string): boolean, к модулю с классом — менеджером логирования и методами записи на WinApi. Время дало мне возможность, так сказать, выбрать самый удобный вариант.
Во время улучшения менеджера логирования я встречал одну интересную реализацию в плане процесса записи:
- единое открытие файла на всё время работы приложения;
- запись по таймеру накопившейся очереди сообщений.
При этом для работы с файлом использовался какой-нить стрим, зачастую TFileStream. Но при таких раскладах у нас есть довольно большой риск «просеять» в логе сообщение именно про ту ошибку, которая и привела к краху проги из-за которой не дописался лог…
Парадоксальная неприятность)
Последнее важное изменение я внёс когда обнаружил, что мой менеджер пишет строку поверх другой строки при могопоточном использовании, хоть и допускал возможность такого использования.
Реализация
Я реализовал следующие элементы удобства:
- единый класс логирования для всего приложения посредством шаблона Singleton;
- типы сообщений (информация, предупреждение, ошибка);
- уровень приоритета сообщений (максимальный, информативный, отладочный);
- событийные типы как для объектных методов так и для обычных процедур;
- настройка менеджера через свойства;
- формирование заголовка из текущей даты и типа сообщения.
Код всего модуля uLogManager отображён в следующем листинге (Delphi 2007):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
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) и настроить подсветку строк относительно типов сообщений или их уровней. Красота да и только)
Пример использования
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// организация лог-менеджера 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 я рассматривал вот в этой статье, где так же приведён исходный код модулей его реализации.