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.