{*****************************************************************************} { } { llLogUtils.pas - TThreadSafeLogger and supporting types/constants } { } { (c)2013-2022 Illuminated Logic, LLC / Ray Marron (see license.txt) } { } {*****************************************************************************} unit llLogUtils; interface uses Windows, SysUtils, Classes, SyncObjs, llDates, llFiles; type TLoggingLevel = (llNone, llSystem, llErrors, llWarnings, llInfo, llVerbose, llDebug, llTrace); const LoggingLevelNames: array[TLoggingLevel] of string = ('None', 'System', 'Errors', 'Warnings', 'Info', 'Verbose', 'Debug', 'Trace'); LoggingLevelLetters = 'NSEWIVDT'; // LoggingLevelLetters[Ord(Level) + 1] DefaultLoggingLevel = llInfo; DefaultLLL = 'I'; // Default LoggingLevelLetter DefaultLogDays = 0; DefaultLogEOL = #13#10; DefaultLogTimeFormat = ANSI_DATETIME; DefaultNormalFormat = '%s %s'; DefaultLevelsFormat = '%s [%s] %s'; DefaultShowLevels = False; DefaultWriterInterval = 1000; DefaultWriterSleep = 500; MinimumWriterSleep = 100; LogDateToken = '%'; type {+ /trim=2 TThreadSafeLogger} TThreadSafeLogger = class(TObject) {-} private (* 25 lines omitted *) public constructor Create; destructor Destroy; override; property LogFile: string read FOrigFile write SetLogFile; property CurrentFilename: string read FLogFile; property LogDays: Integer read FLogDays write FLogDays; property LoggingLevel: TLoggingLevel read FLogLevel write SetLogLevel; property TimeFormat: string read FTimeFormat write FTimeFormat; property EOL: string read FEOL write FEOL; property ConvertEOL: Boolean read FConvertEOL write FConvertEOL; property ShowLevels: Boolean read FShowLevels write FShowLevels; property NormalFormat: string read FNormalFormat write FNormalFormat; property LevelsFormat: string read FLevelsFormat write FLevelsFormat; property Encoding: TEncoding read FEncoding write FEncoding; property Dirty: Boolean read GetDirty; procedure DoLogCleanup(const AWildcard: string); function IsValidLogFile: Boolean; procedure Log(const S: string; const ALevel: TLoggingLevel); procedure LogFmt(const S: string; const Args: array of const; const ALevel: TLoggingLevel); procedure WriteEntries; end; {+ /trim=OFF /addline=OFF Facilitates logging in a multi-threaded application where the threads share a common log file. Works equally as well in a single thread/process. Create a single instance of this object, set the properties, and give each thread a reference to it. Those threads can use the Log and LogFmt methods to add log entries. Just one thread should be responsible for periodically calling WriteEntries. Use TLogWriterThread if you don't already have a utility thread available. ----- Properties (default value): LogFile The fully qualified filename of the log file to write to. The log filename can include a date-substitution format string surrounded by percent signs, e.g. MyApp%yyyymmdd%.log. The value betweeen the percent signs must be a FormatDateTimeEx-compatible format string. The filename will be updated with the current date upon assignment, and a date change will be checked for during each call to WriteEntries. The substitution is assumed to change no more frequently than daily, but could be longer (e.g. weekly). CurrentFilename The current name of the log file after date substitution. If there is no substitution, this will be the same as LogFile. Read-only. LogDays (0) When using a dated log file, LogDays can be set to a value greater than zero to automatically delete any log files older than that number of days. The cleanup is performed whenever the dated log filename is updated. Set LogDays before LogFile if you want cleanup to occur immediately after creation, otherwise you will have to wait for the date to change. LoggingLevel (llInfo) The LoggingLevel determines which log entries actually make it into the log. Entries made with a logging level <= the object's LoggingLevel will be saved. For example, if LoggingLevel is llInfo, and Log('foo', llDebug) is called, "foo" will NOT appear in the log because llDebug > llInfo. TimeFormat ('yyyy-mm-dd hh:nn:ss') TimeFormat is a FormatDateTimeEx-compatible format string. All non-blank log entries will start with a timestamp in the given format. When using a dated log file that changes daily, the suggested TimeFormat is "hh:nn:ss" since the date is implicit for all entries in the same file. EOL (#13#10) The EOL property can be used to change the end of line character(s) used when writing lines to the log. The default is CRLF. ConvertEOL (False) If set to True, log entries will have any internal end-of-line characters converted to match the EOL property. When False (the default), log entries are accepted as-is. ShowLevels (False) When ShowLevels is True, each log entry includes a one-letter indicator of the logging level it was made with. Setting LoggingLevel to llDebug automatically turns this on. Examples: 18:52:23 [I] Here is some handy information 18:52:34 [E] File not found: foo.txt NormalFormat ('%s %s') LevelsFormat ('%s [%s] %s') These are format strings (Delphi's Format function) that determine how the log entries are formatted when written, depending on the ShowLevels setting. When ShowLevels is False, NormalFormat is used and is given the timestamp and log entry. When ShowLevels is True, LevelsFormat is given the timestamp, level letter, and log entry, in that order. You can make them appear in a different order using format indexes (e.g. "%0:s %2:s [%1:s]"). Encoding (UTF8) Determines how the log file is written to disk (Unicode compilers only). In non-Unicode compilers, the file is written as plain ASCII text. Dirty Indicates if there are any log entries waiting to be written. Read-only. ----- Methods Log, LogFmt These add entries to the log file. If the supplied logging level is greater than the object's LoggingLevel property, the entry will not be written. Calling Log with an empty string writes a blank line (no timestamp) to the log file for formatting purposes. IsValidLogFile Returns a boolean indicating whether the CurrentFilename can actually be written to; the path and filename are valid, and nothing is preventing the opening/creation of the file. Typically called just once immediately prior to use. DoLogCleanup Call this to manually perform the dated log file cleanup that happens when the date changes and LogDays > 0. Supply a blank wildcard to use the default of replacing the date substitution string with an asterisk, e.g. D:\MyApp%yyyymmdd%.log -> D:\MyApp*.log. WriteEntries This procedure writes the collected log entries to the log file. Only one thread should be responsible for calling WriteEntries. If LogFile includes date substitution, it also checks if the system date has changed to update CurrentFilename and call DoLogCleanup. ----- Types, Constants, and Utility Functions TLoggingLevel = (llNone, llSystem, llErrors, llWarnings, llInfo, llVerbose, llDebug, llTrace); LoggingLevelNames: array[TLoggingLevel] of string = ('None', 'System', 'Errors', 'Warnings', 'Info', 'Verbose', 'Debug', 'Trace'); LoggingLevelLetters = 'NSEWIVDT'; // LoggingLevelLetters[Ord(Level) + 1] DefaultLoggingLevel = llInfo; DefaultLLL = 'I'; // Default LoggingLevelLetter DefaultLogEOL = #13#10; DefaultLogTimeFormat = ANSI_DATETIME; DefaultNormalFormat = '%s %s'; DefaultLevelsFormat = '%s [%s] %s'; DefaultWriterInterval = 1000; DefaultWriterSleep = 500; MinimumWriterSleep = 100; LogDateToken = '%'; ItoLL() Integer to TLoggingLevel (The integer is Ord(TLoggingLevel)) ItoLLL() Integer to LoggingLevelLetter LLtoC() TLoggingLevel to Char (LoggingLevelLetter) StoLL() String to TLoggingLevel (Input can be a name or a letter) PopulateLevelList() fills a list (like TComboBox.Items) with level names. When storing a logging level in an ini/registry entry, it is recommended that the LoggingLevelLetter is written rather than the integer Ord(TLoggingLevel). In addition to being a user-friendly mnemonic, it makes the setting future- proof against any new levels that may be added. See also: TLogWriterThread -} {+ /trim=2 TLogWriterThread} TLogWriterThread = class(TThread) {-} private (* 3 lines omitted *) protected procedure Execute; override; {+ /trim=2 /addline=OFF TLogWriterThread} public constructor Create(CreateSuspended: Boolean); property Logger: TThreadSafeLogger read FLogger write FLogger; property IntervalMS: Integer read FIntervalMS write FIntervalMS; property SleepMS: Integer read FSleepMS write FSleepMS; end; {+ /trim=OFF /addline=OFF TLogWriterThread is a simple utility thread for use with TThreadSafeLogger. It calls WriteEntries once per second by default, but IntervalMS can be changed if desired. See also: TThreadSafeLogger -} function ItoLL(const i: Integer; const ADefault: TLoggingLevel = DefaultLoggingLevel): TLoggingLevel; function ItoLLL(const i: Integer; const ADefault: TLoggingLevel = DefaultLoggingLevel): Char; function LLtoC(const LL: TLoggingLevel): Char; function StoLL(const S: string; const ADefault: TLoggingLevel = DefaultLoggingLevel): TLoggingLevel; procedure PopulateLevelList(const AList: TStrings; const ADefault: TLoggingLevel = DefaultLoggingLevel; const AMarkDefault: string = ''; const AAltDefaultName: string = ''); implementation (* 372 lines omitted *) end.