-
Notifications
You must be signed in to change notification settings - Fork 71
/
Facade.fs
1396 lines (1207 loc) · 53.6 KB
/
Facade.fs
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
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// The logging namespace, which contains the logging abstraction for this
/// library. See https://github.com/logary/logary for details. This module is
/// completely stand-alone in that it has no external references and its adapter
/// in Logary has been well tested.
namespace Logary.Facade
open System
open System.Runtime.CompilerServices
open Hopac
open Hopac.Infixes
/// Module that contains the 'known' keys of the Maps in the Message type's context.
module Literals =
/// What version of the Facade is this. This is a major version that allows the Facade
/// adapter to choose how it handles the API.
let FacadeVersion = 4u
/// What language this Facade has. This controls things like naming standards.
let FacadeLanguage = "F#"
/// Avoid conflict with user defined context key
[<Literal>]
let internal LogaryPrefix = "_logary."
/// To recognize all fields for generate formatted msg
[<Literal>]
let FieldsPrefix = "_fields."
/// To recognize all gauge fields for generate formatted msg
[<Literal>]
let GaugeNamePrefix = LogaryPrefix + "gauge."
/// All gauges should have this tag. It's added whenever a gauge is added to the message.
[<Literal>]
let GaugeTag = "gauge"
[<Literal>]
let ErrorsContextName = LogaryPrefix + "errors"
/// The tags context field
[<Literal>]
let TagsContextName = LogaryPrefix + "tags"
/// The log level denotes how 'important' the gauge or event message is.
[<CustomEquality; CustomComparison>]
type LogLevel =
/// The log message is not that important; can be used for intricate debugging.
| Verbose
/// The log message is at a default level, debug level. Useful for shipping to
/// infrastructure that further processes it, but not so useful for human
/// inspection in its raw format, except during development.
| Debug
/// The log message is informational; e.g. the service started, stopped or
/// some important business event occurred.
| Info
/// The log message is a warning; e.g. there was an unhandled exception or
/// an even occurred which was unexpected. Sometimes human corrective action
/// is needed.
| Warn
/// The log message is at an error level, meaning an unhandled exception
/// occurred at a location where it is deemed important to keeping the service
/// running. A human should take corrective action.
| Error
/// The log message denotes a fatal error which cannot be recovered from. The
/// service should be shut down. Human corrective action is needed.
| Fatal
/// Converts the LogLevel to a string
override x.ToString () =
match x with
| Verbose -> "verbose"
| Debug -> "debug"
| Info -> "info"
| Warn -> "warn"
| Error -> "error"
| Fatal -> "fatal"
/// Turn the LogLevel into an integer
member x.toInt () =
(function
| Verbose -> 1
| Debug -> 2
| Info -> 3
| Warn -> 4
| Error -> 5
| Fatal -> 6) x
interface IComparable<LogLevel> with
member x.CompareTo other = compare (x.toInt()) (other.toInt())
static member op_LessThan (a, b) = (a :> IComparable<LogLevel>).CompareTo(b) < 0
static member op_LessThanOrEqual (a, b) = (a :> IComparable<LogLevel>).CompareTo(b) <= 0
static member op_GreaterThan (a, b) = (a :> IComparable<LogLevel>).CompareTo(b) > 0
static member op_GreaterThanOrEqual (a, b) = (a :> IComparable<LogLevel>).CompareTo(b) >= 0
override x.GetHashCode () = x.toInt ()
interface IComparable with
member x.CompareTo other =
match other with
| null ->
1
| :? LogLevel as tother ->
(x :> IComparable<LogLevel>).CompareTo tother
| _ ->
failwithf "Invalid comparison %A to %A" x other
interface IEquatable<LogLevel> with
member x.Equals other = x.toInt() = other.toInt()
override x.Equals other =
(x :> IComparable).CompareTo other = 0
/// The # of nanoseconds after 1970-01-01 00:00:00.
type EpochNanoSeconds = int64
/// Allows you to clearly deliniate the accuracy and type of the measurement/gauge.
type Value =
/// A CLR Double / F# float represented as a DU case
| Float of float
/// A CLR Int64 / F# int64 represented as a DU case
| Int64 of int64
| BigInt of bigint
| Fraction of int64 * int64
/// Convert the Gauge value to a float (best as possible; this **may** lead to
/// a loss of accuracy).
member x.toFloat () =
match x with
| Float f -> f
| Int64 i -> float i
| BigInt i -> float i
| Fraction (n, d) -> float n / float d
type Units =
// E.g. to denote nano-seconds since epoch;
// 1474139353507070000 would be Scaled(Seconds, 10.**9.) since year 1970
// so to get back to seconds, you'd divide the value by 10.**9.
// E.g. an op that takes 5ms would be represented as
// Gauge(5000000, Scaled(Seconds, 10.**9.)) (ns) OR:
// Gauge(50000, Scaled(Seconds, 10**7.)) (ticks):
| Scaled of unit:Units * value:float
| Seconds
| Scalar
| Other of unit:string
// reflection-workarounds for non-stable field names on different F# versions (???):
member x.scaledFloat = match x with Scaled (_, f) -> f | _ -> 0.
member x.scaledUnit = match x with Scaled (u, _) -> u | _ -> failwith "Only available on Scaled"
member x.otherUnit = match x with Other s -> s | _ -> null
/// Time calculation constants
module Constants =
/// BCL ticks. Not the same as Stopwatch ticks.
[<Literal>]
let SecondsPerTick = 0.0000001
[<Literal>]
let MillisPerTick = 0.0001
[<Literal>]
let MicrosPerTick = 0.1
[<Literal>]
let NanosPerTick = 100L
[<Literal>]
let NanosPerMicro = 1_000L
[<Literal>]
let NanosPerMilli = 1_000_000L
[<Literal>]
let NanosPerSecond = 1_000_000_000L
[<Literal>]
let NanosPerMinute = 60_000_000_000L
[<Literal>]
let MicrosPerSecond = 1_000_000L
[<Literal>]
let MillisPerSecond = 1_000L
/// BCL ticks. Not the same as Stopwatch ticks.
[<Literal>]
let TicksPerMinute = 600_000_000L
/// BCL ticks. Not the same as Stopwatch ticks.
[<Literal>]
let TicksPerSecond = 10_000_000L
/// BCL ticks. Not the same as Stopwatch ticks.
[<Literal>]
let TicksPerMilli = 10000L
/// BCL ticks. Not the same as Stopwatch ticks.
[<Literal>]
let TicksPerMicro = 10L
[<Struct>]
type Gauge =
Gauge of Value * Units
with
member x.value =
let (Gauge (v, _)) = x in v
member x.unit =
let (Gauge (_, u)) = x in u
static member ofNanos (ns: Value) =
Gauge (ns, Scaled (Seconds, float Constants.NanosPerSecond))
static member ofNanos (ns: int64) =
Gauge (Int64 ns, Scaled (Seconds, float Constants.NanosPerSecond))
static member ofNanos (ns: float) =
Gauge (Float ns, Scaled (Seconds, float Constants.NanosPerSecond))
static member ofMillis (ms: Value) =
Gauge (ms, Scaled (Seconds, float Constants.MillisPerSecond))
static member ofMillis (ms: float) =
Gauge (Float ms, Scaled (Seconds, float Constants.MillisPerSecond))
static member ofMillis (ms: int64) =
Gauge (Int64 ms, Scaled (Seconds, float Constants.MillisPerSecond))
static member ofBclTicks (bclTicks: Value) =
Gauge (bclTicks, Scaled (Seconds, float Constants.TicksPerSecond))
static member ofBclTicks (bclTicks: int64) =
Gauge (Int64 bclTicks, Scaled (Seconds, float Constants.TicksPerSecond))
static member ofBclTicks (bclTicks: float) =
Gauge (Float bclTicks, Scaled (Seconds, float Constants.TicksPerSecond))
static member ofStopwatchTicks (swTicks: Value) =
Gauge (swTicks, Scaled (Seconds, float System.Diagnostics.Stopwatch.Frequency))
static member ofStopwatchTicks (swTicks: int64) =
Gauge (Int64 swTicks, Scaled (Seconds, float System.Diagnostics.Stopwatch.Frequency))
static member ofStopwatchTicks (swTicks: float) =
Gauge (Float swTicks, Scaled (Seconds, float System.Diagnostics.Stopwatch.Frequency))
type StacktraceLineData =
{ site: string
file: string option
lineNo: int option }
static member create site file lineNo =
{ site = site; file = file; lineNo = lineNo }
/// http://geekswithblogs.net/BlackRabbitCoder/archive/2012/01/12/c.net-little-pitfalls-stopwatch-ticks-are-not-timespan-ticks.aspx
type StopwatchTicks = int64
/// http://geekswithblogs.net/BlackRabbitCoder/archive/2012/01/12/c.net-little-pitfalls-stopwatch-ticks-are-not-timespan-ticks.aspx
module StopwatchTicks =
open System.Diagnostics
let inline getTimestamp (): StopwatchTicks =
Stopwatch.GetTimestamp()
let ticksPerNanosecond =
// [ tick / s ] / [ ns / s ] => [ tick / ns ]
float Stopwatch.Frequency / 1_000_000_000.
let toNanoseconds (ticks: StopwatchTicks) =
// e.g. 1 tick / 0.01 = 100 ns
int64 (float ticks / ticksPerNanosecond)
let toTimeSpan (ticks: StopwatchTicks) =
TimeSpan.FromTicks ((ticks * TimeSpan.TicksPerSecond) / Stopwatch.Frequency)
[<AutoOpen>]
module DateTimeOffsetEx =
type DateTimeOffset with
/// Get the Logary timestamp off the DateTimeOffset.
member x.toTimestamp(): EpochNanoSeconds =
(x.Ticks - DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks)
* 100L
module DateTimeOffset =
/// Get the DateTimeOffset ticks from EpochNanoSeconds, at offset +0000.
let ofTimestamp (epoch: EpochNanoSeconds): DateTimeOffset =
let ticks = epoch / 100L + DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks
DateTimeOffset(ticks, TimeSpan.Zero)
module Job =
let timeFun onComplete xJ =
Job.delay (fun () ->
let ts = StopwatchTicks.getTimestamp()
xJ >>- fun x ->
let now = StopwatchTicks.getTimestamp()
onComplete (Gauge.ofStopwatchTicks (now - ts))
x)
let timeJob onCompleteJ xJ =
Job.delay (fun () ->
let ts = StopwatchTicks.getTimestamp()
xJ >>= fun x ->
let now = StopwatchTicks.getTimestamp()
onCompleteJ (Gauge.ofStopwatchTicks (now - ts)) >>- fun () ->
x)
module Alt =
let timeFun onComplete onNack (xA: Alt<'a>) =
Alt.withNackJob (fun nack ->
let ts = StopwatchTicks.getTimestamp ()
let markNack =
nack |> Alt.afterFun (fun () ->
let now = StopwatchTicks.getTimestamp ()
onNack (Gauge.ofStopwatchTicks (now - ts)))
let altCommit =
xA |> Alt.afterFun (fun x ->
let now = StopwatchTicks.getTimestamp ()
onComplete (Gauge.ofStopwatchTicks (now - ts))
x)
Job.start markNack >>-. altCommit)
let timeJob onComplete onNack (xA: Alt<'a>) =
Alt.withNackJob (fun nack ->
let ts = StopwatchTicks.getTimestamp ()
let markNack =
nack |> Alt.afterJob (fun () ->
let now = StopwatchTicks.getTimestamp ()
onNack (Gauge.ofStopwatchTicks (now - ts)))
let altCommit =
xA |> Alt.afterJob (fun x ->
let now = StopwatchTicks.getTimestamp ()
onComplete (Gauge.ofStopwatchTicks (now - ts))
>>-. x)
Job.start markNack >>-. altCommit)
type internal LogResult = Alt<Result<Promise<unit>, string>>
module internal Promise =
let unit: Promise<unit> = Promise (())
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module internal LogResult =
let success: LogResult = Alt.always (Result.Ok Promise.unit)
module internal H =
/// Finds all exceptions
let getExns (context: Map<string, obj>): exn list =
context
|> Map.tryFind Literals.ErrorsContextName
|> Option.map (function
| :? list<exn> as exns -> exns |> List.filter (isNull >> not)
| :? list<obj> as exns -> exns |> List.choose (function :? exn as e when not (isNull e) -> Some e | _ -> None)
| _ -> [])
|> Option.defaultValue []
|> List.rev
/// This is record that is logged. It's capable of representing both metrics
/// (gauges) and events. See https://github.com/logary/logary for details.
type Message =
{ /// The 'path' or 'name' of this data point. Do not confuse message template in message.value
name: string[]
/// Event (template or raw message) E.g. "{user} logged in"
value: string
/// Where in the code? Who did the operation? What tenant did the principal
/// who did it belong to? ... context can be anything, you can decide how to deal with them in target
/// through its key.
context: Map<string, obj>
/// How important? See the docs on the LogLevel type for details.
level: LogLevel
/// When? The # of nanoseconds since the UNIX epoch (1970-01-01T00:00:00Z)
timestamp: EpochNanoSeconds }
member x.timestampDateTimeOffset(): DateTimeOffset =
DateTimeOffset.ofTimestamp x.timestamp
member x.getFields(): Map<string, obj> =
x.context
|> Seq.filter (fun (KeyValue (k, _)) -> k.StartsWith Literals.FieldsPrefix)
|> Seq.map (fun (KeyValue (k, v)) -> k.Substring(Literals.FieldsPrefix.Length), v)
|> Map.ofSeq
member x.getContext(): Map<string, obj> =
x.context
|> Map.filter (fun k _ ->
not (k.StartsWith Literals.FieldsPrefix)
&& not (k.StartsWith Literals.LogaryPrefix))
/// If you're looking for how to transform the Message's fields, then use the
/// module methods rather than instance methods, since you'll be creating new
/// values rather than changing an existing value.
member __.README =
()
type Logger =
abstract member name: string[]
abstract member logWithAck: waitForBuffers:bool * level:LogLevel -> (LogLevel -> Message) -> LogResult
module Logger =
/// Log a message, but don't await all targets to flush. Equivalent to logWithBP.
/// Returns whether the message was successfully placed in the buffers.
/// SAFE.
let log (logger: Logger) logLevel messageFactory: Alt<bool> =
logger.logWithAck (false, logLevel) messageFactory ^-> function
| Ok _ ->
true
| Result.Error error ->
false
let private printDotOnOverflow accepted =
if not accepted then System.Console.Error.Write '.' else ()
let logSimple (logger: Logger) msg: unit =
start (log logger msg.level (fun _ -> msg) ^-> printDotOnOverflow)
let logWith (logger: Logger) level messageFactory: unit =
start (log logger level messageFactory ^-> printDotOnOverflow)
let logWithBP (logger: Logger) logLevel messageFactory: Alt<unit> =
logger.logWithAck (true, logLevel) messageFactory ^=> function
| Ok _ ->
Job.result ()
| Result.Error error ->
Job.result ()
/// Special case: e.g. Fatal messages.
let logAck (logger: Logger) level messageFactory: Promise<unit> =
let ack = IVar ()
let inner =
logger.logWithAck (true, level) messageFactory ^=> function
| Ok promise ->
Job.start (promise ^=> IVar.fill ack)
| Result.Error error ->
IVar.fill ack ()
start inner
ack :> Promise<_>
let private ensureName name =
fun (m: Message) ->
if m.name.Length = 0 then { m with name = name } else m
let apply (transform: Message -> Message) (logger: Logger): Logger =
let ensureName = ensureName logger.name
{ new Logger with
member x.logWithAck (waitForBuffers, logLevel) messageFactory =
logger.logWithAck (waitForBuffers, logLevel) (messageFactory >> ensureName >> transform)
member x.name =
logger.name }
type LoggingConfig =
{ timestamp: unit -> int64
getLogger: string[] -> Logger
consoleSemaphore: obj }
module Literate =
type LiterateToken =
| Text
| Subtext
| Punctuation
| LevelVerbose
| LevelDebug
| LevelInfo
| LevelWarning
| LevelError
| LevelFatal
| KeywordSymbol
| NumericSymbol
| StringSymbol
| OtherSymbol
| NameSymbol
| MissingTemplateField
type LiterateOptions =
{ formatProvider: IFormatProvider
theme: LiterateToken -> ConsoleColor
getLogLevelText: LogLevel -> string
printTemplateFieldNames: bool }
static member create ?formatProvider =
// note: literate is meant for human consumption, and so the default
// format provider of 'Current' is appropriate here. The reader expects
// to see the dates, numbers, currency, etc formatted in the local culture
{ formatProvider = defaultArg formatProvider Globalization.CultureInfo.CurrentCulture
getLogLevelText = function
| Debug -> "DBG"
| Error -> "ERR"
| Fatal -> "FTL"
| Info -> "INF"
| Verbose -> "VRB"
| Warn -> "WRN"
theme = function
| Text -> ConsoleColor.White
| Subtext -> ConsoleColor.Gray
| Punctuation -> ConsoleColor.DarkGray
| LevelVerbose -> ConsoleColor.DarkGray
| LevelDebug -> ConsoleColor.Gray
| LevelInfo -> ConsoleColor.White
| LevelWarning -> ConsoleColor.Yellow
| LevelError -> ConsoleColor.Red
| LevelFatal -> ConsoleColor.Red
| KeywordSymbol -> ConsoleColor.Blue
| NumericSymbol -> ConsoleColor.Magenta
| StringSymbol -> ConsoleColor.Cyan
| OtherSymbol -> ConsoleColor.Green
| NameSymbol -> ConsoleColor.Gray
| MissingTemplateField -> ConsoleColor.Red
printTemplateFieldNames = false }
static member createInvariant() =
LiterateOptions.create Globalization.CultureInfo.InvariantCulture
module internal FsMtParser =
open System.Text
type Property(name: string, format: string) =
static let emptyInstance = Property("", null)
static member empty = emptyInstance
member x.name = name
member x.format = format
member internal x.AppendPropertyString(sb: StringBuilder, ?replacementName) =
sb.Append("{")
.Append(defaultArg replacementName name)
.Append(match x.format with null | "" -> "" | _ -> ":" + x.format)
.Append("}")
override x.ToString() = x.AppendPropertyString(StringBuilder()).ToString()
module internal ParserBits =
let inline isNull o =
match o with
| null -> true
| _ -> false
let inline isLetterOrDigit c = System.Char.IsLetterOrDigit c
let inline isValidInPropName c = c = '_' || System.Char.IsLetterOrDigit c
let inline isValidInFormat c = c <> '}' && (c = ' ' || isLetterOrDigit c || System.Char.IsPunctuation c)
let inline isValidCharInPropTag c = c = ':' || isValidInPropName c || isValidInFormat c
[<Struct>]
type Range(startIndex: int, endIndex: int) =
member inline x.start = startIndex
member inline x.``end`` = endIndex
member inline x.length = (endIndex - startIndex) + 1
member inline x.getSubstring (s: string) = s.Substring(startIndex, x.length)
member inline x.isEmpty = startIndex = -1 && endIndex = -1
static member inline substring (s: string, startIndex, endIndex) = s.Substring(startIndex, (endIndex - startIndex) + 1)
static member inline empty = Range(-1, -1)
let inline tryGetFirstCharInRange predicate (s: string) (range: Range) =
let rec go i =
if i > range.``end`` then -1
else if not (predicate s.[i]) then go (i+1) else i
go range.start
let inline tryGetFirstChar predicate (s: string) first =
tryGetFirstCharInRange predicate s (Range(first, s.Length - 1))
let inline hasAnyInRange predicate (s: string) (range: Range) =
match tryGetFirstChar (predicate) s range.start with
| -1 ->
false
| i ->
i <= range.``end``
let inline hasAny predicate (s: string) = hasAnyInRange predicate s (Range(0, s.Length - 1))
let inline indexOfInRange s range c = tryGetFirstCharInRange ((=) c) s range
let inline tryGetPropInRange (template: string) (within: Range): Property =
// Attempts to validate and parse a property token within the specified range inside
// the template string. If the property insides contains any invalid characters,
// then the `Property.Empty' instance is returned (hence the name 'try')
let nameRange, formatRange =
match indexOfInRange template within ':' with
| -1 ->
within, Range.empty // no format
| formatIndex ->
Range(within.start, formatIndex-1), Range(formatIndex+1, within.``end``) // has format part
let propertyName = nameRange.getSubstring template
if propertyName = "" || (hasAny (not<<isValidInPropName) propertyName) then
Property.empty
elif (not formatRange.isEmpty) && (hasAnyInRange (not<<isValidInFormat) template formatRange) then
Property.empty
else
let format = if formatRange.isEmpty then null else formatRange.getSubstring template
Property(propertyName, format)
let findNextNonPropText (startAt: int) (template: string) (foundText: string->unit): int =
// Finds the next text token (starting from the 'startAt' index) and returns the next character
// index within the template string. If the end of the template string is reached, or the start
// of a property token is found (i.e. a single { character), then the 'consumed' text is passed
// to the 'foundText' method, and index of the next character is returned.
let mutable escapedBuilder = Unchecked.defaultof<StringBuilder> // don't create one until it's needed
let inline append (ch: char) = if not (isNull escapedBuilder) then escapedBuilder.Append(ch) |> ignore
let inline createStringBuilderAndPopulate i =
if isNull escapedBuilder then
escapedBuilder <- StringBuilder() // found escaped open-brace, take the slow path
for chIndex = startAt to i-1 do append template.[chIndex] // append all existing chars
let rec go i =
if i >= template.Length then
template.Length // bail out at the end of the string
else
let ch = template.[i]
match ch with
| '{' ->
if (i+1) < template.Length && template.[i+1] = '{' then
createStringBuilderAndPopulate i; append ch; go (i+2)
else i // found an open brace (potentially a property), so bail out
| '}' when (i+1) < template.Length && template.[i+1] = '}' ->
createStringBuilderAndPopulate i; append ch; go (i+2)
| _ ->
append ch; go (i+1)
let nextIndex = go startAt
if (nextIndex > startAt) then // if we 'consumed' any characters, signal that we 'foundText'
if isNull escapedBuilder then
foundText (Range.substring(template, startAt, nextIndex - 1))
else
foundText (escapedBuilder.ToString())
nextIndex
let findPropOrText (start: int) (template: string)
(foundText: string -> unit)
(foundProp: Property -> unit): int =
// Attempts to find the indices of the next property in the template
// string (starting from the 'start' index). Once the start and end of
// the property token is known, it will be further validated (by the
// tryGetPropInRange method). If the range turns out to be invalid, it's
// not a property token, and we return it as text instead. We also need
// to handle some special case here: if the end of the string is reached,
// without finding the close brace (we just signal 'foundText' in that case).
let nextInvalidCharIndex =
match tryGetFirstChar (not << isValidCharInPropTag) template (start+1) with
| -1 ->
template.Length
| idx ->
idx
if nextInvalidCharIndex = template.Length || template.[nextInvalidCharIndex] <> '}' then
foundText (Range.substring(template, start, (nextInvalidCharIndex - 1)))
nextInvalidCharIndex
else
let nextIndex = nextInvalidCharIndex + 1
let propInsidesRng = Range(start + 1, nextIndex - 2)
match tryGetPropInRange template propInsidesRng with
| prop when not (obj.ReferenceEquals(prop, Property.empty)) ->
foundProp prop
| _ ->
foundText (Range.substring(template, start, (nextIndex - 1)))
nextIndex
/// Parses template strings such as "Hello, {PropertyWithFormat:##.##}"
/// and calls the 'foundTextF' or 'foundPropF' functions as the text or
/// property tokens are encountered.
let parseParts (template: string) foundTextF foundPropF =
let tlen = template.Length
let rec go start =
if start >= tlen then () else
match ParserBits.findNextNonPropText start template foundTextF with
| next when next <> start ->
go next
| _ ->
go (ParserBits.findPropOrText start template foundTextF foundPropF)
go 0
/// Internal module for formatting text for printing to the console.
module internal LiterateTokenisation =
open System.Text
open Literals
open Literate
type TokenisedPart =
string * LiterateToken
type LiterateTokeniser =
LiterateOptions -> Message -> TokenisedPart list
type internal TemplateToken =
| TextToken of text:string
| PropToken of name: string * format: string
let internal parseTemplate template =
let tokens = ResizeArray<TemplateToken>()
let foundText (text: string) = tokens.Add (TextToken text)
let foundProp (prop: FsMtParser.Property) = tokens.Add (PropToken (prop.name, prop.format))
FsMtParser.parseParts template foundText foundProp
tokens
/// Chooses the appropriate `LiterateToken` based on the value `Type`.
let tokenForValue (value: obj) =
match value with
| :? bool ->
KeywordSymbol
| :? int16 | :? int32 | :? int64 | :? decimal | :? float | :? single ->
NumericSymbol
| :? string | :? char ->
StringSymbol
| _ ->
OtherSymbol
let tokeniseValue (options: LiterateOptions) (fields: Map<string, obj>) (context: Map<string, obj>) (template: string) =
let themedParts = ResizeArray<TokenisedPart>()
let matchedFields = ResizeArray<string>()
let foundText (text: string) = themedParts.Add (text, Text)
let foundProp (prop: FsMtParser.Property) =
match fields |> Map.tryFind prop.name |> Option.orElseWith (fun () -> context |> Map.tryFind prop.name) with
| Some propValue ->
// render using string.Format, so the formatting is applied
let stringFormatTemplate = prop.AppendPropertyString(StringBuilder(), "0").ToString()
let fieldAsText = String.Format (options.formatProvider, stringFormatTemplate, [| propValue |])
let valueTokenType = tokenForValue propValue
if options.printTemplateFieldNames then
themedParts.Add ("["+prop.name+"] ", Subtext)
matchedFields.Add prop.name
themedParts.Add (fieldAsText, valueTokenType)
| None ->
themedParts.Add (prop.ToString(), MissingTemplateField)
FsMtParser.parseParts template foundText foundProp
Set.ofSeq matchedFields, (themedParts :> TokenisedPart seq)
let tokeniseExn (options: LiterateOptions) (ex: exn) =
let stackFrameLinePrefix = " at" // 3 spaces
let monoStackFrameLinePrefix = " at" // 2 spaces
use exnLines = new System.IO.StringReader(ex.ToString())
let rec go lines =
match exnLines.ReadLine() with
| null ->
List.rev lines // finished reading
| line ->
if line.StartsWith(stackFrameLinePrefix) || line.StartsWith(monoStackFrameLinePrefix) then
// subtext
go ((line, Subtext) :: (Environment.NewLine, Text) :: lines)
else
// regular text
go ((line, Text) :: (Environment.NewLine, Text) :: lines)
go []
let tokeniseExns (options: LiterateOptions) message =
H.getExns message.context
|> List.collect (tokeniseExn options)
let tokeniseLogLevel = function
| Verbose -> LevelVerbose
| Debug -> LevelDebug
| Info -> LevelInfo
| Warn -> LevelWarning
| Error -> LevelError
| Fatal -> LevelFatal
/// Split a structured message up into theme-able parts (tokens), allowing the
/// final output to display to a user with colours to enhance readability.
let tokeniseMessage (options: LiterateOptions) (message: Message): TokenisedPart list =
let formatLocalTime (epoch: EpochNanoSeconds) =
DateTimeOffset
.ofTimestamp(epoch)
.LocalDateTime
.ToString("HH:mm:ss", options.formatProvider),
Subtext
let fields, context = message.getFields(), message.getContext()
let _, themedMessageParts = message.value |> tokeniseValue options fields context
let themedExceptionParts = tokeniseExns options message
[ yield "[", Punctuation
yield formatLocalTime message.timestamp
yield " ", Subtext
yield options.getLogLevelText message.level, tokeniseLogLevel message.level
yield "] ", Punctuation
yield! themedMessageParts
if not (isNull message.name) && message.name.Length > 0 then
yield " ", Subtext
yield "<", Punctuation
yield String.concat "." message.name, Subtext
yield ">", Punctuation
yield! themedExceptionParts
]
module internal Formatting =
open Literate
open System.Text
let formatValue (fields: Map<string, obj>) (context: Map<string, obj>) value =
let matchedFields, themedParts =
LiterateTokenisation.tokeniseValue (LiterateOptions.createInvariant()) fields context value
matchedFields, System.String.Concat(themedParts |> Seq.map fst)
let formatLevel (level: LogLevel) =
"[" + Char.ToUpperInvariant(level.ToString().[0]).ToString() + "] "
let formatInstant (ts: DateTimeOffset) =
(ts.ToString("o")) + ": "
let formatName (name: string[]) =
" [" + String.concat "." name + "]"
let formatExn (e: exn) =
" exn:\n" + e.ToString()
let formatExns =
H.getExns
>> List.map formatExn
>> String.concat "\n"
let formatFields (ignored: Set<string>) (fields: Map<string, obj>) =
if not (Map.isEmpty fields) then
fields
|> Seq.filter (fun (KeyValue (k, _)) ->
not (ignored |> Set.contains k))
|> Seq.map (fun (KeyValue (k, v)) ->
sprintf "\n - %s: %O" k v)
|> String.concat ""
else
""
/// let the ISO8601 love flow
let defaultFormatter (message: Message) =
let fields, context = message.getFields(), message.getContext()
let matchedFields, valueString = formatValue fields context message.value
// [I] 2014-04-05T12:34:56Z: Hello World! [my.sample.app]
formatLevel message.level +
formatInstant (message.timestampDateTimeOffset()) +
valueString +
formatName message.name +
formatExns message.context +
formatFields matchedFields fields
/// Assists with controlling the output of the `LiterateConsoleTarget`.
module internal LiterateFormatting =
open Literate
open LiterateTokenisation
open System.Text
type ColouredTextPart = string * ConsoleColor
let literateDefaultColourWriter sem (parts: ColouredTextPart list) =
lock sem <| fun _ ->
let originalColour = Console.ForegroundColor
let mutable currentColour = originalColour
parts |> List.iter (fun (text, colour) ->
if currentColour <> colour then
Console.ForegroundColor <- colour
currentColour <- colour
Console.Write(text)
)
if currentColour <> originalColour then
Console.ForegroundColor <- originalColour
[<AutoOpen>]
module OutputTemplateTokenisers =
open System.Collections.Generic
let tokeniseExtraField (options: LiterateOptions) (message: Message) (field: KeyValuePair<string, obj>) =
seq {
yield " - ", Punctuation
yield field.Key, NameSymbol
yield ": ", Punctuation
yield System.String.Format(options.formatProvider, "{0}", field.Value), tokenForValue field.Value
}
let tokeniseExtraFields (options: LiterateOptions) (message: Message) (templateFieldNames: Set<string>) =
let extraFields = message.getFields() |> Map.filter (fun key _ -> not (templateFieldNames.Contains key))
let mutable isFirst = true
seq {
for field in extraFields do
if isFirst then isFirst <- false
else yield Environment.NewLine, Text
yield! tokeniseExtraField options message field
}
let tokeniseTimestamp format (options: LiterateOptions) (message: Message) =
let formattedTimestamp =
message
.timestampDateTimeOffset()
.ToLocalTime()
.ToString(format, options.formatProvider)
[ formattedTimestamp, Subtext ] :> seq<_>
let tokeniseTimestampUtc format (options: LiterateOptions) (message: Message) =
let formattedTimestamp =
message.timestampDateTimeOffset().ToString(format, options.formatProvider)
[ formattedTimestamp, Subtext ] :> seq<_>
let tokeniseMissingField name format =
seq {
yield "{", Punctuation
yield name, MissingTemplateField
if not (String.IsNullOrEmpty format) then
yield ":", Punctuation
yield format, Subtext
yield "}", Punctuation }
let tokeniseLogLevel (options: LiterateOptions) (message: Message) =
seq { yield options.getLogLevelText message.level,
LiterateTokenisation.tokeniseLogLevel message.level }
let tokeniseSource (options: LiterateOptions) (message: Message) =
seq { yield (String.concat "." message.name), Subtext }
let tokeniseNewline (options: LiterateOptions) (message: Message) =
seq { yield Environment.NewLine, Text }
let tokeniseTab (options: LiterateOptions) (message: Message) =
seq { yield "\t", Text }
/// Creates a `LiterateTokeniser` function which can be passed to the `LiterateConsoleTarget`
/// constructor in order to customise how each log message is rendered. The default template
/// would be: `[{timestampLocal:HH:mm:ss} {level}] {message}{newline}{exceptions}`.
/// Available template fields are: `timestamp`, `timestampUtc`, `level`, `source`,
/// `newline`, `tab`, `message`, `exceptions`. Any misspelled or otheriwese invalid property
/// names will be treated as `LiterateToken.MissingTemplateField`.
let tokeniserForOutputTemplate template: LiterateTokeniser =
let tokens = parseTemplate template
fun options message ->
let fields, context = message.getFields(), message.getContext()
// render the message template first so we have the template-matched fields available
let matchedFields, messageParts =
tokeniseValue options fields context message.value
let tokeniseOutputTemplateField fieldName format = seq {
match fieldName with
| "timestamp" -> yield! tokeniseTimestamp format options message
| "timestampUtc" -> yield! tokeniseTimestampUtc format options message
| "level" -> yield! tokeniseLogLevel options message
| "source" -> yield! tokeniseSource options message
| "newline" -> yield! tokeniseNewline options message
| "tab" -> yield! tokeniseTab options message
| "message" -> yield! messageParts
| "properties" -> yield! tokeniseExtraFields options message matchedFields
| "exceptions" -> yield! tokeniseExns options message
| _ -> yield! tokeniseMissingField fieldName format
}
seq {
let lastTokenIndex = tokens.Count - 1
let mutable nextPartsArray: TokenisedPart[] = null
for index in [0..lastTokenIndex] do
let token = tokens.[index]
match token with
| TextToken text -> yield text, LiterateToken.Punctuation
| PropToken (name, format) ->
if index <> lastTokenIndex && name = "newLineIfNext" then
match tokens.[index + 1] with
| PropToken (nextName, nextFormat) ->
// Tokenise the next property now, to determine if it's 'empty'. To avoid doing
// unnecessary work, we save these tokens ('nextPartsArray') so it can be
// 'yield'ed on the next iteration.
nextPartsArray <- tokeniseOutputTemplateField nextName nextFormat |> Seq.toArray
if nextPartsArray.Length > 0 then
yield! tokeniseNewline options message
| _ ->
// It's questionable what to do here. It was an invalid output template,
// because the {newLineIfNext} should only appear immediately prior to some other
// valid output field. We could `failwith "invalid output template"`?
()
else
if not (isNull nextPartsArray) then
yield! nextPartsArray
nextPartsArray <- null
else
yield! tokeniseOutputTemplateField name format
}
|> Seq.toList
/// Logs a line in a format that is great for human consumption,
/// using console colours to enhance readability.
/// Sample: [10:30:49 INF] User "AdamC" began the "checkout" process with 100 cart items
type LiterateConsoleTarget(name, minLevel, ?options, ?literateTokeniser, ?outputWriter, ?consoleSemaphore) =
let sem = defaultArg consoleSemaphore (obj())
let options = defaultArg options (Literate.LiterateOptions.create())
let tokenise = defaultArg literateTokeniser LiterateTokenisation.tokeniseMessage
let colourWriter = defaultArg outputWriter LiterateFormatting.literateDefaultColourWriter sem
let colouriseThenNewLine message =
(tokenise options message) @ [Environment.NewLine, Literate.Text]
|> List.map (fun (s, t) -> s, options.theme(t))
/// Creates the target with a custom output template. The default `outputTemplate`
/// is `[{timestampLocal:HH:mm:ss} {level}] {message}{exceptions}`.
/// Available template fields are: `timestamp`, `timestampUtc`, `level`, `source`,
/// `newline`, `tab`, `message`, `exceptions`. Any misspelled or otheriwese invalid property
/// names will be treated as `LiterateToken.MissingTemplateField`.
new (name, minLevel, outputTemplate, ?options, ?outputWriter, ?consoleSemaphore) =
let tokeniser = LiterateFormatting.tokeniserForOutputTemplate outputTemplate
LiterateConsoleTarget(name, minLevel, ?options=options, literateTokeniser=tokeniser, ?outputWriter=outputWriter, ?consoleSemaphore=consoleSemaphore)
interface Logger with
member __.name = name
member __.logWithAck (wfb, level) msgFactory =
if level >= minLevel then
Alt.prepareFun (fun () ->
colourWriter (colouriseThenNewLine (msgFactory level))
LogResult.success)
else
LogResult.success
type TextWriterTarget(name, minLevel, writer: System.IO.TextWriter, ?formatter) =
let formatter = defaultArg formatter Formatting.defaultFormatter
let log msg = writer.WriteLine(formatter msg)
interface Logger with
member __.name = name
member __.logWithAck (wfb, level) messageFactory =
if level >= minLevel then
Alt.prepareFun (fun () ->
log (messageFactory level)
LogResult.success)
else
LogResult.success
type OutputWindowTarget(name, minLevel, ?formatter) =
let formatter = defaultArg formatter Formatting.defaultFormatter
let log msg = System.Diagnostics.Debug.WriteLine(formatter msg)
interface Logger with
member __.name = name
member __.logWithAck (wfb, level) messageFactory =
if level >= minLevel then
Alt.prepareFun (fun () ->
log (messageFactory level)
LogResult.success)
else
LogResult.success
module Global =
/// This is the global semaphore for colourising the console output. Ensure
/// that the same semaphore is used across libraries by using the Logary
/// Facade Adapter in the final composing app/service.
let private consoleSemaphore = obj ()
/// The global default configuration, which logs to Console at Info level.
let defaultConfig =
{ timestamp = fun () -> DateTimeOffset.UtcNow.toTimestamp()
getLogger = fun name -> LiterateConsoleTarget(name, Debug) :> Logger
consoleSemaphore = consoleSemaphore }
let private config =
ref (defaultConfig, (* logical clock *) 1u)