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.
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