From 273c963fd99538c718febc6d8bf7440423cbf08a Mon Sep 17 00:00:00 2001 From: fastbike Date: Tue, 6 Aug 2024 14:42:02 +1200 Subject: [PATCH 1/2] https://github.com/danieleteti/loggerpro/issues/92 Re factored file rotation: - moved functionality into own helper object with base interface to define behaviour - provide two concrete implementations: - Rotate by file count - Rotate by file date - Tidied up the LogFileFormat checks to use an enumeration - Moved file maintenance methods (Delete / Rename) to the helper objects --- LoggerPro.FileAppender.pas | 541 ++++++++++++++++++++++++++++--------- 1 file changed, 415 insertions(+), 126 deletions(-) diff --git a/LoggerPro.FileAppender.pas b/LoggerPro.FileAppender.pas index 9c45089..52fb9b5 100644 --- a/LoggerPro.FileAppender.pas +++ b/LoggerPro.FileAppender.pas @@ -34,7 +34,8 @@ interface LoggerPro, System.Generics.Collections, System.Classes, - System.SysUtils; + System.SysUtils, + System.JSON; type { @@ -60,26 +61,92 @@ interface } + + // valid placeholders for log file parts + TLogFileNamePart = (lfnModule, lfnNumber, lfnTag, lfnPID, lfnDate); + TLogFileNameParts = set of TLogFileNamePart; + +type + /// handles file rotation and file name formats + ILogFileRotator = interface + ['{4E495CF4-793F-4D7E-8BC1-5257FB11370D}'] + procedure RotateFiles(const aLogTag: string; out aNewFileName: string); + procedure CheckLogFileNameFormat(const LogFileNameFormat: string); + function GetLogFileName(const aTag: string; const aFileNumber: Integer): string; + end; + + { forward declaration } + TLoggerProFileAppenderBase = class; + + TLogFileRotatorBase = class abstract(TInterfacedObject, ILogFileRotator) + protected + FAppender: TLoggerProFileAppenderBase; + FRequiredFileNameParts: TLogFileNameParts; + procedure Setup(Config: TJSONObject); virtual; abstract; + procedure RetryMove(const aFileSrc, aFileDest: string); + procedure RetryDelete(const aFileSrc: string); + { ILogFileMaintainer } + procedure RotateFiles(const aLogTag: string; out aNewFileName: string); virtual; abstract; + function GetLogFileName(const aTag: string; const aFileNumber: Integer): string; virtual; + procedure CheckLogFileNameFormat(const LogFileNameFormat: string); virtual; + public + class function GetDefaultLogFileMaintainer(Appender: TLoggerProFileAppenderBase; AMaxFileCount: Integer = 10): ILogFileRotator; + constructor Create(Appender: TLoggerProFileAppenderBase; AConfiguration: string); virtual; + end; + + TLogFileMaintainerClass = class of TLogFileRotatorBase; + + /// Rotate / purge log files by file count + TLogFileRotatorByCount = class(TLogFileRotatorBase) + private + FMaxBackupFileCount: Integer; + protected + procedure Setup(Config: TJSONObject); override; + procedure RotateFiles(const aLogTag: string; out aNewFileName: string); override; + public + const + { @abstract(Defines number of log file set to maintain during logs rotation) } + DEFAULT_MAX_BACKUP_FILE_COUNT = 5; + constructor Create(Appender: TLoggerProFileAppenderBase; AConfiguration: string); override; + end; + + /// Rotate / purge log files by number of days + TLogFileRotatorByDate = class(TLogFileRotatorBase) + private + FMaxFileDays: Integer; + protected + procedure Setup(Config: TJSONObject); override; + procedure RotateFiles(const aLogTag: string; out aNewFileName: string); override; + function GetLogFileName(const aTag: string; const aFileNumber: Integer): string; override; + public const + { @abstract(Defines number of days of log files to maintain during logs rotation) } + DEFAULT_MAX_BACKUP_FILE_DAYS = 7; + constructor Create(Appender: TLoggerProFileAppenderBase; AConfiguration: string); override; + end; + + { @abstract(The base class for different file appenders) Do not use this class directly, but one of TLoggerProFileAppender or TLoggerProSimpleFileAppender. Check the sample @code(file_appender.dproj) } + TLoggerProFileAppenderBase = class(TLoggerProAppenderBase) private - fMaxBackupFileCount: Integer; + FLogFileRotator: ILogFileRotator; fMaxFileSizeInKiloByte: Integer; fLogFileNameFormat: string; fLogsFolder: string; fEncoding: TEncoding; function CreateWriter(const aFileName: string): TStreamWriter; - procedure RetryMove(const aFileSrc, aFileDest: string); protected - procedure CheckLogFileNameFormat(const LogFileNameFormat: String); virtual; + property LogsFolder: string read fLogsFolder; + property LogFileNameFormat: string read fLogFileNameFormat; + procedure CheckLogFileNameFormat(const LogFileNameFormat: string); procedure EmitStartRotateLogItem(aWriter: TStreamWriter); virtual; procedure EmitEndRotateLogItem(aWriter: TStreamWriter); virtual; - function GetLogFileName(const aTag: string; const aFileNumber: Integer): string; virtual; + function GetLogFileName(const aTag: string; const aFileNumber: Integer): string; procedure WriteToStream(const aStreamWriter: TStreamWriter; const aValue: string); inline; - procedure RotateFile(const aLogTag: string; out aNewFileName: string); virtual; + procedure RotateFile(const aLogTag: string; out aNewFileName: string); procedure InternalWriteLog(const aStreamWriter: TStreamWriter; const aLogItem: TLogItem); public const { @abstract(Defines the default format string used by the @link(TLoggerProFileAppender).) @@ -92,8 +159,7 @@ TLoggerProFileAppenderBase = class(TLoggerProAppenderBase) } DEFAULT_FILENAME_FORMAT = '{module}.{number}.{tag}.log'; DEFAULT_FILENAME_FORMAT_WITH_PID = '{module}.{number}.{pid}.{tag}.log'; - { @abstract(Defines number of log file set to maintain during logs rotation) } - DEFAULT_MAX_BACKUP_FILE_COUNT = 5; + { @abstract(Defines the max size of each log file) The actual meaning is: "If the file size is > than @link(DEFAULT_MAX_FILE_SIZE_KB) then rotate logs. } DEFAULT_MAX_FILE_SIZE_KB = 1000; @@ -102,13 +168,23 @@ TLoggerProFileAppenderBase = class(TLoggerProAppenderBase) { @abstract(How many times do we have to retry if the file is locked?. } RETRY_COUNT = 5; constructor Create( - aMaxBackupFileCount: Integer = TLoggerProFileAppenderBase.DEFAULT_MAX_BACKUP_FILE_COUNT; + aMaxBackupFileCount: Integer = TLogFileRotatorByCount.DEFAULT_MAX_BACKUP_FILE_COUNT; + aMaxFileSizeInKiloByte: Integer = TLoggerProFileAppenderBase.DEFAULT_MAX_FILE_SIZE_KB; + aLogsFolder: string = ''; + aLogFileNameFormat: string = TLoggerProFileAppenderBase.DEFAULT_FILENAME_FORMAT; + aLogItemRenderer: ILogItemRenderer = nil; + aEncoding: TEncoding = nil); + reintroduce; overload; virtual; + + constructor Create( + aLogFileMaintainer: TLogFileMaintainerClass; + aMaintainerConfiguration: string ='{"MaxBackupFileDays":7}'; aMaxFileSizeInKiloByte: Integer = TLoggerProFileAppenderBase.DEFAULT_MAX_FILE_SIZE_KB; aLogsFolder: string = ''; aLogFileNameFormat: string = TLoggerProFileAppenderBase.DEFAULT_FILENAME_FORMAT; aLogItemRenderer: ILogItemRenderer = nil; aEncoding: TEncoding = nil); - reintroduce; virtual; + reintroduce; overload; virtual; procedure Setup; override; end; @@ -139,7 +215,6 @@ TLoggerProSimpleFileAppender = class(TLoggerProFileAppenderBase) fFileWriter: TStreamWriter; procedure RotateLog; protected - procedure CheckLogFileNameFormat(const LogFileNameFormat: String); override; public const DEFAULT_FILENAME_FORMAT = '{module}.{number}.log'; @@ -147,23 +222,24 @@ TLoggerProSimpleFileAppender = class(TLoggerProFileAppenderBase) procedure TearDown; override; procedure WriteLog(const aLogItem: TLogItem); overload; override; constructor Create( - aMaxBackupFileCount: Integer = TLoggerProFileAppenderBase.DEFAULT_MAX_BACKUP_FILE_COUNT; + aMaxBackupFileCount: Integer = TLogFileRotatorByCount.DEFAULT_MAX_BACKUP_FILE_COUNT; aMaxFileSizeInKiloByte: Integer = TLoggerProFileAppenderBase.DEFAULT_MAX_FILE_SIZE_KB; aLogsFolder: string = ''; aLogFileNameFormat: string = TLoggerProSimpleFileAppender.DEFAULT_FILENAME_FORMAT; aLogItemRenderer: ILogItemRenderer = nil; aEncoding: TEncoding = nil); - override; + overload; override; end; - implementation uses System.IOUtils, System.StrUtils, System.Math, - idGlobal + System.DateUtils, + idGlobal, + System.Rtti {$IF Defined(Android), System.SysUtils} ,Androidapi.Helpers ,Androidapi.JNI.GraphicsContentViewText @@ -184,46 +260,14 @@ function OccurrencesOfChar(const S: string; const C: char): integer; procedure TLoggerProFileAppenderBase.CheckLogFileNameFormat(const LogFileNameFormat: String); begin - //DEFAULT_FILENAME_FORMAT = '{module}.{number}.{tag}.log'; - if not (LogFileNameFormat.Contains('{number}') and LogFileNameFormat.Contains('{tag}')) then - begin - raise ELoggerPro.CreateFmt('Wrong FileFormat [%s] - [HINT] A correct file format for %s requires {number} and {tag} placeholders ({module} is optional). A valid file format is : %s', - [ - ClassName, - LogFileNameFormat, - TLoggerProFileAppenderBase.DEFAULT_FILENAME_FORMAT - ]); - end; + FLogFileRotator.CheckLogFileNameFormat(LogFileNameFormat); end; - { TLoggerProFileAppenderBase } function TLoggerProFileAppenderBase.GetLogFileName(const aTag: string; const aFileNumber: Integer): string; -var -// lExt: string; - lModuleName: string; - lPath: string; - lFormat: string; begin -{$IF Defined(Android)} - lModuleName := TAndroidHelper.ApplicationTitle.Replace(' ', '_', [rfReplaceAll]); -{$ENDIF} -{$IF not Defined(Mobile)} - lModuleName := TPath.GetFileNameWithoutExtension(GetModuleName(HInstance)); -{$ENDIF} -{$IF Defined(IOS)} - raise Exception.Create('Platform not supported'); -{$ENDIF} - lFormat := fLogFileNameFormat; - - lPath := fLogsFolder; - lFormat := lFormat - .Replace('{module}', lModuleName, [rfReplaceAll]) - .Replace('{number}', aFileNumber.ToString.PadLeft(2,'0') , [rfReplaceAll]) - .Replace('{tag}', aTag, [rfReplaceAll]) - .Replace('{pid}', CurrentProcessId.ToString.PadLeft(8,'0'), [rfReplaceAll]); - Result := TPath.Combine(lPath, lFormat); + Result := FLogFileRotator.GetLogFileName(aTag, aFileNumber); end; procedure TLoggerProFileAppenderBase.Setup; @@ -254,57 +298,9 @@ procedure TLoggerProFileAppenderBase.InternalWriteLog(const aStreamWriter: TStre WriteToStream(aStreamWriter, FormatLog(aLogItem)); end; -procedure TLoggerProFileAppenderBase.RetryMove(const aFileSrc, aFileDest: string); -var - lRetries: Integer; -const - MAX_RETRIES = 5; -begin - lRetries := 0; - repeat - try - Sleep(50); - // the incidence of "Locked file goes to nearly zero..." - TFile.Move(aFileSrc, aFileDest); - Break; - except - on E: EInOutError do - begin - Inc(lRetries); - Sleep(50); - end; - on E: Exception do - begin - raise; - end; - end; - until lRetries = MAX_RETRIES; - - if lRetries = MAX_RETRIES then - raise ELoggerPro.CreateFmt('Cannot rename %s to %s', [aFileSrc, aFileDest]); -end; - procedure TLoggerProFileAppenderBase.RotateFile(const aLogTag: string; out aNewFileName: string); -var - lRenamedFile: string; - I: Integer; - lCurrentFileName: string; begin - aNewFileName := GetLogFileName(aLogTag, 0); - // remove the last file of backup set - lRenamedFile := GetLogFileName(aLogTag, fMaxBackupFileCount - 1); - if TFile.Exists(lRenamedFile) then - TFile.Delete(lRenamedFile); - // shift the files names - for I := fMaxBackupFileCount - 1 downto 1 do - begin - lCurrentFileName := GetLogFileName(aLogTag, I); - lRenamedFile := GetLogFileName(aLogTag, I + 1); - if TFile.Exists(lCurrentFileName) then - RetryMove(lCurrentFileName, lRenamedFile); - end; - lRenamedFile := GetLogFileName(aLogTag, 1); - RetryMove(aNewFileName, lRenamedFile); + FLogFileRotator.RotateFiles(aLogTag, aNewFileName); end; constructor TLoggerProFileAppenderBase.Create( @@ -314,11 +310,25 @@ constructor TLoggerProFileAppenderBase.Create( aLogFileNameFormat: string; aLogItemRenderer: ILogItemRenderer; aEncoding: TEncoding); +begin + Create(TLogFileRotatorByCount, Format('{"MaxBackupFileCount":%d}', [aMaxBackupFileCount]), + aMaxFileSizeInKiloByte, aLogsFolder, aLogFileNameFormat, aLogItemRenderer, aEncoding); +end; + +constructor TLoggerProFileAppenderBase.Create( + aLogFileMaintainer: TLogFileMaintainerClass; + aMaintainerConfiguration: string; + aMaxFileSizeInKiloByte: Integer; + aLogsFolder, aLogFileNameFormat: string; + aLogItemRenderer: ILogItemRenderer; + aEncoding: TEncoding); begin inherited Create(aLogItemRenderer); fLogsFolder := aLogsFolder; - fMaxBackupFileCount:= Max(1, aMaxBackupFileCount); fMaxFileSizeInKiloByte := aMaxFileSizeInKiloByte; + + FLogFileRotator := aLogFileMaintainer.Create(Self, aMaintainerConfiguration); + CheckLogFileNameFormat(aLogFileNameFormat); fLogFileNameFormat := aLogFileNameFormat; if Assigned(aEncoding) then @@ -337,7 +347,7 @@ function TLoggerProFileAppenderBase.CreateWriter(const aFileName: string): TStre if not TFile.Exists(aFileName) then lFileAccessMode := lFileAccessMode or fmCreate; - // If the file si still blocked by a precedent execution or + // If the file is still blocked by a precedent execution or // for some other reasons, we try to access the file for 5 times. // If after 5 times (with a bit of delay in between) the file is still // locked, then the exception is raised. @@ -394,14 +404,12 @@ procedure TLoggerProFileAppender.RotateLog(const aLogTag: string; aWriter: TStre lLogFileName: string; begin EmitEndRotateLogItem(aWriter); - //WriteToStream(aWriter, '#[ROTATE LOG ' + datetimetostr(Now, FormatSettings) + ']'); // remove the writer during rename fWritersDictionary.Remove(aLogTag); RotateFile(aLogTag, lLogFileName); // re-create the writer AddWriter(aLogTag, aWriter, lLogFileName); EmitStartRotateLogItem(aWriter); - //WriteToStream(aWriter, '#[START LOG ' + datetimetostr(Now, FormatSettings) + ']'); end; procedure TLoggerProFileAppender.Setup; @@ -435,33 +443,13 @@ procedure TLoggerProFileAppender.WriteLog(const aLogItem: TLogItem); end; { TLoggerProSimpleFileAppender } - -procedure TLoggerProSimpleFileAppender.CheckLogFileNameFormat(const LogFileNameFormat: String); -begin - //DEFAULT_FILENAME_FORMAT = '{module}.{number}.{tag}.log'; - if not LogFileNameFormat.Contains('{number}') then - begin - raise ELoggerPro.CreateFmt('Wrong FileFormat [%s] - [HINT] A correct file format for %s requires {number} placeholder ({module} is optional). A valid file format is : %s', - [ - ClassName, - LogFileNameFormat, - TLoggerProSimpleFileAppender.DEFAULT_FILENAME_FORMAT - ]); - end; -end; - constructor TLoggerProSimpleFileAppender.Create(aMaxBackupFileCount, aMaxFileSizeInKiloByte: Integer; - aLogsFolder: string; aLogFileNameFormat: String; + aLogsFolder: string; aLogFileNameFormat: string; aLogItemRenderer: ILogItemRenderer; aEncoding: TEncoding); begin - inherited Create( - aMaxBackupFileCount, - aMaxFileSizeInKiloByte, - aLogsFolder, - aLogFileNameFormat, - aLogItemRenderer, - aEncoding); + Create(TLogFileRotatorByCount, Format('{"MaxBackupFileCount":%d}', [aMaxBackupFileCount]), + aMaxFileSizeInKiloByte, aLogsFolder, aLogFileNameFormat, aLogItemRenderer, aEncoding); end; procedure TLoggerProSimpleFileAppender.RotateLog; @@ -473,7 +461,7 @@ procedure TLoggerProSimpleFileAppender.RotateLog; fFileWriter.Free; RotateFile('', lLogFileName); // re-create the writer - fFileWriter := CreateWriter(GetLogFileName('', 0)); + fFileWriter := CreateWriter(lLogFileName); EmitStartRotateLogItem(fFileWriter); end; @@ -498,5 +486,306 @@ procedure TLoggerProSimpleFileAppender.WriteLog(const aLogItem: TLogItem); end; end; + +{ TLogFileRotatorBase } +class function TLogFileRotatorBase.GetDefaultLogFileMaintainer(Appender: TLoggerProFileAppenderBase; AMaxFileCount: Integer): ILogFileRotator; +begin + Result := TLogFileRotatorByCount.Create(Appender, ''); +end; + +procedure TLogFileRotatorBase.CheckLogFileNameFormat(const LogFileNameFormat: string); + + function GetFilePartEnumValue(Value: TLogFileNamePart): string; + begin + Result := Format('{%s}', [Copy(TRttiEnumerationType.GetName(Value), 4).ToLower]); + end; + + function GetMissingFileNameParts: string; + var + NamePart: string; + begin + for var FileNamePart := Low(TLogFileNamePart) to High(TLogFileNamePart) do + begin + NamePart := GetFilePartEnumValue(FileNamePart); + if (FileNamePart in FRequiredFileNameParts) and + not LogFileNameFormat.Contains(NamePart) then + Result := Result +NamePart +','; + end; + if not Result.IsEmpty then + SetLength(Result, Length(Result) -1); + end; + +var + MissingParts: string; +begin + MissingParts := GetMissingFileNameParts; + if not MissingParts.IsEmpty then + begin + raise ELoggerPro.CreateFmt( + 'Wrong FileFormat [%s] - [HINT] A correct file format for %s requires %s placeholders. A valid file format is like : %s', + [ + LogFileNameFormat, + FAppender.ClassName, + MissingParts, + TLoggerProFileAppenderBase.DEFAULT_FILENAME_FORMAT + ]); + end; +end; + +constructor TLogFileRotatorBase.Create(Appender: TLoggerProFileAppenderBase; + AConfiguration: string); +var + Config: TJSONObject; +begin + inherited Create; + FAppender := Appender; + Config:= TJSONObject.ParseJSONValue(AConfiguration) as TJSONObject; + try + Setup(Config); + finally + Config.Free; + end; +end; + +procedure TLogFileRotatorBase.RetryDelete(const aFileSrc: string); +var + lRetries: Integer; +const + MAX_RETRIES = 5; +begin + lRetries := 0; + repeat + try + Sleep(50); + // the incidence of "Locked file goes to nearly zero..." + TFile.Delete(aFileSrc); + if not TFile.Exists(aFileSrc) then + begin + Break; + end; + except + on E: Exception do + begin + Inc(lRetries); + Sleep(100); + end; + end; + until lRetries = MAX_RETRIES; + + if lRetries = MAX_RETRIES then + raise ELoggerPro.CreateFmt('Cannot delete file %s', [aFileSrc]); +end; + +procedure TLogFileRotatorBase.RetryMove(const aFileSrc, aFileDest: string); +var + lRetries: Integer; +const + MAX_RETRIES = 5; +begin + lRetries := 0; + repeat + try + Sleep(50); + // the incidence of "Locked file goes to nearly zero..." + TFile.Move(aFileSrc, aFileDest); + Break; + except + on E: EInOutError do + begin + Inc(lRetries); + Sleep(100); + end; + on E: Exception do + begin + raise; + end; + end; + until lRetries = MAX_RETRIES; + + if lRetries = MAX_RETRIES then + raise ELoggerPro.CreateFmt('Cannot rename %s to %s', [aFileSrc, aFileDest]); +end; + +function TLogFileRotatorBase.GetLogFileName(const aTag: string; const aFileNumber: Integer): string; +var + lModuleName: string; + lPath: string; + lFormat: string; +begin +{$IF Defined(Android)} + lModuleName := TAndroidHelper.ApplicationTitle.Replace(' ', '_', [rfReplaceAll]); +{$ENDIF} +{$IF not Defined(Mobile)} + lModuleName := TPath.GetFileNameWithoutExtension(GetModuleName(HInstance)); +{$ENDIF} +{$IF Defined(IOS)} + raise Exception.Create('Platform not supported'); +{$ENDIF} + lFormat := FAppender.LogFileNameFormat; + + lPath := FAppender.LogsFolder; + lFormat := lFormat + .Replace('{module}', lModuleName, [rfReplaceAll]) +// todo: what happens when more than one hundred files +// should this be linked to max file count ? + .Replace('{number}', aFileNumber.ToString.PadLeft(2,'0') , [rfReplaceAll]) + .Replace('{tag}', aTag, [rfReplaceAll]) + .Replace('{date}', FormatDateTime('yyyy-mm-dd', Now), [rfReplaceAll]) + .Replace('{pid}', CurrentProcessId.ToString.PadLeft(8,'0'), [rfReplaceAll]); + Result := TPath.Combine(lPath, lFormat); +end; + + +{ TLogFileRotatorByCount } +constructor TLogFileRotatorByCount.Create(Appender: TLoggerProFileAppenderBase; + AConfiguration: string); +begin + inherited Create(Appender, AConfiguration); + FRequiredFileNameParts:= [lfnModule, lfnNumber]; +end; + +procedure TLogFileRotatorByCount.RotateFiles(const aLogTag: string; out aNewFileName: string); +var + lRenamedFile: string; + I: Integer; + lCurrentFileName: string; +begin + aNewFileName := FAppender.GetLogFileName(aLogTag, 0); + // remove the last file of backup set + lRenamedFile := FAppender.GetLogFileName(aLogTag, fMaxBackupFileCount - 1); + if TFile.Exists(lRenamedFile) then + begin + TFile.Delete(lRenamedFile); + if TFile.Exists(lRenamedFile) then // double check for slow file systems + begin + RetryDelete(lRenamedFile); + end; + end; + // shift the files names + for I := fMaxBackupFileCount - 1 downto 1 do + begin + lCurrentFileName := FAppender.GetLogFileName(aLogTag, I); + lRenamedFile := FAppender.GetLogFileName(aLogTag, I + 1); + if TFile.Exists(lCurrentFileName) then + begin + RetryMove(lCurrentFileName, lRenamedFile); + end; + end; + lRenamedFile := FAppender.GetLogFileName(aLogTag, 1); + RetryMove(aNewFileName, lRenamedFile); +end; + +procedure TLogFileRotatorByCount.Setup(Config: TJSONObject); +begin + if not Config.TryGetValue('MaxBackupFileCount', FMaxBackupFileCount) then + FMaxBackupFileCount := DEFAULT_MAX_BACKUP_FILE_COUNT; + FMaxBackupFileCount := Max(1, FMaxBackupFileCount); +end; + +{ TLogFileRotatorByDate } +constructor TLogFileRotatorByDate.Create(Appender: TLoggerProFileAppenderBase; AConfiguration: string); +begin + inherited Create(Appender, AConfiguration); + FRequiredFileNameParts := [lfnModule, lfnNumber, lfnDate]; +end; + +procedure TLogFileRotatorByDate.RotateFiles(const aLogTag: string; out aNewFileName: string); +type + TTLogFileNamePartLookup = array[TLogFileNamePart] of Integer; + + function GetCurrentFileDateString(Index: Integer): string; + var + FileNameParts: TArray; + begin + FileNameParts := FAppender.GetLogFileName(aLogTag, 0).Split(['.']); + Result := FileNameParts[Index]; + end; + + function CreateLookupArrayForLogFileNameFormat: TTLogFileNamePartLookup; + { get the relative position of the file name format placeholders in the LogFileNameFormat field} + var + Parts: TArray; + EnumStr: string; + Enum: TLogFileNamePart; + begin + Result := Default(TTLogFileNamePartLookup); + Parts := FAppender.LogFileNameFormat.Split(['.']); + for var J := Low(Parts) to High(Parts) do + begin + EnumStr := 'lfn' + Copy(Parts[J], 2, Length(Parts[J]) - 2); + Enum := TRttiEnumerationType.GetValue(EnumStr); + if (Enum >= Low(TLogFileNamePart)) and (Enum <= High(TLogFileNamePart)) then + Result[Enum] := J; + end; + end; + +var + ModuleName: string; + FilesToDelete: TArray; + FileDateThreshold: TDate; + CurrentFileDateString: string; + MaxFileVersion: Integer; + FilePartIndex: TTLogFileNamePartLookup; + +begin + { delete all files older than a certain date } + FileDateThreshold := Trunc(Now) - FMaxFileDays; + FilesToDelete := TDirectory.GetFiles(FAppender.LogsFolder, + function(const Path: string; const SearchRec: TSearchRec): Boolean + begin + Result := SearchRec.TimeStamp < FileDateThreshold; + end); + for var I := Low(FilesToDelete) to High(FilesToDelete) do + begin + if TFile.Exists(FilesToDelete[I]) then + try + TFile.Delete(FilesToDelete[I]); + if TFile.Exists(FilesToDelete[I]) then // double check for slow file systems + begin + RetryDelete(FilesToDelete[I]); + end; + except + { no point retrying, file monitoring will alert us when we have too many files } + end; + end; + + { files will look like module.date.xx.log, we will just roll the xx part forward } + FilePartIndex := CreateLookupArrayForLogFileNameFormat; + ModuleName := TPath.GetFileNameWithoutExtension(GetModuleName(HInstance)); + CurrentFileDateString := GetCurrentFileDateString(FilePartIndex[lfnDate]); + + MaxFileVersion := 0; + TDirectory.GetFiles(FAppender.LogsFolder, + function(const Path: string; const SearchRec: TSearchRec): Boolean + var + NameParts: TArray; + begin + { only want files with module and date in filename root } + NameParts := string(SearchRec.Name).Split(['.']); + if Length(NameParts) > 2 then + begin + if SameText(NameParts[FilePartIndex[lfnModule]], ModuleName) + and SameText(NameParts[FilePartIndex[lfnDate]], CurrentFileDateString) then + MaxFileVersion := Max(MaxFileVersion, StrToIntDef(NameParts[FilePartIndex[lfnNumber]], 0)); + end; + Result := False; { NB: Predicate does not return any files } + end); + Inc(MaxFileVersion); + aNewFileName := FAppender.GetLogFileName(aLogTag, MaxFileVersion); +end; + +procedure TLogFileRotatorByDate.Setup(Config: TJSONObject); +begin + if not Config.TryGetValue('MaxBackupFileDays', FMaxFileDays) then + FMaxFileDays := DEFAULT_MAX_BACKUP_FILE_DAYS; + FMaxFileDays := Max(1, FMaxFileDays); +end; + +function TLogFileRotatorByDate.GetLogFileName(const aTag: string; const aFileNumber: Integer): string; +begin + Result := inherited; + Result := Result.Replace('{date}', FormatDateTime('yyyy-mm-dd', Now), [rfReplaceAll]); +end; + end. From f076c51a28d450143c3ae6d637f8a8be6fdf7a89 Mon Sep 17 00:00:00 2001 From: fastbike Date: Tue, 13 Aug 2024 09:29:12 +1200 Subject: [PATCH 2/2] Added interface to allow for the Loglevel to be read/written at runtime --- LoggerPro.pas | 88 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/LoggerPro.pas b/LoggerPro.pas index 695597a..6cd5cf4 100644 --- a/LoggerPro.pas +++ b/LoggerPro.pas @@ -176,6 +176,12 @@ TAppenderQueue = class(TThreadSafeQueue) procedure LogFmt(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); deprecated; end; + ILogWriterLogLevel = interface + ['{7345DEA0-377E-4799-9187-038B4EE30D62}'] + function GetLogLevel: TLogType; + procedure SetLogLevel(Value: TLogType); + end; + TLogAppenderList = TList; TAppenderThread = class(TThread) @@ -247,9 +253,9 @@ TCustomLogWriter = class(TLoggerProInterfacedObject, ICustomLogWriter) FLoggerThread: TLoggerThread; FLogAppenders: TLogAppenderList; FFreeAllowed: Boolean; - FLogLevel: TLogType; function GetAppendersClassNames: TArray; protected + FLogLevel: TLogType; procedure Initialize(const aEventsHandler: TLoggerProEventsHandler); public constructor Create(const aLogLevel: TLogType = TLogType.Debug); overload; @@ -291,6 +297,14 @@ TLogWriter = class(TCustomLogWriter, ILogWriter) procedure LogFmt(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); end; + TLogWriterClass = class of TLogWriter; + + TLogDynamicWriter = class(TLogWriter, ILogWriterLogLevel) + protected + function GetLogLevel: TLogType; + procedure SetLogLevel(Value: TLogType); + end; + TOnAppenderLogRow = reference to procedure(const LogItem: TLogItem; out LogRow: string); TLoggerProAppenderBase = class abstract(TInterfacedObject, ILogAppender) @@ -368,6 +382,8 @@ TLogItemRenderer = class abstract(TInterfacedObject, ILogItemRenderer) procedure Setup; virtual; procedure TearDown; virtual; function RenderLogItem(const aLogItem: TLogItem): String; virtual;abstract; + public + class function GetDefaultLogItemRenderer: ILogItemRenderer; end; TLogItemRendererClass = class of TLogItemRenderer; @@ -375,7 +391,14 @@ TLogItemRendererClass = class of TLogItemRenderer; function GetDefaultFormatSettings: TFormatSettings; function StringToLogType(const aLogType: string): TLogType; function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler = nil; - aLogLevel: TLogType = TLogType.Debug): ILogWriter; + aLogLevel: TLogType = TLogType.Debug): ILogWriter; overload; +function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler; + aLogLevels: TArray): ILogWriter; overload; +function BuildLogWriter(aWriterClass: TLogWriterClass; aAppenders: array of ILogAppender; + aEventsHandlers: TLoggerProEventsHandler = nil; aLogLevel: TLogType = TLogType.Debug): ILogWriter; overload; +function BuildLogWriter(aWriterClass: TLogWriterClass; aAppenders: array of ILogAppender; + aEventsHandlers: TLoggerProEventsHandler; aLogLevels: TArray): ILogWriter; overload; + function LogLayoutByPlaceHoldersToLogLayoutByIndexes(const LogLayoutByPlaceHolders: String; const UseZeroBasedIncrementalIndexes: Boolean): String; implementation @@ -481,19 +504,56 @@ function StringToLogType(const aLogType: string): TLogType; end; function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler; aLogLevel: TLogType): ILogWriter; +begin + Result := BuildLogWriter(TLogWriter, aAppenders, aEventsHandlers, aLogLevel); +end; + +function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler; aLogLevels: TArray): ILogWriter; +begin + Result := BuildLogWriter(TLogWriter, aAppenders, aEventsHandlers, aLogLevels); +end; + +function BuildLogWriter(aWriterClass: TLogWriterClass; aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler; aLogLevels: TArray): ILogWriter; var lLogAppenders: TLogAppenderList; - lLogAppender: ILogAppender; + lLowestLogLevel: TLogType; + I: Integer; begin + lLowestLogLevel := TLogType.Fatal; + if Length(aAppenders) <> Length(aLogLevels) then + begin + raise ELoggerPro.Create('LogLevels.Count <> Appenders.Count'); + end; lLogAppenders := TLogAppenderList.Create; - for lLogAppender in aAppenders do + for I := 0 to Length(aAppenders) - 1 do begin - lLogAppenders.Add(lLogAppender); + lLogAppenders.Add(aAppenders[I]); + aAppenders[I].SetLogLevel(aLogLevels[I]); + if aLogLevels[I] < lLowestLogLevel then + begin + lLowestLogLevel := aLogLevels[I]; + end; end; - Result := TLogWriter.Create(lLogAppenders, aLogLevel); + Result := + aWriterClass.Create(lLogAppenders, lLowestLogLevel); TLogWriter(Result).Initialize(aEventsHandlers); end; +function BuildLogWriter(aWriterClass: TLogWriterClass; aAppenders: array of ILogAppender; + aEventsHandlers: TLoggerProEventsHandler = nil; aLogLevel: TLogType = TLogType.Debug): ILogWriter; overload; +var + lLogLevelsArray: TArray; + I: Integer; +begin + SetLength(lLogLevelsArray, length(aAppenders)); + for I := 0 to Length(lLogLevelsArray) - 1 do + begin + lLogLevelsArray[I] := aLogLevel; + end; + Result := BuildLogWriter(aWriterClass, aAppenders, aEventsHandlers, lLogLevelsArray); +end; + + { TLogger.TCustomLogWriter } function TCustomLogWriter.AppendersCount: Integer; @@ -689,6 +749,17 @@ procedure TLogWriter.WarnFmt(const aMessage: string; const aParams: array of TVa Warn(aMessage, aParams, aTag); end; +{ TLogDynamicWriter } +function TLogDynamicWriter.GetLogLevel: TLogType; +begin + Result := FLogLevel; +end; + +procedure TLogDynamicWriter.SetLogLevel(Value: TLogType); +begin + FLogLevel := Value; +end; + { TLogger.TLogItem } function TLogItem.Clone: TLogItem; @@ -1093,6 +1164,11 @@ function TLoggerProInterfacedObject._Release: Integer; { TLogItemRenderer } +class function TLogItemRenderer.GetDefaultLogItemRenderer: ILogItemRenderer; +begin + Result := LoggerPro.Renderers.GetDefaultLogItemRenderer; +end; + procedure TLogItemRenderer.Setup; begin // do nothing