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.