From 9571bb4f97f00f62e3ba4a16151b3ba50cad967c Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sat, 15 Oct 2016 11:22:51 +0200 Subject: [PATCH] Added Json text formatter for file output --- .gitignore | 1 + Packages/X2LogDXE2.dpk | 6 +- Packages/X2LogDXE2.dproj | 3 + Packages/X2LogDXE2Group.groupproj | 18 ++- Packages/X2LogJsonDXE2.dpk | 39 +++++++ Packages/X2LogJsonDXE2.dproj | 187 ++++++++++++++++++++++++++++++ Packages/X2LogJsonDXE2.res | Bin 0 -> 448 bytes Test/X2LogTest.dpr | 5 +- Test/X2LogTest.dproj | 3 + Test/source/MainFrm.dfm | 56 ++++++++- Test/source/MainFrm.pas | 32 ++++- X2Log.Constants.pas | 6 + X2Log.Details.Default.pas | 50 ++++++++ X2Log.Details.Intf.pas | 2 + X2Log.Observer.LogFile.pas | 141 ++++++++++++++-------- X2Log.Observer.MonitorForm.dfm | 2 +- X2Log.Observer.MonitorForm.pas | 15 +-- X2Log.Observer.RollingLogFile.pas | 29 ++--- X2Log.TextFormatter.Default.pas | 78 +++++++++++++ X2Log.TextFormatter.Intf.pas | 20 ++++ X2Log.TextFormatter.Json.pas | 127 ++++++++++++++++++++ 21 files changed, 732 insertions(+), 88 deletions(-) create mode 100644 Packages/X2LogJsonDXE2.dpk create mode 100644 Packages/X2LogJsonDXE2.dproj create mode 100644 Packages/X2LogJsonDXE2.res create mode 100644 X2Log.TextFormatter.Default.pas create mode 100644 X2Log.TextFormatter.Intf.pas create mode 100644 X2Log.TextFormatter.Json.pas diff --git a/.gitignore b/.gitignore index bfdd04b..54f2f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Test/bin/ *.tvsconfig *.local *.identcache +*.dcu diff --git a/Packages/X2LogDXE2.dpk b/Packages/X2LogDXE2.dpk index 7212a7e..541bb7a 100644 --- a/Packages/X2LogDXE2.dpk +++ b/Packages/X2LogDXE2.dpk @@ -52,7 +52,11 @@ contains X2Log.Util.Stream in '..\X2Log.Util.Stream.pas', X2Log.Translations.Dutch in '..\X2Log.Translations.Dutch.pas', X2Log.Decorator in '..\X2Log.Decorator.pas', - X2Log.Observer.RollingLogFile in '..\X2Log.Observer.RollingLogFile.pas'; + X2Log.Observer.RollingLogFile in '..\X2Log.Observer.RollingLogFile.pas', + X2Log.TextFormatter.Default in '..\X2Log.TextFormatter.Default.pas', + X2Log.TextFormatter.Intf in '..\X2Log.TextFormatter.Intf.pas', + X2Log.Intf.NamedPipe in '..\X2Log.Intf.NamedPipe.pas'; end. + diff --git a/Packages/X2LogDXE2.dproj b/Packages/X2LogDXE2.dproj index 8146f87..eb66afa 100644 --- a/Packages/X2LogDXE2.dproj +++ b/Packages/X2LogDXE2.dproj @@ -109,6 +109,9 @@ + + + Base diff --git a/Packages/X2LogDXE2Group.groupproj b/Packages/X2LogDXE2Group.groupproj index a68d75e..9c46d93 100644 --- a/Packages/X2LogDXE2Group.groupproj +++ b/Packages/X2LogDXE2Group.groupproj @@ -12,6 +12,9 @@ + + + Default.Personality.12 @@ -47,14 +50,23 @@ + + + + + + + + + - + - + - + diff --git a/Packages/X2LogJsonDXE2.dpk b/Packages/X2LogJsonDXE2.dpk new file mode 100644 index 0000000..ba6e72a --- /dev/null +++ b/Packages/X2LogJsonDXE2.dpk @@ -0,0 +1,39 @@ +package X2LogJsonDXE2; + +{$R *.res} +{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users} +{$ALIGN 8} +{$ASSERTIONS ON} +{$BOOLEVAL OFF} +{$DEBUGINFO ON} +{$EXTENDEDSYNTAX ON} +{$IMPORTEDDATA ON} +{$IOCHECKS ON} +{$LOCALSYMBOLS ON} +{$LONGSTRINGS ON} +{$OPENSTRINGS ON} +{$OPTIMIZATION OFF} +{$OVERFLOWCHECKS OFF} +{$RANGECHECKS OFF} +{$REFERENCEINFO ON} +{$SAFEDIVIDE OFF} +{$STACKFRAMES ON} +{$TYPEDADDRESS OFF} +{$VARSTRINGCHECKS ON} +{$WRITEABLECONST OFF} +{$MINENUMSIZE 1} +{$IMAGEBASE $400000} +{$DEFINE DEBUG} +{$DEFINE $(DELPHIBIN64)} +{$ENDIF IMPLICITBUILDING} +{$IMPLICITBUILD ON} + +requires + rtl, + X2LogDXE2; + +contains + X2Log.TextFormatter.Json in '..\X2Log.TextFormatter.Json.pas'; + +end. + diff --git a/Packages/X2LogJsonDXE2.dproj b/Packages/X2LogJsonDXE2.dproj new file mode 100644 index 0000000..6d26e67 --- /dev/null +++ b/Packages/X2LogJsonDXE2.dproj @@ -0,0 +1,187 @@ + + + {5742A326-C831-4737-AA01-900CC5C18CCA} + X2LogJsonDXE2.dpk + 13.4 + None + True + Debug + Win32 + 3 + Package + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + 1043 + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments= + All + true + true + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + + + true + $(DELPHIBIN64);$(DCC_Define) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + $(DELPHIBIN64) + $(DELPHILIB64) + 1033 + + + $(DELPHIBIN);$(DCC_Define) + $(DELPHILIB) + $(DELPHIBIN) + true + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + 1033 + + + DEBUG;$(DCC_Define) + false + true + true + true + + + true + 1033 + + + true + false + 1033 + false + + + false + RELEASE;$(DCC_Define) + 0 + false + + + true + 1033 + + + true + 1033 + + + + MainSource + + + + + + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + + + + Delphi.Personality.12 + Package + + + + X2LogJsonDXE2.dpk + + + True + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 1043 + 1252 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + + + True + True + + + 12 + + + + diff --git a/Packages/X2LogJsonDXE2.res b/Packages/X2LogJsonDXE2.res new file mode 100644 index 0000000000000000000000000000000000000000..27efe49b5f5b31b0c6298daa89904be58dbfef3a GIT binary patch literal 448 zcmZ9I%Sr=55Jk`EEQ~8Z!G&uXVK#!01bh(iMKWY7LwpdB3?#bqWBd{S#=U>wc)G_X znu6-CuDbVDbph<_ZQHrwKO1DJz9+_qR?y26XpohLQzqPbo-sBWal^Imk>|N_u#f!f z8v?)6NOwP0Aqb|*Z;JProAAPtISZatoAbZM$ptONLZ&XJf96hVJviA60I(v%kCFHuVGYJ2#pD literal 0 HcmV?d00001 diff --git a/Test/X2LogTest.dpr b/Test/X2LogTest.dpr index a986388..080f46b 100644 --- a/Test/X2LogTest.dpr +++ b/Test/X2LogTest.dpr @@ -25,7 +25,10 @@ uses X2Log.Util.Stream in '..\X2Log.Util.Stream.pas', X2Log.Decorator in '..\X2Log.Decorator.pas', X2Log.Observer.RollingLogFile in '..\X2Log.Observer.RollingLogFile.pas', - X2Log.Intf.NamedPipe in '..\X2Log.Intf.NamedPipe.pas'; + X2Log.Intf.NamedPipe in '..\X2Log.Intf.NamedPipe.pas', + X2Log.TextFormatter.Intf in '..\X2Log.TextFormatter.Intf.pas', + X2Log.TextFormatter.Default in '..\X2Log.TextFormatter.Default.pas', + X2Log.TextFormatter.Json in '..\X2Log.TextFormatter.Json.pas'; {$R *.res} diff --git a/Test/X2LogTest.dproj b/Test/X2LogTest.dproj index 2fc63b0..1666038 100644 --- a/Test/X2LogTest.dproj +++ b/Test/X2LogTest.dproj @@ -197,6 +197,9 @@ + + + RCDATA GraphicDetails diff --git a/Test/source/MainFrm.dfm b/Test/source/MainFrm.dfm index 61657c1..34bffdc 100644 --- a/Test/source/MainFrm.dfm +++ b/Test/source/MainFrm.dfm @@ -26,7 +26,7 @@ object MainForm: TMainForm Margins.Top = 8 Margins.Right = 8 Margins.Bottom = 8 - ActivePage = tsNamedPipe + ActivePage = tsRollingFile Align = alClient Images = ilsObservers TabOrder = 0 @@ -131,6 +131,32 @@ object MainForm: TMainForm Caption = 'Absolute path' TabOrder = 5 end + object pnlFileTextFormatter: TPanel + Left = 88 + Top = 176 + Width = 532 + Height = 49 + BevelOuter = bvNone + TabOrder = 6 + object rbFileTextFormatterDefault: TRadioButton + Left = 0 + Top = 0 + Width = 113 + Height = 17 + Caption = 'Default' + Checked = True + TabOrder = 0 + TabStop = True + end + object rbFileTextFormatterJson: TRadioButton + Left = 0 + Top = 23 + Width = 113 + Height = 17 + Caption = 'Json' + TabOrder = 1 + end + end end object tsRollingFile: TTabSheet Caption = 'Rolling File' @@ -212,6 +238,32 @@ object MainForm: TMainForm TabOrder = 6 Text = '7' end + object pnlRollingFileTextFormatter: TPanel + Left = 88 + Top = 224 + Width = 532 + Height = 49 + BevelOuter = bvNone + TabOrder = 7 + object rbRollingFileTextFormatterDefault: TRadioButton + Left = 0 + Top = 0 + Width = 113 + Height = 17 + Caption = 'Default' + Checked = True + TabOrder = 0 + TabStop = True + end + object rbRollingFileTextFormatterJson: TRadioButton + Left = 0 + Top = 23 + Width = 113 + Height = 17 + Caption = 'Json' + TabOrder = 1 + end + end end object tsNamedPipe: TTabSheet Caption = 'Named Pipe' @@ -609,7 +661,7 @@ object MainForm: TMainForm Left = 552 Top = 176 Bitmap = { - 494C01010200140060000C000C00FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 + 494C01010200140068000C000C00FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000300000000C00000001002000000000000009 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 diff --git a/Test/source/MainFrm.pas b/Test/source/MainFrm.pas index e096835..60e65b2 100644 --- a/Test/source/MainFrm.pas +++ b/Test/source/MainFrm.pas @@ -81,6 +81,12 @@ type edtRollingDays: TEdit; tsStructured: TTabSheet; btnValueTypes: TButton; + pnlFileTextFormatter: TPanel; + rbFileTextFormatterDefault: TRadioButton; + rbFileTextFormatterJson: TRadioButton; + pnlRollingFileTextFormatter: TPanel; + rbRollingFileTextFormatterDefault: TRadioButton; + rbRollingFileTextFormatterJson: TRadioButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); @@ -136,6 +142,8 @@ uses X2Log.Observer.MonitorForm, X2Log.Observer.NamedPipe, X2Log.Observer.RollingLogFile, + X2Log.TextFormatter.Intf, + X2Log.TextFormatter.Json, X2Log.Global; @@ -339,15 +347,22 @@ end; procedure TMainForm.btnFileStartClick(Sender: TObject); +var + textFormatter: IX2LogTextFormatter; + begin if not Assigned(FFileObserver) then begin + textFormatter := nil; + if rbFileTextFormatterJson.Checked then + textFormatter := TX2LogJsonTextFormatter.Create; + if rbProgramData.Checked then - FFileObserver := TX2LogFileObserver.CreateInProgramData(edtFilename.Text) + FFileObserver := TX2LogFileObserver.CreateInProgramData(edtFilename.Text, X2LogLevelsDefault, True, textFormatter) else if rbUserData.Checked then - FFileObserver := TX2LogFileObserver.CreateInUserAppData(edtFilename.Text) + FFileObserver := TX2LogFileObserver.CreateInUserAppData(edtFilename.Text, X2LogLevelsDefault, True, textFormatter) else - FFileObserver := TX2LogFileObserver.Create(edtFilename.Text); + FFileObserver := TX2LogFileObserver.Create(edtFilename.Text, X2LogLevelsDefault, True, textFormatter); FLog.Attach(FFileObserver); @@ -371,18 +386,23 @@ end; procedure TMainForm.btnRollingFileStartClick(Sender: TObject); var days: Integer; + textFormatter: IX2LogTextFormatter; begin if not Assigned(FRollingFileObserver) then begin + textFormatter := nil; + if rbFileTextFormatterJson.Checked then + textFormatter := TX2LogJsonTextFormatter.Create; + days := StrToIntDef(edtRollingDays.Text, 7); if rbRollingProgramData.Checked then - FRollingFileObserver := TX2RollingLogFileObserver.CreateInProgramData(edtFilename.Text, days) + FRollingFileObserver := TX2RollingLogFileObserver.CreateInProgramData(edtFilename.Text, days, X2LogLevelsDefault, True, textFormatter) else if rbRollingUserData.Checked then - FRollingFileObserver := TX2RollingLogFileObserver.CreateInUserAppData(edtFilename.Text, days) + FRollingFileObserver := TX2RollingLogFileObserver.CreateInUserAppData(edtFilename.Text, days, X2LogLevelsDefault, True, textFormatter) else - FRollingFileObserver := TX2RollingLogFileObserver.Create(edtFilename.Text, days); + FRollingFileObserver := TX2RollingLogFileObserver.Create(edtFilename.Text, days, X2LogLevelsDefault, True, textFormatter); FLog.Attach(FRollingFileObserver); diff --git a/X2Log.Constants.pas b/X2Log.Constants.pas index 0e64794..8a25ffe 100644 --- a/X2Log.Constants.pas +++ b/X2Log.Constants.pas @@ -48,6 +48,12 @@ resourcestring } LogFileLineDetails = '%0:s (details: %1:s)'; + { The format of the log message when structured information is present + + 0: Message + 1: Comma-separated key-value pairs + } + LogFileLineStructured = '%0:s [%1:s]'; { X2Log.Observer.RollingLogFile diff --git a/X2Log.Details.Default.pas b/X2Log.Details.Default.pas index 9720ebf..8b084f0 100644 --- a/X2Log.Details.Default.pas +++ b/X2Log.Details.Default.pas @@ -45,6 +45,9 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); virtual; abstract; procedure SaveToStream(AStream: TStream); virtual; abstract; + function GetDisplayValue: string; virtual; abstract; + + property DisplayValue: string read GetDisplayValue; property ValueType: TX2LogValueType read FValueType; end; @@ -80,6 +83,7 @@ type { IX2LogDetailsDictionary } function GetKeys: TEnumerable; function GetValueType(const Key: string): TX2LogValueType; + function GetDisplayValue(const Key: string): string; function GetStringValue(const Key: string): string; function GetBooleanValue(const Key: string): Boolean; @@ -201,6 +205,8 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); override; procedure SaveToStream(AStream: TStream); override; + function GetDisplayValue: string; override; + property Value: string read FValue write FValue; end; @@ -214,6 +220,8 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); override; procedure SaveToStream(AStream: TStream); override; + function GetDisplayValue: string; override; + property Value: Boolean read FValue write FValue; end; @@ -227,6 +235,8 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); override; procedure SaveToStream(AStream: TStream); override; + function GetDisplayValue: string; override; + property Value: Int64 read FValue write FValue; end; @@ -240,6 +250,8 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); override; procedure SaveToStream(AStream: TStream); override; + function GetDisplayValue: string; override; + property Value: Extended read FValue write FValue; end; @@ -253,6 +265,8 @@ type procedure LoadFromStream(AStream: TStream; ASize: Cardinal); override; procedure SaveToStream(AStream: TStream); override; + function GetDisplayValue: string; override; + property Value: TDateTime read FValue write FValue; end; @@ -424,6 +438,12 @@ begin end; +function TX2LogDictionaryDetails.GetDisplayValue(const Key: string): string; +begin + Result := FValues[Key].DisplayValue; +end; + + function TX2LogDictionaryDetails.GetStringValue(const Key: string): string; begin Result := (FValues[Key] as TX2LogDictionaryStringValue).Value; @@ -768,6 +788,12 @@ begin end; +function TX2LogDictionaryStringValue.GetDisplayValue: string; +begin + Result := Value; +end; + + { TX2LogDictionaryBooleanValue } constructor TX2LogDictionaryBooleanValue.Create(AValue: Boolean); begin @@ -792,6 +818,12 @@ begin end; +function TX2LogDictionaryBooleanValue.GetDisplayValue: string; +begin + Result := BoolToStr(Value, True); +end; + + { TX2LogDictionaryIntValue } constructor TX2LogDictionaryIntValue.Create(AValue: Int64); begin @@ -816,6 +848,12 @@ begin end; +function TX2LogDictionaryIntValue.GetDisplayValue: string; +begin + Result := IntToStr(Value); +end; + + { TX2LogDictionaryFloatValue } constructor TX2LogDictionaryFloatValue.Create(AValue: Extended); begin @@ -840,6 +878,12 @@ begin end; +function TX2LogDictionaryFloatValue.GetDisplayValue: string; +begin + Result := FormatFloat('0.########', Value); +end; + + { TX2LogDictionaryDateTimeValue } constructor TX2LogDictionaryDateTimeValue.Create(AValue: TDateTime); begin @@ -864,6 +908,12 @@ begin end; +function TX2LogDictionaryDateTimeValue.GetDisplayValue: string; +begin + Result := DateTimeToStr(Value); +end; + + initialization TX2LogDetailsRegistry.Register(StringDetailsSerializerIID, TX2LogStringDetailsSerializer.Create); TX2LogDetailsRegistry.Register(DictionaryDetailsSerializerIID, TX2LogDictionaryDetailsSerializer.Create); diff --git a/X2Log.Details.Intf.pas b/X2Log.Details.Intf.pas index dc0f59f..8a87a5c 100644 --- a/X2Log.Details.Intf.pas +++ b/X2Log.Details.Intf.pas @@ -25,6 +25,7 @@ type function GetKeys: TEnumerable; function GetValueType(const Key: string): TX2LogValueType; + function GetDisplayValue(const Key: string): string; function GetStringValue(const Key: string): string; function GetBooleanValue(const Key: string): Boolean; function GetIntValue(const Key: string): Int64; @@ -33,6 +34,7 @@ type property Keys: TEnumerable read GetKeys; property ValueType[const Key: string]: TX2LogValueType read GetValueType; + property DisplayValue[const Key: string]: string read GetDisplayValue; property StringValue[const Key: string]: string read GetStringValue; property BooleanValue[const Key: string]: Boolean read GetBooleanValue; diff --git a/X2Log.Observer.LogFile.pas b/X2Log.Observer.LogFile.pas index 5f63c05..ff9faeb 100644 --- a/X2Log.Observer.LogFile.pas +++ b/X2Log.Observer.LogFile.pas @@ -8,7 +8,8 @@ uses X2Log.Intf, X2Log.Observer.Custom, - X2Log.Observer.CustomThreaded; + X2Log.Observer.CustomThreaded, + X2Log.TextFormatter.Intf; type @@ -16,15 +17,17 @@ type private FOutputFileName: string; FLogDetails: Boolean; + FTextFormatter: IX2LogTextFormatter; protected + function GetTextFormatter: IX2LogTextFormatter; virtual; function CreateWorkerThread: TX2LogObserverWorkerThread; override; property OutputFileName: string read FOutputFileName; property LogDetails: Boolean read FLogDetails; public - constructor Create(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); - constructor CreateInProgramData(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); - constructor CreateInUserAppData(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); + constructor Create(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); + constructor CreateInProgramData(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); + constructor CreateInUserAppData(const AOutputFileName: string; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); end; @@ -32,14 +35,16 @@ type private FOutputFileName: string; FLogDetails: Boolean; + FTextFormatter: IX2LogTextFormatter; protected function GetFileName(AEntry: TX2LogQueueEntry): string; virtual; procedure ProcessEntry(AEntry: TX2LogQueueEntry); override; property OutputFileName: string read FOutputFileName; property LogDetails: Boolean read FLogDetails; + property TextFormatter: IX2LogTextFormatter read FTextFormatter; public - constructor Create(const AOutputFileName: string; ALogDetails: Boolean = True); + constructor Create(const AOutputFileName: string; ATextFormatter: IX2LogTextFormatter; ALogDetails: Boolean = True); end; @@ -51,21 +56,42 @@ uses Winapi.SHFolder, Winapi.Windows, - X2Log.Constants; + X2Log.Constants, + X2Log.TextFormatter.Default; +type + TX2LogFileTextFormatterHelper = class(TInterfacedObject, IX2LogTextFormatterHelper) + private + FEntry: TX2LogQueueEntry; + FLogFileName: string; + FLogDetails: Boolean; + protected + { IX2LogTextFormatterHelper } + function GetDetailsFilename: string; + + property Entry: TX2LogQueueEntry read FEntry; + property LogFileName: string read FLogFileName; + property LogDetails: Boolean read FLogDetails; + public + constructor Create(AEntry: TX2LogQueueEntry; const ALogFileName: string; ALogDetails: Boolean); + end; + { TX2LogFileObserver } -constructor TX2LogFileObserver.Create(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2LogFileObserver.Create(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); begin FOutputFileName := AOutputFileName; FLogDetails := ALogDetails; + FTextFormatter := ATextFormatter; + if not Assigned(FTextFormatter) then + FTextFormatter := TX2LogDefaultTextFormatter.Create; inherited Create(ALogLevels); end; -constructor TX2LogFileObserver.CreateInProgramData(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2LogFileObserver.CreateInProgramData(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); var path: PWideChar; @@ -73,14 +99,14 @@ begin GetMem(path, MAX_PATH); try OleCheck(SHGetFolderPath(0, CSIDL_COMMON_APPDATA, 0, SHGFP_TYPE_CURRENT, path)); - Create(IncludeTrailingPathDelimiter(path) + AOutputFileName, ALogLevels, ALogDetails); + Create(IncludeTrailingPathDelimiter(path) + AOutputFileName, ALogLevels, ALogDetails, ATextFormatter); finally FreeMem(path); end; end; -constructor TX2LogFileObserver.CreateInUserAppData(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2LogFileObserver.CreateInUserAppData(const AOutputFileName: string; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); var path: PWideChar; @@ -88,7 +114,7 @@ begin GetMem(path, MAX_PATH); try OleCheck(SHGetFolderPath(0, CSIDL_APPDATA, 0, SHGFP_TYPE_CURRENT, path)); - Create(IncludeTrailingPathDelimiter(path) + AOutputFileName, ALogLevels, ALogDetails); + Create(IncludeTrailingPathDelimiter(path) + AOutputFileName, ALogLevels, ALogDetails, ATextFormatter); finally FreeMem(path); end; @@ -97,15 +123,22 @@ end; function TX2LogFileObserver.CreateWorkerThread: TX2LogObserverWorkerThread; begin - Result := TX2LogFileWorkerThread.Create(OutputFileName, LogDetails); + Result := TX2LogFileWorkerThread.Create(OutputFileName, GetTextFormatter, LogDetails); +end; + + +function TX2LogFileObserver.GetTextFormatter: IX2LogTextFormatter; +begin + Result := FTextFormatter; end; { TX2LogFileWorkerThread } -constructor TX2LogFileWorkerThread.Create(const AOutputFileName: string; ALogDetails: Boolean); +constructor TX2LogFileWorkerThread.Create(const AOutputFileName: string; ATextFormatter: IX2LogTextFormatter; ALogDetails: Boolean); begin FOutputFileName := AOutputFileName; FLogDetails := ALogDetails; + FTextFormatter := ATextFormatter; inherited Create; end; @@ -114,29 +147,62 @@ end; procedure TX2LogFileWorkerThread.ProcessEntry(AEntry: TX2LogQueueEntry); var fileName: string; - baseReportFileName: string; - errorMsg: string; - detailsExtension: string; - detailsFile: THandle; - detailsFileStream: THandleStream; - detailsFileName: string; - detailsNumber: Integer; + line: string; writer: TStreamWriter; - logDetailsStreamable: IX2LogDetailsStreamable; begin fileName := GetFileName(AEntry); ForceDirectories(ExtractFilePath(fileName)); - if Length(AEntry.Category) > 0 then - errorMsg := Format(GetLogResourceString(@LogFileLineCategory), [AEntry.Message, AEntry.Category]) - else - errorMsg := Format(GetLogResourceString(@LogFileLineNoCategory), [AEntry.Message]); + line := TextFormatter.GetText(TX2LogFileTextFormatterHelper.Create(AEntry, fileName, LogDetails), + AEntry.Level, AEntry.DateTime, AEntry.Message, AEntry.Category, AEntry.Details); - if LogDetails and Supports(AEntry.Details, IX2LogDetailsStreamable, logDetailsStreamable) then + { Append line to log file } + writer := TFile.AppendText(fileName); + try + writer.WriteLine(line); + finally + FreeAndNil(writer); + end; +end; + + +function TX2LogFileWorkerThread.GetFileName(AEntry: TX2LogQueueEntry): string; +begin + Result := FOutputFileName; +end; + + +{ TX2LogFileTextFormatterHelper } +constructor TX2LogFileTextFormatterHelper.Create(AEntry: TX2LogQueueEntry; const ALogFileName: string; ALogDetails: Boolean); +begin + inherited Create; + + FEntry := AEntry; + FLogFileName := ALogFileName; + FLogDetails := ALogDetails; +end; + + +function TX2LogFileTextFormatterHelper.GetDetailsFilename: string; +var + logDetailsStreamable: IX2LogDetailsStreamable; + baseReportFileName: string; + detailsExtension: string; + detailsFile: THandle; + detailsFileStream: THandleStream; + detailsFileName: string; + detailsNumber: Integer; + +begin + Result := ''; + if not LogDetails then + exit; + + if Supports(Entry.Details, IX2LogDetailsStreamable, logDetailsStreamable) then begin - detailsExtension := ExtractFileExt(fileName); - baseReportFileName := ChangeFileExt(fileName, '_' + FormatDateTime(GetLogResourceString(@LogFileNameDateFormat), AEntry.DateTime)); + detailsExtension := ExtractFileExt(LogFileName); + baseReportFileName := ChangeFileExt(LogFileName, '_' + FormatDateTime(GetLogResourceString(@LogFileNameDateFormat), Entry.DateTime)); detailsFileName := baseReportFileName + detailsExtension; detailsNumber := 0; @@ -172,29 +238,12 @@ begin CloseHandle(detailsFile); end; - // ErrorLogs.Add(reportFileName); - - errorMsg := Format(GetLogResourceString(@LogFileLineDetails), [errorMsg, ExtractFileName(detailsFileName)]); + Result := detailsFileName; break; end; until False; end; end; - - { Append line to log file } - writer := TFile.AppendText(fileName); - try - writer.WriteLine('[' + FormatDateTime(GetLogResourceString(@LogFileLineDateFormat), AEntry.DateTime) + '] ' + - GetLogLevelText(AEntry.Level) + ': ' + errorMsg); - finally - FreeAndNil(writer); - end; -end; - - -function TX2LogFileWorkerThread.GetFileName(AEntry: TX2LogQueueEntry): string; -begin - Result := FOutputFileName; end; end. diff --git a/X2Log.Observer.MonitorForm.dfm b/X2Log.Observer.MonitorForm.dfm index 3a51ad2..165ddff 100644 --- a/X2Log.Observer.MonitorForm.dfm +++ b/X2Log.Observer.MonitorForm.dfm @@ -284,7 +284,7 @@ object X2LogObserverMonitorForm: TX2LogObserverMonitorForm Left = 448 Top = 48 Bitmap = { - 494C01010A004000000110001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 + 494C01010A004000040110001000FFFFFFFFFF10FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000003000000001002000000000000030 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 diff --git a/X2Log.Observer.MonitorForm.pas b/X2Log.Observer.MonitorForm.pas index 0ee67ec..8e475c2 100644 --- a/X2Log.Observer.MonitorForm.pas +++ b/X2Log.Observer.MonitorForm.pas @@ -745,25 +745,12 @@ end; procedure TX2LogObserverMonitorForm.SetDictionaryDetails(ADetails: IX2LogDetailsDictionary); var key: string; - displayValue: string; begin vleDetailsDictionary.Strings.Clear; for key in ADetails.Keys do - begin - displayValue := ''; - - case ADetails.ValueType[key] of - StringValue: displayValue := ADetails.StringValue[key]; - BooleanValue: displayValue := BoolToStr(ADetails.BooleanValue[key], True); - IntValue: displayValue := IntToStr(ADetails.IntValue[key]); - FloatValue: displayValue := FormatFloat('0.########', ADetails.FloatValue[key]); - DateTimeValue: displayValue := DateTimeToStr(ADetails.DateTimeValue[key]); - end; - - vleDetailsDictionary.Values[key] := displayValue; - end; + vleDetailsDictionary.Values[key] := ADetails.DisplayValue[key]; FCopyHandler := CopyDictionaryDetails; SetVisibleDetails(vleDetailsDictionary); diff --git a/X2Log.Observer.RollingLogFile.pas b/X2Log.Observer.RollingLogFile.pas index 3958202..c4ccd77 100644 --- a/X2Log.Observer.RollingLogFile.pas +++ b/X2Log.Observer.RollingLogFile.pas @@ -6,7 +6,8 @@ uses X2Log.Intf, X2Log.Observer.CustomThreaded, - X2Log.Observer.LogFile; + X2Log.Observer.LogFile, + X2Log.TextFormatter.Intf; const @@ -22,9 +23,9 @@ type property Days: Integer read FDays; public - constructor Create(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); - constructor CreateInProgramData(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); - constructor CreateInUserAppData(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True); + constructor Create(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); + constructor CreateInProgramData(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); + constructor CreateInUserAppData(const AFileName: string; ADays: Integer = X2LogDefaultDays; ALogLevels: TX2LogLevels = X2LogLevelsDefault; ALogDetails: Boolean = True; ATextFormatter: IX2LogTextFormatter = nil); end; @@ -44,7 +45,7 @@ type property Days: Integer read FDays; property LastCleanupDate: TDateTime read FLastCleanupDate write FLastCleanupDate; public - constructor Create(const AFileName: string; ADays: Integer; ALogDetails: Boolean = True); + constructor Create(const AFileName: string; ADays: Integer; ATextFormatter: IX2LogTextFormatter; ALogDetails: Boolean = True); end; @@ -59,44 +60,44 @@ uses { TX2RollingLogFileObserver } -constructor TX2RollingLogFileObserver.Create(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2RollingLogFileObserver.Create(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); begin FDays := ADays; - inherited Create(AFileName, ALogLevels, ALogDetails); + inherited Create(AFileName, ALogLevels, ALogDetails, ATextFormatter); end; -constructor TX2RollingLogFileObserver.CreateInProgramData(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2RollingLogFileObserver.CreateInProgramData(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); begin FDays := ADays; - inherited CreateInProgramData(AFileName, ALogLevels, ALogDetails); + inherited CreateInProgramData(AFileName, ALogLevels, ALogDetails, ATextFormatter); end; -constructor TX2RollingLogFileObserver.CreateInUserAppData(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean); +constructor TX2RollingLogFileObserver.CreateInUserAppData(const AFileName: string; ADays: Integer; ALogLevels: TX2LogLevels; ALogDetails: Boolean; ATextFormatter: IX2LogTextFormatter); begin FDays := ADays; - inherited CreateInUserAppData(AFileName, ALogLevels, ALogDetails); + inherited CreateInUserAppData(AFileName, ALogLevels, ALogDetails, ATextFormatter); end; function TX2RollingLogFileObserver.CreateWorkerThread: TX2LogObserverWorkerThread; begin - Result := TX2RollingLogFileWorkerThread.Create(OutputFileName, Days, LogDetails); + Result := TX2RollingLogFileWorkerThread.Create(OutputFileName, Days, GetTextFormatter, LogDetails); end; { TX2RollingLogFileWorkerThread } -constructor TX2RollingLogFileWorkerThread.Create(const AFileName: string; ADays: Integer; ALogDetails: Boolean); +constructor TX2RollingLogFileWorkerThread.Create(const AFileName: string; ADays: Integer; ATextFormatter: IX2LogTextFormatter; ALogDetails: Boolean); begin FDays := ADays; FFormatSettings := TFormatSettings.Create; FDateFormat := GetLogResourceString(@RollingLogFileDateFormat); - inherited Create(AFileName, ALogDetails); + inherited Create(AFileName, ATextFormatter, ALogDetails); end; diff --git a/X2Log.TextFormatter.Default.pas b/X2Log.TextFormatter.Default.pas new file mode 100644 index 0000000..9cb1341 --- /dev/null +++ b/X2Log.TextFormatter.Default.pas @@ -0,0 +1,78 @@ +unit X2Log.TextFormatter.Default; + +interface +uses + X2Log.Details.Intf, + X2Log.Intf, + X2Log.TextFormatter.Intf; + + +type + TX2LogDefaultTextFormatter = class(TInterfacedObject, IX2LogTextFormatter) + protected + function GetText(AHelper: IX2LogTextFormatterHelper; ALevel: TX2LogLevel; ADateTime: TDateTime; const AMessage: string; const ACategory: string; ADetails: IX2LogDetails): string; + + function GetDictionaryDisplayValues(ADetails: IX2LogDetailsDictionary): string; + end; + + +implementation +uses + System.SysUtils, + + X2Log.Constants; + + +{ TX2LogDefaultTextFormatter } +function TX2LogDefaultTextFormatter.GetText(AHelper: IX2LogTextFormatterHelper; ALevel: TX2LogLevel; ADateTime: TDateTime; + const AMessage, ACategory: string; ADetails: IX2LogDetails): string; +var + line: string; + dictionaryDetails: IX2LogDetailsDictionary; + detailsFileName: string; + +begin + if Length(ACategory) > 0 then + line := Format(GetLogResourceString(@LogFileLineCategory), [AMessage, ACategory]) + else + line := Format(GetLogResourceString(@LogFileLineNoCategory), [AMessage]); + + if Supports(ADetails, IX2LogDetailsDictionary, dictionaryDetails) then + begin + line := Format(GetLogResourceString(@LogFileLineStructured), [line, GetDictionaryDisplayValues(dictionaryDetails)]); + end else + begin + detailsFileName := AHelper.GetDetailsFilename; + if Length(detailsFileName) > 0 then + line := Format(GetLogResourceString(@LogFileLineDetails), [line, ExtractFileName(detailsFileName)]); + end; + + Result := '[' + FormatDateTime(GetLogResourceString(@LogFileLineDateFormat), ADateTime) + '] ' + + GetLogLevelText(ALevel) + ': ' + line; + +end; + + +function TX2LogDefaultTextFormatter.GetDictionaryDisplayValues(ADetails: IX2LogDetailsDictionary): string; +var + displayValues: TStringBuilder; + key: string; + +begin + displayValues := TStringBuilder.Create; + try + for key in ADetails.Keys do + begin + if displayValues.Length > 0 then + displayValues.Append(', '); + + displayValues.Append(key).Append(': ').Append(ADetails.DisplayValue[key]); + end; + + Result := displayValues.ToString; + finally + FreeAndNil(displayValues); + end; +end; + +end. diff --git a/X2Log.TextFormatter.Intf.pas b/X2Log.TextFormatter.Intf.pas new file mode 100644 index 0000000..fd7acfd --- /dev/null +++ b/X2Log.TextFormatter.Intf.pas @@ -0,0 +1,20 @@ +unit X2Log.TextFormatter.Intf; + +interface +uses + X2Log.Intf; + +type + IX2LogTextFormatterHelper = interface + ['{D1A1DAD5-0F96-491F-8BD5-0B9D0BE87C32}'] + function GetDetailsFilename: string; + end; + + IX2LogTextFormatter = interface + ['{C49BE49D-8563-4097-A2B7-0869F27F5EDD}'] + function GetText(AHelper: IX2LogTextFormatterHelper; ALevel: TX2LogLevel; ADateTime: TDateTime; const AMessage: string; const ACategory: string; ADetails: IX2LogDetails): string; + end; + +implementation + +end. diff --git a/X2Log.TextFormatter.Json.pas b/X2Log.TextFormatter.Json.pas new file mode 100644 index 0000000..c71d8ab --- /dev/null +++ b/X2Log.TextFormatter.Json.pas @@ -0,0 +1,127 @@ +unit X2Log.TextFormatter.Json; + +interface +uses + JsonDataObjects, + + X2Log.Details.Intf, + X2Log.Intf, + X2Log.TextFormatter.Intf; + +type + { + Default: + X2Log naming convention is used for the property names + + Kibana: + Logstash/Kibana compatible (message, @timestamp, @version) property names + } + TX2LogJsonStyle = (Default, Kibana); + + + TX2LogJsonTextFormatter = class(TInterfacedObject, IX2LogTextFormatter) + private + FSingleLine: Boolean; + FStyle: TX2LogJsonStyle; + protected + function GetText(AHelper: IX2LogTextFormatterHelper; ALevel: TX2LogLevel; ADateTime: TDateTime; const AMessage: string; const ACategory: string; ADetails: IX2LogDetails): string; + procedure AddDictionaryDetails(AObject: TJsonObject; ADetails: IX2LogDetailsDictionary); + + property SingleLine: Boolean read FSingleLine; + property Style: TX2LogJsonStyle read FStyle; + public + constructor Create(ASingleLine: Boolean = True; AStyle: TX2LogJsonStyle = TX2LogJsonStyle.Default); + end; + + +implementation +uses + System.SysUtils; + + +{ TX2LogJsonTextFormatter } +constructor TX2LogJsonTextFormatter.Create(ASingleLine: Boolean; AStyle: TX2LogJsonStyle); +begin + inherited Create; + + FSingleLine := ASingleLine; + FStyle := AStyle; +end; + + +function TX2LogJsonTextFormatter.GetText(AHelper: IX2LogTextFormatterHelper; ALevel: TX2LogLevel; ADateTime: TDateTime; + const AMessage, ACategory: string; ADetails: IX2LogDetails): string; +const + LogLevelName: array[TX2LogLevel] of string = ('verbose', 'info', 'warning', 'error'); + +var + line: TJsonObject; + dictionaryDetails: IX2LogDetailsDictionary; + detailsFileName: string; + +begin + line := TJsonObject.Create; + try + case Style of + Default: + begin + line.D['dateTime'] := ADateTime; + line.S['message'] := AMessage; + end; + + Kibana: + begin + line.S['@version'] := '1'; + line.D['@timestamp'] := ADateTime; + line.S['message'] := AMessage; + end; + end; + + line.S['level'] := LogLevelName[ALevel]; + line.S['category'] := ACategory; + + if Supports(ADetails, IX2LogDetailsDictionary, dictionaryDetails) then + begin + AddDictionaryDetails(line, dictionaryDetails); + end else + begin + detailsFileName := AHelper.GetDetailsFilename; + if Length(detailsFileName) > 0 then + line.S['details'] := ExtractFileName(detailsFileName); + end; + + Result := line.ToJSON(SingleLine); + finally + FreeAndNil(line); + end; +end; + + +procedure TX2LogJsonTextFormatter.AddDictionaryDetails(AObject: TJsonObject; ADetails: IX2LogDetailsDictionary); +var + key: string; + jsonKey: string; + valueType: TX2LogValueType; + +begin + for key in ADetails.Keys do + begin + jsonKey := key; + while AObject.Contains(jsonKey) do + jsonKey := '_' + jsonKey; + + valueType := ADetails.ValueType[key]; + + case valueType of + StringValue: AObject.S[jsonKey] := ADetails.StringValue[key]; + BooleanValue: AObject.B[jsonKey] := ADetails.BooleanValue[key]; + IntValue: AObject.L[jsonKey] := ADetails.IntValue[key]; + FloatValue: AObject.F[jsonKey] := ADetails.FloatValue[key]; + DateTimeValue: AObject.D[jsonKey] := ADetails.DateTimeValue[key]; + else + AObject.S[jsonKey] := Format('', [Ord(valueType)]); + end; + end; +end; + +end.