unit LoggerPro; { <@abstract(Contains the LoggerPro core. Include this if you want to create your own logger, otherwise you can use the global one using @link(LoggerPro.GlobalLogger.pas)) @author(Daniele Teti) } {$SCOPEDENUMS ON} interface uses System.Generics.Collections, System.SysUtils, System.Classes, ThreadSafeQueueU; var DefaultLoggerProMainQueueSize: Cardinal = 50000; DefaultLoggerProAppenderQueueSize: Cardinal = 50000; type TLogType = (Debug = 0, Info, Warning, Error); TLogErrorReason = (QueueFull); TLogErrorAction = (SkipNewest, DiscardOlder); TLogExtendedInfo = (EIUserName, EIComputerName, EIProcessName, EIProcessID, EIDeviceID { mobile } ); TLoggerProExtendedInfo = set of TLogExtendedInfo; { @abstract(Represent the single log item) Each call to some kind of log method is wrapped in a @link(TLogItem) instance and passed down the layour of LoggerPro. } TLogItem = class sealed private FType: TLogType; FMessage: string; FTag: string; FTimeStamp: TDateTime; FThreadID: TThreadID; function GetLogTypeAsString: string; public constructor Create(const aType: TLogType; const aMessage: string; const aTag: string); overload; constructor Create(const aType: TLogType; const aMessage: string; const aTag: string; const aTimeStamp: TDateTime; const aThreadID: TThreadID); overload; function Clone: TLogItem; { @abstract(The type of the log) Log can be one of the following types: @unorderedlist( @item(DEBUG) @item(INFO) @item(WARNING) @item(ERROR) ) } property LogType: TLogType read FType; { @abstract(The text of the log message) } property LogMessage: string read FMessage; { @abstract(The tag of the log message) } property LogTag: string read FTag; { @abstract(The timestamp when the @link(TLogItem) is generated) } property TimeStamp: TDateTime read FTimeStamp; { @abstract(The IDof the thread which generated the log item) } property ThreadID: TThreadID read FThreadID; { @abstract(The type of the log converted in string) } property LogTypeAsString: string read GetLogTypeAsString; end; TLoggerProAppenderErrorEvent = reference to procedure(const AppenderClassName: string; const aFailedLogItem: TLogItem; const Reason: TLogErrorReason; var Action: TLogErrorAction); TLoggerProEventsHandler = class sealed public OnAppenderError: TLoggerProAppenderErrorEvent; end; { @abstract(Interface implemented by all the classes used as appenders) } ILogAppender = interface ['{58AFB557-C594-4A4B-8DC9-0F13B37F60CB}'] { @abstract(This method is internally called by LoggerPro to initialize the appender) } procedure Setup; { @abstract(This method is called at each log item represented by @link(TLogItem)) The appender should be as-fast-as-it-can to handle the message, however each appender runs in a separated thread. } procedure WriteLog(const aLogItem: TLogItem); { @abstract(This method is internally called by LoggerPro to deinitialize the appender) } procedure TearDown; // { @abstract(Enable or disable the log appender. Is used internally by LoggerPro but must be // implemented by each logappender. A simple @code(if enabled then dolog) is enough } // procedure SetEnabled(const Value: Boolean); // { @abstract(Returns if the logappender is currently enabled or not. } // function IsEnabled: Boolean; { @abstract(Set a custom log level for this appender. This value must be lower than the global LogWriter log level. } procedure SetLogLevel(const Value: TLogType); { @abstract(Get the loglevel for the appender. } function GetLogLevel: TLogType; { @abstract(If the appender is disabled, this method is called at each new logitem. This method should not raise exceptions and should try to restart the appender at specified time and only if some appropriate seconds/miutes are elapsed between the LastErrorTimestamp. } procedure TryToRestart(var Restarted: Boolean); procedure SetLastErrorTimeStamp(const LastErrorTimeStamp: TDateTime); function GetLastErrorTimeStamp: TDateTime; property LastErrorTimeStamp: TDateTime read GetLastErrorTimeStamp write SetLastErrorTimeStamp; end; ELoggerPro = class(Exception) end; TAppenderQueue = class(TThreadSafeQueue) end; ILogWriter = interface ['{A717A040-4493-458F-91B2-6F6E2AFB496F}'] procedure Debug(const aMessage: string; const aTag: string); overload; procedure Debug(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure DebugFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); deprecated; procedure Info(const aMessage: string; const aTag: string); overload; procedure Info(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure InfoFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); deprecated; procedure Warn(const aMessage: string; const aTag: string); overload; procedure Warn(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure WarnFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); deprecated; procedure Error(const aMessage: string; const aTag: string); overload; procedure Error(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure ErrorFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); deprecated; procedure Log(const aType: TLogType; const aMessage: string; const aTag: string); overload; procedure Log(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); overload; procedure LogFmt(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); deprecated; function GetAppendersClassNames: TArray; function GetAppenders(const Index: Integer): ILogAppender; property Appenders[const index: Integer]: ILogAppender read GetAppenders; function AppendersCount(): Integer; end; TLogAppenderList = TList; TAppenderThread = class(TThread) private FLogAppender: ILogAppender; FAppenderQueue: TAppenderQueue; FFailing: Boolean; procedure SetFailing(const Value: Boolean); protected procedure Execute; override; type TAppenderStatus = (BeforeSetup, Running, WaitAfterFail, ToRestart, BeforeTearDown); public constructor Create(aLogAppender: ILogAppender; aAppenderQueue: TAppenderQueue); property Failing: Boolean read FFailing write SetFailing; end; TLoggerThread = class(TThread) private type TAppenderAdapter = class private FAppenderQueue: TAppenderQueue; FAppenderThread: TAppenderThread; FLogAppender: ILogAppender; FFailsCount: Cardinal; public constructor Create(aAppender: ILogAppender); virtual; destructor Destroy; override; function EnqueueLog(const aLogItem: TLogItem): Boolean; property Queue: TAppenderQueue read FAppenderQueue; property FailsCount: Cardinal read FFailsCount; function GetLogLevel: TLogType; end; TAppenderAdaptersList = class(TObjectList) public constructor Create; end; private FQueue: TThreadSafeQueue; FAppenders: TLogAppenderList; FEventsHandlers: TLoggerProEventsHandler; function BuildAppendersDecorator: TAppenderAdaptersList; procedure DoOnAppenderError(const FailAppenderClassName: string; const aFailedLogItem: TLogItem; const aReason: TLogErrorReason; var aAction: TLogErrorAction); procedure SetEventsHandlers(const Value: TLoggerProEventsHandler); protected procedure Execute; override; public constructor Create(aAppenders: TLogAppenderList); destructor Destroy; override; property EventsHandlers: TLoggerProEventsHandler read FEventsHandlers write SetEventsHandlers; property LogWriterQueue: TThreadSafeQueue read FQueue; end; TLogWriter = class(TInterfacedObject, ILogWriter) private FLoggerThread: TLoggerThread; FLogAppenders: TLogAppenderList; FFreeAllowed: Boolean; FLogLevel: TLogType; procedure Initialize(aEventsHandler: TLoggerProEventsHandler); function GetAppendersClassNames: TArray; public function GetAppenders(const Index: Integer): ILogAppender; function AppendersCount(): Integer; constructor Create(aLogLevel: TLogType = TLogType.Debug); overload; constructor Create(aLogAppenders: TLogAppenderList; aLogLevel: TLogType = TLogType.Debug); overload; destructor Destroy; override; procedure Debug(const aMessage: string; const aTag: string); overload; procedure Debug(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure DebugFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); procedure Info(const aMessage: string; const aTag: string); overload; procedure Info(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure InfoFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); procedure Warn(const aMessage: string; const aTag: string); overload; procedure Warn(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure WarnFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); procedure Error(const aMessage: string; const aTag: string); overload; procedure Error(const aMessage: string; const aParams: array of TVarRec; const aTag: string); overload; procedure ErrorFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); procedure Log(const aType: TLogType; const aMessage: string; const aTag: string); overload; procedure Log(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); overload; procedure LogFmt(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); end; TLoggerProAppenderBase = class abstract(TInterfacedObject, ILogAppender) private FLogLevel: TLogType; FEnabled: Boolean; FLastErrorTimeStamp: TDateTime; public constructor Create; virtual; procedure Setup; virtual; abstract; procedure WriteLog(const aLogItem: TLogItem); virtual; abstract; procedure TearDown; virtual; abstract; procedure TryToRestart(var Restarted: Boolean); virtual; procedure SetLogLevel(const Value: TLogType); function GetLogLevel: TLogType; inline; procedure SetLastErrorTimeStamp(const Value: TDateTime); function GetLastErrorTimeStamp: TDateTime; property LogLevel: TLogType read GetLogLevel write SetLogLevel; end; { @abstract(Builds a new ILogWriter instance. Call this global function to start logging like a pro.) Here's a sample unit that you can use in your code @longcode(# unit LoggerProConfig; interface uses LoggerPro; function Log: ILogWriter; implementation uses LoggerPro.FileAppender; var _Log: ILogWriter; function Log: ILogWriter; begin Result := _Log; end; initialization //If you need other appenders, feel free to add them here in the array _Log := BuildLogWriter([TLoggerProFileAppender.Create(10, 5)]); end. #) Add this unit to your project, then when you need to use the logger, include the unit and call one of the followings: @unorderedlist( @item(Log.Debug('This is a debug message', 'tag1')) @item(Log.Info('This is an information message', 'tag1')) @item(Log.Warn('This is a warning message', 'tag1')) @item(Log.Error('This is an error message', 'tag1')) ) } function GetDefaultFormatSettings: TFormatSettings; function StringToLogType(const aLogType: string): TLogType; function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler = nil; aLogLevel: TLogType = TLogType.Debug): ILogWriter; implementation uses System.Types, LoggerPro.FileAppender, System.SyncObjs, System.DateUtils, System.IOUtils; function GetDefaultFormatSettings: TFormatSettings; begin Result.DateSeparator := '-'; Result.TimeSeparator := ':'; Result.ShortDateFormat := 'YYYY-MM-DD HH:NN:SS:ZZZ'; Result.ShortTimeFormat := 'HH:NN:SS'; end; function StringToLogType(const aLogType: string): TLogType; var lLogType: string; begin lLogType := LowerCase(Trim(aLogType)); if lLogType = 'debug' then Exit(TLogType.Debug); if lLogType = 'info' then Exit(TLogType.Info); if lLogType = 'warning' then Exit(TLogType.Warning); if lLogType = 'error' then Exit(TLogType.Error); raise ELoggerPro.CreateFmt('Invalid LogType: ', [aLogType]); end; function BuildLogWriter(aAppenders: array of ILogAppender; aEventsHandlers: TLoggerProEventsHandler; aLogLevel: TLogType): ILogWriter; var lLogAppenders: TLogAppenderList; lLogAppender: ILogAppender; begin lLogAppenders := TLogAppenderList.Create; for lLogAppender in aAppenders do begin lLogAppenders.Add(lLogAppender); end; Result := TLogWriter.Create(lLogAppenders, aLogLevel); TLogWriter(Result).Initialize(aEventsHandlers); end; { TLogger.TLogWriter } function TLogWriter.AppendersCount: Integer; begin Result := Self.FLogAppenders.Count; end; constructor TLogWriter.Create(aLogAppenders: TLogAppenderList; aLogLevel: TLogType); begin inherited Create; FFreeAllowed := False; FLogAppenders := aLogAppenders; FLogLevel := aLogLevel; end; constructor TLogWriter.Create(aLogLevel: TLogType); begin Create(TLogAppenderList.Create, aLogLevel); end; procedure TLogWriter.Debug(const aMessage, aTag: string); begin Log(TLogType.Debug, aMessage, aTag); end; procedure TLogWriter.Debug(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Log(TLogType.Debug, aMessage, aParams, aTag); end; procedure TLogWriter.DebugFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Debug(aMessage, aParams, aTag); end; destructor TLogWriter.Destroy; begin FLoggerThread.Terminate; FLoggerThread.WaitFor; FLoggerThread.Free; FLogAppenders.Free; inherited; end; procedure TLogWriter.Error(const aMessage, aTag: string); begin Log(TLogType.Error, aMessage, aTag); end; procedure TLogWriter.Error(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Log(TLogType.Error, aMessage, aParams, aTag); end; procedure TLogWriter.ErrorFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Error(aMessage, aParams, aTag); end; function TLogWriter.GetAppenders(const Index: Integer): ILogAppender; begin Result := Self.FLogAppenders[index]; end; function TLogWriter.GetAppendersClassNames: TArray; var I: Cardinal; begin TMonitor.Enter(FLogAppenders); try SetLength(Result, FLogAppenders.Count); for I := 0 to FLogAppenders.Count - 1 do begin Result[I] := TObject(FLogAppenders[I]).ClassName; end; finally TMonitor.Exit(FLogAppenders); end; end; procedure TLogWriter.Info(const aMessage, aTag: string); begin Log(TLogType.Info, aMessage, aTag); end; procedure TLogWriter.Info(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Log(TLogType.Info, aMessage, aParams, aTag); end; procedure TLogWriter.InfoFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Info(aMessage, aParams, aTag); end; procedure TLogWriter.Log(const aType: TLogType; const aMessage, aTag: string); var lLogItem: TLogItem; begin if aType >= FLogLevel then begin lLogItem := TLogItem.Create(aType, aMessage, aTag); try if not FLoggerThread.LogWriterQueue.Enqueue(lLogItem) then begin raise ELoggerPro.Create ('Main logs queue is full. Hints: Are there appenders? Are these appenders fast enough considering the log item production?'); end; except FreeAndNil(lLogItem); raise; end; end; end; procedure TLogWriter.Log(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); begin Log(aType, Format(aMessage, aParams), aTag); end; procedure TLogWriter.LogFmt(const aType: TLogType; const aMessage: string; const aParams: array of const; const aTag: string); begin Log(aType, aMessage, aParams, aTag); end; procedure TLogWriter.Initialize(aEventsHandler: TLoggerProEventsHandler); begin FLoggerThread := TLoggerThread.Create(FLogAppenders); FLoggerThread.EventsHandlers := aEventsHandler; FLoggerThread.Start; end; procedure TLogWriter.Warn(const aMessage, aTag: string); begin Log(TLogType.Warning, aMessage, aTag); end; procedure TLogWriter.Warn(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Log(TLogType.Warning, aMessage, aParams, aTag); end; procedure TLogWriter.WarnFmt(const aMessage: string; const aParams: array of TVarRec; const aTag: string); begin Warn(aMessage, aParams, aTag); end; { TLogger.TLogItem } function TLogItem.Clone: TLogItem; begin Result := TLogItem.Create(FType, FMessage, FTag, FTimeStamp, FThreadID); end; constructor TLogItem.Create(const aType: TLogType; const aMessage, aTag: string); begin Create(aType, aMessage, aTag, now, TThread.CurrentThread.ThreadID); end; { TLogger.TLoggerThread } constructor TLoggerThread.Create(aAppenders: TLogAppenderList); begin FQueue := TThreadSafeQueue.Create(DefaultLoggerProMainQueueSize, 500); FAppenders := aAppenders; inherited Create(true); FreeOnTerminate := False; end; destructor TLoggerThread.Destroy; begin FQueue.Free; inherited; end; procedure TLoggerThread.DoOnAppenderError(const FailAppenderClassName: string; const aFailedLogItem: TLogItem; const aReason: TLogErrorReason; var aAction: TLogErrorAction); begin if Assigned(FEventsHandlers) and (Assigned(FEventsHandlers.OnAppenderError)) then begin FEventsHandlers.OnAppenderError(FailAppenderClassName, aFailedLogItem, aReason, aAction); end; end; procedure TLoggerThread.Execute; var lQSize: UInt64; lLogItem: TLogItem; I: Integer; lAppendersDecorators: TObjectList; lAction: TLogErrorAction; lWaitResult: TWaitResult; begin lAppendersDecorators := BuildAppendersDecorator; try while true do begin lWaitResult := FQueue.Dequeue(lQSize, lLogItem); case lWaitResult of wrSignaled: begin if lLogItem <> nil then begin try for I := 0 to lAppendersDecorators.Count - 1 do begin if lLogItem.LogType >= lAppendersDecorators[I].GetLogLevel then begin if not lAppendersDecorators[I].EnqueueLog(lLogItem) then begin lAction := TLogErrorAction.SkipNewest; // default DoOnAppenderError(TObject(lAppendersDecorators[I].FLogAppender).ClassName, lLogItem, TLogErrorReason.QueueFull, lAction); case lAction of TLogErrorAction.SkipNewest: begin // just skip the new message end; TLogErrorAction.DiscardOlder: begin // just remove the oldest log message lAppendersDecorators[I].Queue.Dequeue.Free; end; end; end; end; end; finally lLogItem.Free; end; end; end; wrTimeout, wrAbandoned, wrError: begin if Terminated then Break; end; wrIOCompletion: begin raise ELoggerPro.Create('Unhandled WaitResult: wrIOCompletition'); end; end; end; finally lAppendersDecorators.Free; end; end; procedure TLoggerThread.SetEventsHandlers(const Value: TLoggerProEventsHandler); begin FEventsHandlers := Value; end; function TLoggerThread.BuildAppendersDecorator: TAppenderAdaptersList; var I: Integer; begin Result := TAppenderAdaptersList.Create; try for I := 0 to FAppenders.Count - 1 do begin Result.Add(TAppenderAdapter.Create(FAppenders[I])); end; except Result.Free; raise; end; end; constructor TLogItem.Create(const aType: TLogType; const aMessage, aTag: string; const aTimeStamp: TDateTime; const aThreadID: TThreadID); begin inherited Create; FType := aType; FMessage := aMessage; FTag := aTag; FTimeStamp := aTimeStamp; FThreadID := aThreadID; end; function TLogItem.GetLogTypeAsString: string; begin case FType of TLogType.Debug: Exit('DEBUG'); TLogType.Info: Exit('INFO'); TLogType.Warning: Exit('WARNING'); TLogType.Error: Exit('ERROR'); else raise ELoggerPro.Create('Invalid LogType'); end; end; { TLoggerThread.TAppenderDecorator } constructor TLoggerThread.TAppenderAdapter.Create(aAppender: ILogAppender); begin inherited Create; FFailsCount := 0; FLogAppender := aAppender; FAppenderQueue := TAppenderQueue.Create(DefaultLoggerProAppenderQueueSize, 10); FAppenderThread := TAppenderThread.Create(FLogAppender, FAppenderQueue); end; destructor TLoggerThread.TAppenderAdapter.Destroy; begin FAppenderQueue.DoShutDown; FAppenderThread.Terminate; FAppenderThread.Free; FAppenderQueue.Free; inherited; end; function TLoggerThread.TAppenderAdapter.GetLogLevel: TLogType; begin Result := FLogAppender.GetLogLevel; end; function TLoggerThread.TAppenderAdapter.EnqueueLog(const aLogItem: TLogItem): Boolean; var lLogItem: TLogItem; begin lLogItem := aLogItem.Clone; Result := FAppenderQueue.Enqueue(lLogItem); // = TWaitResult.wrSignaled; if not Result then begin lLogItem.Free; FFailsCount := FFailsCount + 1 end else FFailsCount := 0; end; { TLoggerProAppenderBase } constructor TLoggerProAppenderBase.Create; begin inherited; Self.FEnabled := true; Self.FLogLevel := TLogType.Debug; end; function TLoggerProAppenderBase.GetLastErrorTimeStamp: TDateTime; begin Result := FLastErrorTimeStamp; end; function TLoggerProAppenderBase.GetLogLevel: TLogType; begin Result := FLogLevel; end; procedure TLoggerProAppenderBase.SetLastErrorTimeStamp(const Value: TDateTime); begin FLastErrorTimeStamp := Value; end; procedure TLoggerProAppenderBase.SetLogLevel(const Value: TLogType); begin FLogLevel := Value; end; procedure TLoggerProAppenderBase.TryToRestart(var Restarted: Boolean); begin Restarted := False; // do nothing "smart" here... descendant must implement specific "restart" strategies end; { TAppenderThread } constructor TAppenderThread.Create(aLogAppender: ILogAppender; aAppenderQueue: TAppenderQueue); begin FLogAppender := aLogAppender; FAppenderQueue := aAppenderQueue; inherited Create(False); end; procedure TAppenderThread.Execute; var lLogItem: TLogItem; lRestarted: Boolean; lStatus: TAppenderStatus; lSetupFailCount: Integer; begin lSetupFailCount := 0; lStatus := TAppenderStatus.BeforeSetup; try { the appender tries to log all the messages before terminate... } while (not Terminated) or (FAppenderQueue.QueueSize > 0) do begin { ...but if the thread should be terminated, and the appender is failing, its messages will be lost } if Terminated and (lStatus = TAppenderStatus.WaitAfterFail) then Break; try { this state machine handles the status of the appender } case lStatus of TAppenderStatus.BeforeTearDown: begin Break; end; TAppenderStatus.BeforeSetup: begin try FLogAppender.Setup; lStatus := TAppenderStatus.Running; except if lSetupFailCount = 10 then begin lStatus := TAppenderStatus.WaitAfterFail; end else begin Inc(lSetupFailCount); Sleep(1000); // wait before next setup call end; end; end; TAppenderStatus.ToRestart: begin try lRestarted := False; FLogAppender.TryToRestart(lRestarted); if lRestarted then begin lStatus := TAppenderStatus.Running; FLogAppender.LastErrorTimeStamp := 0; end else begin lRestarted := False; FLogAppender.LastErrorTimeStamp := now; lStatus := TAppenderStatus.WaitAfterFail; end; except lRestarted := False; end; Failing := not lRestarted; end; TAppenderStatus.WaitAfterFail: begin Sleep(500); if SecondsBetween(now, FLogAppender.LastErrorTimeStamp) >= 5 then lStatus := TAppenderStatus.ToRestart; end; TAppenderStatus.Running: begin if FAppenderQueue.Dequeue(lLogItem) = TWaitResult.wrSignaled then begin if lLogItem <> nil then begin try try FLogAppender.WriteLog(lLogItem); except Failing := true; FLogAppender.LastErrorTimeStamp := now; lStatus := TAppenderStatus.WaitAfterFail; Continue; end; finally lLogItem.Free; end; end; end; end; end; except // something wrong... but we cannot stop the thread. Let's retry. end; end; finally FLogAppender.TearDown; end; end; procedure TAppenderThread.SetFailing(const Value: Boolean); begin FFailing := Value; end; { TLoggerThread.TAppenderAdaptersList } constructor TLoggerThread.TAppenderAdaptersList.Create; begin inherited Create(true); end; end.