-
Notifications
You must be signed in to change notification settings - Fork 90
/
Grijjy.CloudLogging.pas
616 lines (521 loc) · 19.7 KB
/
Grijjy.CloudLogging.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
unit Grijjy.CloudLogging;
interface
uses
System.Classes,
System.SysUtils,
System.TypInfo,
System.Messaging,
System.Generics.Collections,
Grijjy.Bson.IO,
Grijjy.Collections,
Grijjy.CloudLogging.Protocol;
type
{ Logging levels for the GrijjyLog.Send and GrijjyLog.SetLogLevel routines. }
TgoLogLevel = (
{ Informational message. By default, informational messages are logged in
DEBUG mode, but not in RELEASE mode.
Call Grijjy.SetLogLevel(TgoLogLevel.Info) to always log
informational messages (as well as all other message levels) }
Info,
{ Warning message. Warning messages are logged by default, unless you call
grSetLogLevel(TgrLogLevel.Error) to only log error messages. }
Warning,
{ Error message. Error messages are always logged. }
Error);
type
{ Static class that forms the main entry for logging messages. }
GrijjyLog = class // static
public const
DEFAULT_BROKER = 'tcp://localhost:7337';
DEFAULT_SERVICE = 'Default';
{$REGION 'Internal Declarations'}
private class var
FLogLevel: TgoLogLevel;
FBroker: String;
FService: String;
FLogger: TgoCloudLogger;
FMaxInstancesPerClass: Integer;
private
class procedure Send(const AMsg: String; const ALevel: TgoLogLevel;
const AService: String; const ADataFormat: Integer;
const AData: TBytes); overload; static;
class function ObjectToJson(const AObject: TObject;
const AMinFieldVisibility: TMemberVisibility;
const AMaxNesting: Integer): String;
class procedure WriteObject(const ALevel: Integer; const AObject: TObject;
const AWriter: IgoJsonWriter; const AMinFieldVisibility: TMemberVisibility;
const AMaxNesting: Integer; const AVisitedObjects: TgoSet<TObject>);
public
class constructor Create;
class destructor Destroy;
{$ENDREGION 'Internal Declarations'}
public
{ Connects the the broker.
Parameters:
ABroker: host name for the logging broker.
AService: service name for the logging viewer.
If you do not call this method, then the logger will automatically connect
the first time you call one of the Send methods (using the Broker and
Service property values). }
class procedure Connect(const ABroker, AService: String); static;
{ Sets the log/verbosity level of messages logged with Log.
* Info: all messages are logged.
* Warning: only warning and error messages are logged.
* Error: only error messages are logged.
The default log level is Info in DEBUG mode and Warning in RELEASE mode.
Parameters:
ALevel: the logging level. }
class procedure SetLogLevel(const ALevel: TgoLogLevel); static;
{ Platform-independent logging routine.
Parameters:
AMsg: the message to log.
ALevel: (optional) the level of the log message. Defaults to Warning.
AService: (optional) the service to use when sending the message with ZMQ.
Note that logging is also enabled in RELEASE configurations, so you may
want to surround the log call with an IFDEF DEBUG directive to only log in
debug builds. }
class procedure Send(const AMsg: String;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
{ Platform-independent logging routine.
Parameters:
AMsg: the message to log.
AArgs: arguments to format the message.
ALevel: (optional) the level of the log message. Defaults to Warning.
AService: (optional) the service to use when sending the message with ZMQ.
Note that logging is also enabled in RELEASE configurations, so you may
want to surround the log call with an IFDEF DEBUG directive to only log in
debug builds. }
class procedure Send(const AMsg: String; const AArgs: array of const;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; static;
{ These Log overloads allow for logging simple values. }
class procedure Send(const AMsg, AValue: String;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: Integer;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: Boolean;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: Extended;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: TStrings;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: TBytes;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
class procedure Send(const AMsg: String; const AValue: Pointer;
const ASize: Integer; const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; inline; static;
{ Logs an object and all of its fields and properties.
Parameters:
AValue: the object whose fields and properties to log.
AMinFieldVisibility: (optional) minimum field visibility. Defaults to
mvPublic so only public and published fields are send. Set to
mvProtected to also include protected fields, or mvPrivate to include
all fields.
Note that properties are only send if they are published!
AMaxNesting: (optional) maximum number of levels of subobjects to send.
Set to 1 to only send the fields and properties of this object. Higher
values will also send fields and properties of subobjects. Defaults
to 4 to save space and time.
Note that this can potentially be a slow and bandwidth-intensive call since
RTTI is used to query the object, and it may result in large data loads
depending on the AMinVisibility and AMaxNesting parameters. }
class procedure Send(const AMsg: String; const AValue: TObject;
const AMinFieldVisibility: TMemberVisibility = mvPublic;
const AMaxNesting: Integer = 4;
const ALevel: TgoLogLevel = TgoLogLevel.Warning;
const AService: String = ''); overload; static;
{ Logs the start of a method block. Subsequent calls to Log will be treated
as part of this method, until ExitMethod is called.
Parameters:
AInstance: object instance whose method is begin entered.
AMethodName: name of the method that is being entered.
SeeAlso:
ExitMethod }
class procedure EnterMethod(const AInstance: TObject;
const AMethodName: String; const AService: String = ''); overload; static;
{ Logs the start of a method block. Subsequent calls to Log will be treated
as part of this method, until ExitMethod is called.
Parameters:
AMethodName: name of the procedure or function that is being entered.
SeeAlso:
ExitMethod }
class procedure EnterMethod(const AMethodName: String;
const AService: String = ''); overload; inline; static;
{ Logs the end of a method block, previously started with EnterMethod.
Parameters:
AInstance: object instance whose method is begin exited.
AMethodName: name of the method that is being exited.
SeeAlso:
EnterMethod }
class procedure ExitMethod(const AInstance: TObject;
const AMethodName: String; const AService: String = ''); overload; static;
{ Logs the end of a method block, previously started with EnterMethod.
Parameters:
AMethodName: name of the procedure or function that is being exited.
SeeAlso:
EnterMethod }
class procedure ExitMethod(const AMethodName: String;
const AService: String = ''); overload; inline; static;
{ Hostname for the logging broker.
Changing this value will reconnect to the new broker. }
class property Broker: String read FBroker write FBroker;
{ Service name for the logging viewer.
You can change this value at any time. }
class property Service: String read FService write FService;
{ When the log viewer request a list of all instances of a specific class,
this property determines the maximum number of instances returned.
Defaults to 100 to limit traffic and memory use. }
class property MaxInstancesPerClass: Integer read FMaxInstancesPerClass write FMaxInstancesPerClass;
end;
type
{ When the Grijjy Log Viewer requests a list of live watches, a message
of this type is broadcast. You can subscribe to this message type to add
to the list of watches.
Multiple listeners can subscribe to this message type and add their own
watches.
In your message handler, you can use the Add methods to add your own
watches.
This message is send from the UI thread. }
TgoLiveWatchesMessage = class(TMessage)
{$REGION 'Internal Declarations'}
private
FWatches: TList<TgoLiveWatch>;
{$ENDREGION 'Internal Declarations'}
public
constructor Create;
destructor Destroy; override;
{ Various methods for adding watches for different types of data.
Parameters:
AName: the name of the watch.
AValue: the value of the watch.
AValueAlign: (optional) display text alignment of the watch. Defaults
to Right alignment for numeric values, or Left alignment otherwise. }
procedure Add(const AName, AValue: String;
const AValueAlign: TgoWatchAlign = TgoWatchAlign.Left); overload;
procedure Add(const AName: String; const AValue: Integer;
const AValueAlign: TgoWatchAlign = TgoWatchAlign.Right); overload;
procedure Add(const AName: String; const AValue: Double;
const ANumDecimals: Integer = 2;
const AValueAlign: TgoWatchAlign = TgoWatchAlign.Right); overload;
procedure Add(const AName: String; const AValue: Boolean;
const AValueAlign: TgoWatchAlign = TgoWatchAlign.Left); overload;
{ Returns the current array of watches. Used internally. }
function GetWatches: TArray<TgoLiveWatch>;
end;
implementation
uses
System.ZLib,
System.Rtti,
Grijjy.SysUtils;
const
LOG_LEVEL_ENTER_METHOD = Ord(TgoLogLevel.Error) + 1;
LOG_LEVEL_EXIT_METHOD = LOG_LEVEL_ENTER_METHOD + 1;
{ GrijjyLog }
class procedure GrijjyLog.Connect(const ABroker, AService: String);
begin
FBroker := ABroker;
FService := AService;
Send('', TgoLogLevel.Error, AService, LOG_FORMAT_CONNECTED, nil);
end;
class constructor GrijjyLog.Create;
begin
FLogLevel := TgoLogLevel.Warning;
FBroker := DEFAULT_BROKER;
FService := DEFAULT_SERVICE;
FMaxInstancesPerClass := 100;
FLogger := nil;
end;
class destructor GrijjyLog.Destroy;
begin
FreeAndNil(FLogger);
end;
class procedure GrijjyLog.EnterMethod(const AMethodName, AService: String);
begin
Send(AMethodName, TgoLogLevel(LOG_LEVEL_ENTER_METHOD), AService, 0, nil);
end;
class procedure GrijjyLog.EnterMethod(const AInstance: TObject;
const AMethodName, AService: String);
begin
if Assigned(AInstance) then
begin
if (AInstance is TComponent) then
EnterMethod(AInstance.ClassName + '(' + TComponent(AInstance).Name + ').' + AMethodName, AService)
else
EnterMethod(AInstance.ClassName + '.' + AMethodName, AService)
end
else
EnterMethod(AMethodName, AService);
end;
class procedure GrijjyLog.ExitMethod(const AMethodName, AService: String);
begin
Send(AMethodName, TgoLogLevel(LOG_LEVEL_EXIT_METHOD), AService, 0, nil);
end;
class function GrijjyLog.ObjectToJson(const AObject: TObject;
const AMinFieldVisibility: TMemberVisibility;
const AMaxNesting: Integer): String;
var
VisitedObjects: TgoSet<TObject>;
Writer: IgoJsonWriter;
begin
VisitedObjects := nil;
TRttiContext.KeepContext;
try
VisitedObjects := TgoSet<TObject>.Create;
Writer := TgoJsonWriter.Create;
WriteObject(1, AObject, Writer, AMinFieldVisibility, AMaxNesting, VisitedObjects);
Result := Writer.ToJson;
finally
VisitedObjects.Free;
TRttiContext.DropContext;
end;
end;
class procedure GrijjyLog.ExitMethod(const AInstance: TObject;
const AMethodName, AService: String);
begin
if Assigned(AInstance) then
begin
if (AInstance is TComponent) then
ExitMethod(AInstance.ClassName + '(' + TComponent(AInstance).Name + ').' + AMethodName, AService)
else
ExitMethod(AInstance.ClassName + '.' + AMethodName, AService)
end
else
ExitMethod(AMethodName, AService);
end;
class procedure GrijjyLog.Send(const AMsg: String; const ALevel: TgoLogLevel;
const AService: String; const ADataFormat: Integer; const AData: TBytes);
begin
if (FLogger = nil) then
FLogger := TgoCloudLogger.Create;
FLogger.Broker := FBroker;
FLogger.Send(AService, AMsg, Ord(ALevel), ADataFormat, AData);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: Integer;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg + ' = ' + IntToStr(AValue), ALevel, AService, 0, nil);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: Boolean;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg + ' = ' + BoolToStr(AValue, True), ALevel, AService, 0, nil);
end;
class procedure GrijjyLog.Send(const AMsg, AValue: String;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg + ' = ' + AValue, ALevel, AService, 0, nil);
end;
class procedure GrijjyLog.Send(const AMsg: String; const ALevel: TgoLogLevel;
const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg, ALevel, AService, LOG_FORMAT_NONE, nil);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AArgs: array of const;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(Format(AMsg, AArgs, goUSFormatSettings), ALevel, AService, 0, nil);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: Pointer;
const ASize: Integer; const ALevel: TgoLogLevel; const AService: String);
var
Bytes: TBytes;
begin
if (ALevel >= FLogLevel) and Assigned(AValue) and (ASize > 0) then
begin
SetLength(Bytes, ASize);
Move(AValue^, Bytes[0], ASize);
Send(AMsg, ALevel, AService, LOG_FORMAT_MEMORY, Bytes);
end;
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: TObject;
const AMinFieldVisibility: TMemberVisibility; const AMaxNesting: Integer;
const ALevel: TgoLogLevel; const AService: String);
var
Json: String;
Bytes, ZBytes: TBytes;
begin
if (ALevel >= FLogLevel) and Assigned(AValue) then
begin
ZBytes := nil;
Json := ObjectToJson(AValue, AMinFieldVisibility, AMaxNesting);
if (Json <> '') then
begin
Bytes := TEncoding.UTF8.GetBytes(Json);
ZCompress(Bytes, ZBytes);
end;
Send(AMsg, ALevel, AService, LOG_FORMAT_OBJECT, ZBytes);
end;
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: TBytes;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg, ALevel, AService, LOG_FORMAT_MEMORY, AValue);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: Extended;
const ALevel: TgoLogLevel; const AService: String);
begin
if (ALevel >= FLogLevel) then
Send(AMsg + ' = ' + FloatToStr(AValue, goUSFormatSettings), ALevel, AService, 0, nil);
end;
class procedure GrijjyLog.Send(const AMsg: String; const AValue: TStrings;
const ALevel: TgoLogLevel; const AService: String);
var
S: String;
Bytes: TBytes;
begin
if (ALevel >= FLogLevel) and Assigned(AValue) then
begin
S := AValue.CommaText;
Bytes := TEncoding.UTF8.GetBytes(S);
Send(AMsg, ALevel, AService, LOG_FORMAT_TSTRINGS, Bytes);
end;
end;
class procedure GrijjyLog.SetLogLevel(const ALevel: TgoLogLevel);
begin
FLogLevel := ALevel;
end;
class procedure GrijjyLog.WriteObject(const ALevel: Integer;
const AObject: TObject; const AWriter: IgoJsonWriter;
const AMinFieldVisibility: TMemberVisibility; const AMaxNesting: Integer;
const AVisitedObjects: TgoSet<TObject>);
var
Context: TRttiContext;
ObjType: TRttiType;
Field: TRttiField;
Prop: TRttiProperty;
Names: TgoSet<String>;
procedure WriteValue(const AName: String; const AValue: TValue);
var
SubObject: TObject;
TypeData: PTypeData;
S: String;
begin
if (Names.Contains(AName)) then
Exit;
Names.Add(AName);
case AValue.Kind of
tkClass:
begin
SubObject := AValue.AsObject;
if (SubObject = nil) then
AWriter.WriteString(AName, '(empty)')
else if (ALevel < AMaxNesting) then
begin
AWriter.WriteName(AName);
WriteObject(ALevel + 1, SubObject, AWriter, AMinFieldVisibility,
AMaxNesting, AVisitedObjects);
end;
end;
tkSet:
begin
{ Cannot use AValue.ToString here since it calls SetToString, and
SetToString raises an AV when type info is insufficient. }
S := '';
TypeData := AValue.TypeData;
if Assigned(TypeData) and Assigned(TypeData.CompType) then
S := AValue.ToString;
if (S = '') then
S := '[???]';
AWriter.WriteString(AName, S);
end
else
AWriter.WriteString(AName, AValue.ToString);
end;
end;
begin
if (AObject = nil) or (ALevel > AMaxNesting) then
Exit;
if (AVisitedObjects.Contains(AObject)) then
begin
AWriter.WriteString(Format('(%s @ %p)', [AObject.ClassName, Pointer(AObject)]));
Exit;
end;
AWriter.WriteStartDocument;
try
AWriter.WriteString('@Class', AObject.ClassName);
AVisitedObjects.Add(AObject);
ObjType := Context.GetType(AObject.ClassType);
if (ObjType = nil) then
Exit;
Names := TgoSet<String>.Create;
try
for Field in ObjType.GetFields do
begin
if (Field.Visibility >= AMinFieldVisibility) then
try
WriteValue(Field.Name, Field.GetValue(AObject));
except
{ Ignore this field }
end;
end;
for Prop in ObjType.GetProperties do
begin
if (Prop.Visibility = mvPublished) and (Prop.IsReadable) then
try
WriteValue(Prop.Name, Prop.GetValue(AObject));
except
{ Ignore this property }
end;
end;
finally
Names.Free;
end;
finally
AWriter.WriteEndDocument;
end;
end;
{ TgoLiveWatchesMessage }
procedure TgoLiveWatchesMessage.Add(const AName, AValue: String;
const AValueAlign: TgoWatchAlign);
var
Watch: TgoLiveWatch;
begin
Watch.Name := AName;
Watch.Value := AValue;
Watch.ValueAlign := AValueAlign;
FWatches.Add(Watch);
end;
procedure TgoLiveWatchesMessage.Add(const AName: String; const AValue: Integer;
const AValueAlign: TgoWatchAlign);
begin
Add(AName, IntToStr(AValue), AValueAlign);
end;
procedure TgoLiveWatchesMessage.Add(const AName: String; const AValue: Boolean;
const AValueAlign: TgoWatchAlign);
begin
Add(AName, BoolToStr(AValue, True), AValueAlign);
end;
procedure TgoLiveWatchesMessage.Add(const AName: String; const AValue: Double;
const ANumDecimals: Integer; const AValueAlign: TgoWatchAlign);
begin
Add(AName, FloatToStrF(AValue, ffFixed, 15, ANumDecimals, goUSFormatSettings),
AValueAlign);
end;
constructor TgoLiveWatchesMessage.Create;
begin
inherited Create;
FWatches := TList<TgoLiveWatch>.Create;
end;
destructor TgoLiveWatchesMessage.Destroy;
begin
FWatches.Free;
inherited;
end;
function TgoLiveWatchesMessage.GetWatches: TArray<TgoLiveWatch>;
begin
Result := FWatches.ToArray;
end;
end.