2024-01-02 16:30:52 +01:00
// *************************************************************************** }
//
// LoggerPro
//
2024-04-03 18:42:32 +02:00
// Copyright (c) 2010-2024 Daniele Teti
2024-01-02 16:30:52 +01:00
//
// https://github.com/danieleteti/loggerpro
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ***************************************************************************
2023-02-27 12:26:25 +01:00
unit LoggerPro. FileAppender;
{$IF Defined(Android) or Defined(iOS)}
{$DEFINE MOBILE}
{$ENDIF}
interface
uses
LoggerPro,
System. Generics. Collections,
2023-12-19 21:48:50 +01:00
System. Classes,
System. SysUtils;
2023-02-27 12:26:25 +01:00
type
{
@ abstract( Logs to file using one different file for each different TAG used. )
@ author( Daniele Teti - d. teti@ bittime. it)
Implements log rotations.
This appender is the default appender when no configuration is done on the @ link( TLogger) class .
Without any configuration LoggerPro uses the @ link( TLoggerProFileAppender) with the default configuration.
So the following two blocks of code are equivalent:
@ longcode( #
.. .
TLogger. Initialize; //=> uses the TLoggerProFileAppender because no other configuration is provided
.. .
.. .
TLogger. AddAppender( TLoggerProFileAppender. Create) ;
TLogger. Initialize //=> uses the TLoggerProFileAppender as configured
.. .
# )
}
{ @ 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
2024-05-29 14:26:01 +02:00
procedure RetryMove( const aFileSrc, aFileDest: string ) ;
procedure RetryDelete( const aFileSrc: string ) ;
protected
fEncoding: TEncoding;
2023-02-27 12:26:25 +01:00
fMaxBackupFileCount: Integer ;
fMaxFileSizeInKiloByte: Integer ;
fLogFileNameFormat: string ;
fLogsFolder: string ;
2024-05-29 14:26:01 +02:00
function CreateWriter( const aFileName: string ; const aBufferSize: Integer = 3 2 ) : TStreamWriter;
2023-12-19 21:48:50 +01:00
procedure CheckLogFileNameFormat( const LogFileNameFormat: String ) ; virtual ;
procedure EmitStartRotateLogItem( aWriter: TStreamWriter) ; virtual ;
procedure EmitEndRotateLogItem( aWriter: TStreamWriter) ; virtual ;
function GetLogFileName( const aTag: string ; const aFileNumber: Integer ) : string ; virtual ;
2023-02-27 12:26:25 +01:00
procedure WriteToStream( const aStreamWriter: TStreamWriter; const aValue: string ) ; inline ;
procedure RotateFile( const aLogTag: string ; out aNewFileName: string ) ; virtual ;
procedure InternalWriteLog( const aStreamWriter: TStreamWriter; const aLogItem: TLogItem) ;
public const
{ @ abstract( Defines the default format string used by the @ link( TLoggerProFileAppender) . )
The positional parameters are the following:
@ orderedList(
2024-01-02 16:30:52 +01:00
@ item Number
@ item Module
@ item Tag
2023-02-27 12:26:25 +01:00
)
}
2023-10-18 16:25:23 +02:00
DEFAULT_FILENAME_FORMAT = '{module}.{number}.{tag}.log' ;
2024-01-02 16:30:52 +01:00
DEFAULT_FILENAME_FORMAT_WITH_PID = '{module}.{number}.{pid}.{tag}.log' ;
2023-02-27 12:26:25 +01:00
{ @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 = 1 0 0 0 ;
{ @abstract(Milliseconds to wait between the RETRY_COUNT times. }
RETRY_DELAY = 2 0 0 ;
{ @abstract(How many times do we have to retry if the file is locked?. }
RETRY_COUNT = 5 ;
2023-12-19 21:48:50 +01:00
constructor Create(
aMaxBackupFileCount: Integer = TLoggerProFileAppenderBase. DEFAULT_MAX_BACKUP_FILE_COUNT;
aMaxFileSizeInKiloByte: Integer = TLoggerProFileAppenderBase. DEFAULT_MAX_FILE_SIZE_KB;
aLogsFolder: string = '' ;
aLogFileNameFormat: string = TLoggerProFileAppenderBase. DEFAULT_FILENAME_FORMAT;
2024-01-23 17:20:36 +01:00
aLogItemRenderer: ILogItemRenderer = nil ;
2023-12-19 21:48:50 +01:00
aEncoding: TEncoding = nil ) ;
reintroduce ; virtual ;
2023-02-27 12:26:25 +01:00
procedure Setup; override ;
end ;
{ @ abstract( The default file appender)
This file appender separates TLogItems with different tags into a log file for each tag.
To learn how to use this appender, check the sample @ code( file_appender. dproj)
}
TLoggerProFileAppender = class( TLoggerProFileAppenderBase)
private
fWritersDictionary: TObjectDictionary< string , TStreamWriter> ;
procedure AddWriter( const aLogTag: string ; var aWriter: TStreamWriter; var aLogFileName: string ) ;
procedure RotateLog( const aLogTag: string ; aWriter: TStreamWriter) ;
public
procedure Setup; override ;
procedure TearDown; override ;
procedure WriteLog( const aLogItem: TLogItem) ; overload ; override ;
end ;
2023-12-19 21:48:50 +01:00
{ @ abstract( File appender with multiple tags)
2023-02-27 12:26:25 +01:00
This file appender writes all TLogItems into a single log file .
Combined with a @ code( TLoggerProAppenderFilterImpl) you can filter out any log tags you like.
If you want to run several TLoggerProSimpleFileAppender in parallel you have to provide a different
LogFileFormat for each of them in the constructor in order to prevent name collisions.
To learn how to use this appender, check the sample @ code( file_appender. dproj)
}
TLoggerProSimpleFileAppender = class( TLoggerProFileAppenderBase)
private
fFileWriter: TStreamWriter;
procedure RotateLog;
2023-12-19 21:48:50 +01:00
protected
procedure CheckLogFileNameFormat( const LogFileNameFormat: String ) ; override ;
2023-02-27 12:26:25 +01:00
public
2023-12-19 21:48:50 +01:00
const
DEFAULT_FILENAME_FORMAT = '{module}.{number}.log' ;
2023-02-27 12:26:25 +01:00
procedure Setup; override ;
procedure TearDown; override ;
procedure WriteLog( const aLogItem: TLogItem) ; overload ; override ;
2023-12-19 21:48:50 +01:00
constructor Create(
aMaxBackupFileCount: Integer = TLoggerProFileAppenderBase. DEFAULT_MAX_BACKUP_FILE_COUNT;
aMaxFileSizeInKiloByte: Integer = TLoggerProFileAppenderBase. DEFAULT_MAX_FILE_SIZE_KB;
aLogsFolder: string = '' ;
aLogFileNameFormat: string = TLoggerProSimpleFileAppender. DEFAULT_FILENAME_FORMAT;
2024-01-23 17:20:36 +01:00
aLogItemRenderer: ILogItemRenderer = nil ;
2023-12-19 21:48:50 +01:00
aEncoding: TEncoding = nil ) ;
override ;
2023-02-27 12:26:25 +01:00
end ;
2023-12-19 21:48:50 +01:00
2023-02-27 12:26:25 +01:00
implementation
uses
System. IOUtils,
System. StrUtils,
System. Math,
2024-02-06 23:42:27 +01:00
System. DateUtils,
2023-02-27 12:26:25 +01:00
idGlobal
2023-12-19 21:48:50 +01:00
{$IF Defined(Android), System.SysUtils}
2023-02-27 12:26:25 +01:00
, Androidapi. Helpers
, Androidapi. JNI. GraphicsContentViewText
, Androidapi. JNI. JavaTypes
{$ENDIF}
;
2024-01-02 16:30:52 +01:00
2023-10-18 16:25:23 +02:00
function OccurrencesOfChar( const S: string ; const C: char ) : integer ;
var
i: Integer ;
begin
result : = 0 ;
for i : = 1 to Length( S) do
if S[ i] = C then
inc( result ) ;
end ;
2023-12-19 21:48:50 +01:00
procedure TLoggerProFileAppenderBase. CheckLogFileNameFormat( const LogFileNameFormat: String ) ;
2023-10-18 16:25:23 +02:00
begin
//DEFAULT_FILENAME_FORMAT = '{module}.{number}.{tag}.log';
2023-12-19 21:48:50 +01:00
if not ( LogFileNameFormat. Contains( '{number}' ) and LogFileNameFormat. Contains( '{tag}' ) ) then
2023-10-18 16:25:23 +02:00
begin
2023-12-19 21:48:50 +01:00
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
] ) ;
2023-10-18 16:25:23 +02:00
end ;
end ;
2023-12-19 21:48:50 +01:00
2023-02-27 12:26:25 +01:00
{ TLoggerProFileAppenderBase }
function TLoggerProFileAppenderBase. GetLogFileName( const aTag: string ; const aFileNumber: Integer ) : string ;
var
2023-12-19 21:48:50 +01:00
// lExt: string;
2023-02-27 12:26:25 +01:00
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;
2023-12-19 21:48:50 +01:00
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) ;
2023-02-27 12:26:25 +01:00
end ;
procedure TLoggerProFileAppenderBase. Setup;
begin
inherited ;
if fLogsFolder = '' then
begin
{$IF (Defined(MSWINDOWS) or Defined(POSIX)) and (not Defined(MOBILE))}
fLogsFolder : = TPath. GetDirectoryName( GetModuleName( HInstance) ) ;
{$ENDIF}
{$IF Defined(Android) or Defined(IOS)}
fLogsFolder : = TPath. GetSharedDocumentsPath( ) ;
{$ENDIF}
end ;
if not TDirectory. Exists( fLogsFolder) then
TDirectory. CreateDirectory( fLogsFolder) ;
end ;
procedure TLoggerProFileAppenderBase. WriteToStream( const aStreamWriter: TStreamWriter; const aValue: string ) ;
begin
aStreamWriter. WriteLine( aValue) ;
aStreamWriter. Flush;
end ;
procedure TLoggerProFileAppenderBase. InternalWriteLog( const aStreamWriter: TStreamWriter; const aLogItem: TLogItem) ;
begin
WriteToStream( aStreamWriter, FormatLog( aLogItem) ) ;
end ;
2024-02-06 23:42:27 +01:00
procedure TLoggerProFileAppenderBase. RetryDelete( const aFileSrc: string ) ;
var
lRetries: Integer ;
const
MAX_RETRIES = 5 ;
begin
lRetries : = 0 ;
repeat
try
Sleep( 5 0 ) ;
// 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( 1 0 0 ) ;
end ;
end ;
until lRetries = MAX_RETRIES;
if lRetries = MAX_RETRIES then
raise ELoggerPro. CreateFmt( 'Cannot delete file %s' , [ aFileSrc] ) ;
end ;
2023-02-27 12:26:25 +01:00
procedure TLoggerProFileAppenderBase. RetryMove( const aFileSrc, aFileDest: string ) ;
var
lRetries: Integer ;
const
MAX_RETRIES = 5 ;
begin
lRetries : = 0 ;
repeat
try
Sleep( 5 0 ) ;
// the incidence of "Locked file goes to nearly zero..."
TFile. Move( aFileSrc, aFileDest) ;
Break;
except
on E: EInOutError do
begin
Inc( lRetries) ;
2024-02-06 23:42:27 +01:00
Sleep( 1 0 0 ) ;
2023-02-27 12:26:25 +01:00
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
2023-12-19 21:48:50 +01:00
lRenamedFile : = GetLogFileName( aLogTag, fMaxBackupFileCount - 1 ) ;
2023-02-27 12:26:25 +01:00
if TFile. Exists( lRenamedFile) then
2024-02-06 23:42:27 +01:00
begin
2023-02-27 12:26:25 +01:00
TFile. Delete( lRenamedFile) ;
2024-02-06 23:42:27 +01:00
if TFile. Exists( lRenamedFile) then // double check for slow file systems
begin
RetryDelete( lRenamedFile) ;
end ;
end ;
2023-02-27 12:26:25 +01:00
// 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
2024-02-06 23:42:27 +01:00
begin
2023-02-27 12:26:25 +01:00
RetryMove( lCurrentFileName, lRenamedFile) ;
2024-02-06 23:42:27 +01:00
end ;
2023-02-27 12:26:25 +01:00
end ;
lRenamedFile : = GetLogFileName( aLogTag, 1 ) ;
RetryMove( aNewFileName, lRenamedFile) ;
end ;
2024-01-02 16:30:52 +01:00
constructor TLoggerProFileAppenderBase. Create(
aMaxBackupFileCount: Integer ;
aMaxFileSizeInKiloByte: Integer ;
aLogsFolder: string ;
aLogFileNameFormat: string ;
2024-01-23 17:20:36 +01:00
aLogItemRenderer: ILogItemRenderer;
2024-01-02 16:30:52 +01:00
aEncoding: TEncoding) ;
2023-02-27 12:26:25 +01:00
begin
2024-01-23 17:20:36 +01:00
inherited Create( aLogItemRenderer) ;
2023-10-18 16:25:23 +02:00
fLogsFolder : = aLogsFolder;
fMaxBackupFileCount: = Max( 1 , aMaxBackupFileCount) ;
2023-02-27 12:26:25 +01:00
fMaxFileSizeInKiloByte : = aMaxFileSizeInKiloByte;
2023-12-19 21:48:50 +01:00
CheckLogFileNameFormat( aLogFileNameFormat) ;
fLogFileNameFormat : = aLogFileNameFormat;
2023-02-27 12:26:25 +01:00
if Assigned( aEncoding) then
fEncoding : = aEncoding
else
fEncoding : = TEncoding. DEFAULT ;
end ;
2024-05-29 14:26:01 +02:00
function TLoggerProFileAppenderBase. CreateWriter( const aFileName: string ; const aBufferSize: Integer = 3 2 ) : TStreamWriter;
2023-02-27 12:26:25 +01:00
var
lFileStream: TFileStream;
lFileAccessMode: Word ;
lRetries: Integer ;
begin
lFileAccessMode : = fmOpenWrite or fmShareDenyNone;
if not TFile. Exists( aFileName) then
lFileAccessMode : = lFileAccessMode or fmCreate;
// If the file si 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.
lRetries : = 0 ;
while true do
begin
try
lFileStream : = TFileStream. Create( aFileName, lFileAccessMode) ;
try
lFileStream. Seek( 0 , TSeekOrigin. soEnd) ;
2024-05-29 14:26:01 +02:00
Result : = TStreamWriter. Create( lFileStream, fEncoding, aBufferSize) ;
2023-02-27 12:26:25 +01:00
Result . AutoFlush : = true ;
Result . OwnStream;
Break;
except
lFileStream. Free;
raise ;
end ;
except
if lRetries = RETRY_COUNT then
begin
raise ;
end
else
begin
Inc( lRetries) ;
Sleep( RETRY_DELAY) ; // just wait a little bit
end ;
end ;
end ;
end ;
{ TLoggerProFileAppender }
procedure TLoggerProFileAppender. AddWriter( const aLogTag: string ; var aWriter: TStreamWriter; var aLogFileName: string ) ;
begin
aLogFileName : = GetLogFileName( aLogTag, 0 ) ;
aWriter : = CreateWriter( aLogFileName) ;
fWritersDictionary. Add( aLogTag, aWriter) ;
end ;
2023-12-19 21:48:50 +01:00
procedure TLoggerProFileAppenderBase. EmitEndRotateLogItem( aWriter: TStreamWriter) ;
begin
WriteToStream( aWriter, '#[ROTATE LOG ' + datetimetostr( Now, FormatSettings) + ']' ) ;
end ;
procedure TLoggerProFileAppenderBase. EmitStartRotateLogItem( aWriter: TStreamWriter) ;
begin
WriteToStream( aWriter, '#[START LOG ' + datetimetostr( Now, FormatSettings) + ']' ) ;
end ;
2023-02-27 12:26:25 +01:00
procedure TLoggerProFileAppender. RotateLog( const aLogTag: string ; aWriter: TStreamWriter) ;
var
lLogFileName: string ;
begin
2023-12-19 21:48:50 +01:00
EmitEndRotateLogItem( aWriter) ;
//WriteToStream(aWriter, '#[ROTATE LOG ' + datetimetostr(Now, FormatSettings) + ']');
2023-02-27 12:26:25 +01:00
// remove the writer during rename
fWritersDictionary. Remove( aLogTag) ;
RotateFile( aLogTag, lLogFileName) ;
// re-create the writer
AddWriter( aLogTag, aWriter, lLogFileName) ;
2023-12-19 21:48:50 +01:00
EmitStartRotateLogItem( aWriter) ;
//WriteToStream(aWriter, '#[START LOG ' + datetimetostr(Now, FormatSettings) + ']');
2023-02-27 12:26:25 +01:00
end ;
procedure TLoggerProFileAppender. Setup;
begin
inherited ;
fWritersDictionary : = TObjectDictionary< string , TStreamWriter> . Create( [ doOwnsValues] ) ;
end ;
procedure TLoggerProFileAppender. TearDown;
begin
fWritersDictionary. Free;
inherited ;
end ;
procedure TLoggerProFileAppender. WriteLog( const aLogItem: TLogItem) ;
var
lWriter: TStreamWriter;
lLogFileName: string ;
begin
if not fWritersDictionary. TryGetValue( aLogItem. LogTag, lWriter) then
begin
AddWriter( aLogItem. LogTag, lWriter, lLogFileName) ;
end ;
InternalWriteLog( lWriter, aLogItem) ;
if lWriter. BaseStream. Size > fMaxFileSizeInKiloByte * 1 0 2 4 then
begin
RotateLog( aLogItem. LogTag, lWriter) ;
end ;
end ;
{ TLoggerProSimpleFileAppender }
2023-12-19 21:48:50 +01:00
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 ;
2024-01-23 17:20:36 +01:00
aLogsFolder: string ; aLogFileNameFormat: String ;
aLogItemRenderer: ILogItemRenderer;
2023-12-19 21:48:50 +01:00
aEncoding: TEncoding) ;
begin
inherited Create(
aMaxBackupFileCount,
aMaxFileSizeInKiloByte,
aLogsFolder,
aLogFileNameFormat,
2024-01-23 17:20:36 +01:00
aLogItemRenderer,
2023-12-19 21:48:50 +01:00
aEncoding) ;
end ;
2023-02-27 12:26:25 +01:00
procedure TLoggerProSimpleFileAppender. RotateLog;
var
lLogFileName: string ;
begin
2023-12-19 21:48:50 +01:00
EmitEndRotateLogItem( fFileWriter) ;
2023-02-27 12:26:25 +01:00
// remove the writer during rename
fFileWriter. Free;
2023-12-19 21:48:50 +01:00
RotateFile( '' , lLogFileName) ;
2023-02-27 12:26:25 +01:00
// re-create the writer
2023-12-19 21:48:50 +01:00
fFileWriter : = CreateWriter( GetLogFileName( '' , 0 ) ) ;
EmitStartRotateLogItem( fFileWriter) ;
2023-02-27 12:26:25 +01:00
end ;
procedure TLoggerProSimpleFileAppender. Setup;
begin
inherited ;
2023-12-19 21:48:50 +01:00
fFileWriter : = CreateWriter( GetLogFileName( '' , 0 ) ) ;
2023-02-27 12:26:25 +01:00
end ;
procedure TLoggerProSimpleFileAppender. TearDown;
begin
fFileWriter. Free;
inherited ;
end ;
procedure TLoggerProSimpleFileAppender. WriteLog( const aLogItem: TLogItem) ;
begin
InternalWriteLog( fFileWriter, aLogItem) ;
if fFileWriter. BaseStream. Size > fMaxFileSizeInKiloByte * 1 0 2 4 then
begin
RotateLog;
end ;
end ;
end .