-
-
Notifications
You must be signed in to change notification settings - Fork 96
/
Logging.fs
1432 lines (1232 loc) · 56.1 KB
/
Logging.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.
///
/// This file is licensed under the Apache 2.0 license without modifications.
/// This license applies to v3 of the Logary Facade. You can copy and paste this
/// code into your software, which freezes this license in place.
///
/// Original Source:
/// https://github.com/logary/logary/blob/996bdf92713f406b17c6cd7284e4d674f49e3ff6/src/Logary.Facade/Facade.fs
///
namespace Expecto.Logging
#nowarn "9"
open System
open System.Text
type ColourLevel =
| Colour0
| Colour8
| Colour256
/// 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"
/// Converts the string passed to a Loglevel.
static member ofString (str: string) =
if str = null then invalidArg "str" "may not be null"
match str.ToLowerInvariant() with
| "verbose" -> Verbose
| "debug" -> Debug
| "info" -> Info
| "warn" -> Warn
| "error" -> Error
| "fatal" -> Fatal
| _ -> Info
/// Turn the LogLevel into an integer
member x.toInt () =
(function
| Verbose -> 1
| Debug -> 2
| Info -> 3
| Warn -> 4
| Error -> 5
| Fatal -> 6) x
/// Turn an integer into a LogLevel
static member ofInt i =
(function
| 1 -> Verbose
| 2 -> Debug
| 3 -> Info
| 4 -> Warn
| 5 -> Error
| 6 -> Fatal
| i -> failwithf "LogLevel matching integer %i is not available" i) i
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
/// Represents a logged value; either a Gauge or an Event.
type PointValue =
/// An event is what it sounds like; something occurred and needs to be
/// logged. Its field is named 'template' because it should not be interpolated
/// with values; instead these values should be put in the 'fields' field of
/// the Message.
| Event of template:string
/// This is as value for a metric, with a unit attached. The unit can be
/// something like Seconds or Hz.
| Gauge of value:float * units:string
/// The # of nanoseconds after 1970-01-01 00:00:00.
type EpochNanoSeconds = int64
/// Helper functions for transforming DateTime to timestamps in unix epoch.
module DateTime =
/// Get the Logary timestamp off the DateTime.
let timestamp (dt: DateTime): EpochNanoSeconds =
(dt.Ticks - DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks)
* 100L
/// Get the DateTimeOffset ticks off from the EpochNanoSeconds.
let ticksUTC (epoch: EpochNanoSeconds): int64 =
epoch / 100L
+ DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks
/// Helper functions for transforming DateTimeOffset to timestamps in unix epoch.
module DateTimeOffset =
/// Get the Logary timestamp off the DateTimeOffset.
let timestamp (dt: DateTimeOffset): EpochNanoSeconds =
(dt.Ticks - DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks)
* 100L
/// Get the DateTimeOffset ticks from EpochNanoSeconds
let ticksUTC (epoch: EpochNanoSeconds): int64 =
epoch / 100L
+ DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks
/// 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 template in
/// (Event template) = message.value
name: string[]
/// The main value for this metric or event. Either a Gauge or an Event. (A
/// discriminated union type)
value: PointValue
/// The structured-logging data.
fields: Map<string, obj>
/// When? nanoseconds since UNIX epoch.
timestamp: EpochNanoSeconds
/// How important? See the docs on the LogLevel type for details.
level: LogLevel }
/// Gets the ticks for UTC since 0001-01-01 00:00:00 for this message. You
/// can pass this value into a DateTimeOffset c'tor
member x.utcTicks =
DateTimeOffset.ticksUTC x.timestamp
/// 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 x.README =
()
/// The logger is the interface for calling code to use for logging. Its
/// different functions have different semantics - read the docs for each
/// method to choose the right one for your use-case.
type Logger =
/// Gets the name of the logger instance.
abstract member name: string[]
/// Logs with the specified log level with backpressure via the logging
/// library's buffers *and* ACK/flush to the underlying message targets.
///
/// Calls to this function will block the caller only while executing the
/// callback (if the level is active).
///
/// The returned async value will yield when the message has been flushed to
/// the underlying message targets.
///
/// You need to start the (cold) async value for the logging to happen.
///
/// You should not do blocking/heavy operations in the callback.
abstract member logWithAck: LogLevel -> (LogLevel -> Message) -> Async<unit>
/// Logs with the specified log level with backpressure via the logging
/// library's buffers.
///
/// Calls to this function will block the caller only while executing the
/// callback (if the level is active).
///
/// The returned async value will yield when the message has been added to
/// the buffers of the logging library.
///
/// You need to start the (cold) async value for the logging to happen.
///
/// You should not do blocking/heavy operations in the callback.
abstract member log: LogLevel -> (LogLevel -> Message) -> Async<unit>
/// Syntactic sugar on top of Logger for F# libraries.
[<AutoOpen>]
module LoggerEx =
let private logWithTimeout (logger: Logger) level messageFactory =
async {
let! child = Async.StartChild (logger.log level messageFactory, 5000)
try
do! child
with
| :? TimeoutException ->
Console.Error.WriteLine(
"Logary (facade) message timed out. This means that you have an underperforming target. (Reevaluated) message name '{0}'.",
String.concat "." (messageFactory level).name)
}
type Logger with
member x.verbose (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Verbose messageFactory |> Async.Start
/// Log with backpressure
member x.verboseWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Verbose messageFactory
member x.debug (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Debug messageFactory |> Async.Start
/// Log with backpressure
member x.debugWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Debug messageFactory
member x.info (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Info messageFactory |> Async.Start
/// Log with backpressure
member x.infoWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Info messageFactory
member x.warn (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Warn messageFactory |> Async.Start
/// Log with backpressure
member x.warnWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Warn messageFactory
member x.error (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Error messageFactory |> Async.Start
/// Log with backpressure
member x.errorWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Error messageFactory
member x.fatal (messageFactory: LogLevel -> Message): unit =
logWithTimeout x Fatal messageFactory |> Async.Start
/// Log with backpressure
member x.fatalWithBP (messageFactory: LogLevel -> Message): Async<unit> =
x.log Fatal messageFactory
/// Log a message, but don't synchronously wait for the message to be placed
/// inside the logging library's buffers. Instead the message will be added
/// to the logging library's buffers asynchronously (with respect to the
/// caller) with a timeout of 5 seconds, and will then be dropped.
///
/// This is the way we avoid the unbounded buffer problem.
///
/// If you have dropped messages, they will be logged to STDERR. You should load-
/// test your app to ensure that your targets can send at a rate high enough
/// without dropping messages.
///
/// It's recommended to have alerting on STDERR.
member x.logSimple message: unit =
logWithTimeout x message.level (fun _ -> message) |> Async.Start
type LoggingConfig =
{ timestamp: unit -> int64
getLogger: string[] -> Logger
consoleSemaphore: obj
}
static member create u2ts n2l sem = {
timestamp = u2ts
getLogger = n2l
consoleSemaphore = sem
}
module Literate =
/// The output tokens, which can be potentially coloured.
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 that contains the 'known' keys of the Maps in the Message type's
/// fields/runtime data.
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 = 3u
/// What language this Facade has. This controls things like naming standards.
let FacadeLanguage = "F#"
[<Literal>]
let FieldExnKey = "exn"
[<Literal>]
let FieldErrorsKey = "errors"
type DVar<'a> = private { mutable cell: 'a; event: Event<'a>; mutable changed: bool }
module DVar =
open System.Threading
let create (x: 'x) = { cell = x; event = new Event<'x>(); changed = false }
let get (xD: DVar<'x>) = xD.cell
let wasChanged (xD: DVar<'x>) = xD.changed
let put (x: 'x) (xD: DVar<'x>) =
let prevX = Interlocked.Exchange (&xD.cell, x) // last writer wins
xD.changed <- true // monotonically reaches true, hence thread safe
xD.event.Trigger x
let changes (xD: DVar<'x>) = xD.event.Publish
let subs (xD: DVar<'x>) (x2u: 'x -> unit) = xD |> changes |> Event.add x2u
let apply (a2bD: DVar<'a -> 'b>) (aD: DVar<'a>): DVar<'b> =
let b = (get a2bD) (get aD)
let bD = create b
subs a2bD <| fun a2b -> let b = a2b (get aD) in put b bD
subs aD <| fun a -> let f = get a2bD in let b = f a in put b bD
bD
let map (x2y: 'x -> 'y) (xD: DVar<'x>): DVar<'y> = let yD = create (x2y (get xD)) in subs xD (x2y >> fun x -> put x yD); yD
let bindToRef (xR: 'x ref) (xD: DVar<'x>) = xR := get xD; subs xD (fun a -> xR := a)
module Operators =
let (<!>) = map
let (<*>) = apply
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 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
module internal MessageTemplates =
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
/// Internal module for converting message parts into theme-able tokens.
module internal LiterateTokenisation =
open System.Text
open Literals
open Literate
/// A piece of text with an associated `LiterateToken`.
type TokenisedPart = string * LiterateToken
/// Converts some part(s) of a `Message` into text with an associated `LiterateToken`.
type LiterateTokeniser = LiterateOptions -> Message -> TokenisedPart list
/// Chooses the appropriate `LiterateToken` based on the value `Type`.
let getTokenTypeForValue (value: obj) =
match value with
| :? bool -> KeywordSymbol
| :? int16 | :? int32 | :? int64 | :? decimal | :? float | :? double -> NumericSymbol
| :? string | :? char -> StringSymbol
| _ -> OtherSymbol
/// Converts a `PointValue` into a sequence literate tokens. The returned `Set<string>` contains
/// the names of the properties that were found in the `Event` template.
let tokenisePointValue (options: LiterateOptions) (fields: Map<string, obj>) = function
| Event template ->
let themedParts = ResizeArray<TokenisedPart>()
let matchedFields = ResizeArray<string>()
let foundText (text: string) = themedParts.Add (text, Text)
let foundProp (prop: FsMtParser.Property) =
match Map.tryFind prop.name fields 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 = getTokenTypeForValue 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)
| Gauge (value, units) ->
Set.empty, ([| sprintf "%f" value, NumericSymbol
sprintf "%s" units, KeywordSymbol |] :> TokenisedPart seq)
/// Converts a single exception into a sequence of literate tokens.
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: TokenisedPart list) =
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 []
/// Converts all exceptions in a `Message` into a sequence of literate tokens.
let tokeniseMessageExns (context: LiterateOptions) message =
let exnExceptionParts =
match message.fields.TryFind FieldExnKey with
| Some (:? Exception as ex) ->
tokeniseExn context ex
| _ ->
[] // there is no spoon
let errorsExceptionParts =
match message.fields.TryFind FieldErrorsKey with
| Some (:? List<obj> as exnListAsObjList) ->
exnListAsObjList |> List.collect (function
| :? exn as ex ->
tokeniseExn context ex
| _ ->
[])
| _ ->
[]
exnExceptionParts @ errorsExceptionParts
let tokeniseLogLevel = function
| Verbose -> LevelVerbose
| Debug -> LevelDebug
| Info -> LevelInfo
| Warn -> LevelWarning
| Error -> LevelError
| Fatal -> LevelFatal
/// Converts a `Message` into sequence of literate tokens.
let tokeniseMessage (options: LiterateOptions) (message: Message): TokenisedPart list =
let formatLocalTime (utcTicks: int64) =
DateTimeOffset(utcTicks, TimeSpan.Zero).LocalDateTime.ToString("HH:mm:ss", options.formatProvider),
Subtext
let _, themedMessageParts =
message.value |> tokenisePointValue options message.fields
let themedExceptionParts = tokeniseMessageExns options message
[ yield "[", Punctuation
yield formatLocalTime message.utcTicks
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
]
/// Internal module for formatting text for printing to the console.
module internal Formatting =
open Literals
open Literate
open LiterateTokenisation
let formatValue (fields: Map<string, obj>) (pv: PointValue) =
let matchedFields, themedParts =
tokenisePointValue (LiterateOptions.createInvariant()) fields pv
matchedFields, System.String.Concat(themedParts |> Seq.map fst)
/// let the ISO8601 love flow
let defaultFormatter (message: Message) =
let app (x: obj) (sb: StringBuilder) =
sb.Append x |> ignore
let formatLevel (level: LogLevel) =
"[" + Char.ToUpperInvariant(level.ToString().[0]).ToString() + "] "
let formatInstant (utcTicks: int64) =
(DateTimeOffset(utcTicks, TimeSpan.Zero).ToString("o")) + ": "
let formatName (name: string[]) =
" [" + String.concat "." name + "]"
let formatExn (fields: Map<string, obj>) =
match fields |> Map.tryFind FieldExnKey with
| None ->
String.Empty
| Some ex ->
" exn:\n" + ex.ToString()
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 matchedFields, valueString =
formatValue message.fields message.value
// [I] 2014-04-05T12:34:56Z: Hello World! [my.sample.app]
formatLevel message.level +
formatInstant message.utcTicks +
valueString +
formatName message.name +
formatExn message.fields +
formatFields matchedFields message.fields
/// Assists with controlling the output of the `LiterateConsoleTarget`.
module internal LiterateFormatting =
open Literate
open LiterateTokenisation
open MessageTemplates
type ColouredTextPart = string * ConsoleColor
/// Writes string*ConsoleColor parts atomically (in a `lock`)
let atomicallyWriteColouredTextToConsole 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 exceptionFieldNames = set [ Literals.FieldExnKey; Literals.FieldErrorsKey ]
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), getTokenTypeForValue field.Value
}
let tokeniseExtraFields (options: LiterateOptions) (message: Message) (templateFieldNames: Set<string>) =
let fieldsToExclude = Set.union templateFieldNames exceptionFieldNames
let extraFields = message.fields |> Map.filter (fun key _ -> not (fieldsToExclude.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 localDateTimeOffset = DateTimeOffset(message.utcTicks, TimeSpan.Zero).ToLocalTime()
let formattedTimestamp = localDateTimeOffset.ToString(format, options.formatProvider)
seq { yield formattedTimestamp, Subtext }
let tokeniseTimestampUtc format (options: LiterateOptions) (message: Message) =
let utcDateTimeOffset = DateTimeOffset(message.utcTicks, TimeSpan.Zero)
let formattedTimestamp = utcDateTimeOffset.ToString(format, options.formatProvider)
seq { yield formattedTimestamp, Subtext }
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, 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 used by the `LiterateConsoleTarget`
/// to customise how each log message is rendered. The default output template
/// would be: `[{timestamp:HH:mm:ss} {level}] {message}{newline}{exceptions}`.
/// Available template fields are: `timestamp`, `timestampUtc`, `level`, `source`,
/// `newline`, `tab`, `message`, `properties`, `exceptions`. A special field named
/// `newLineIfNext` will output a new line if the next field in the template will render
/// anything (i.e. non-empty). Any misspelled or different property names will become a
/// `LiterateToken.MissingTemplateField`.
let tokeniserForOutputTemplate template: LiterateTokeniser =
let tokens = parseTemplate template
fun options message ->
// render the message template first so we have the template-matched fields available
let fieldsInMessageTemplate, messageParts =
tokenisePointValue options message.fields 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 fieldsInMessageTemplate
| "exceptions" -> yield! tokeniseMessageExns 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
module internal ANSIOutputWriter =
open System.IO
open System.Runtime.InteropServices
type private FuncTextWriter(encoding: Encoding, write: string -> unit) =
inherit TextWriter()
override __.Encoding = encoding
override __.Write (s:string) = write s
override __.WriteLine (s:string) = s + "\n" |> write
override __.WriteLine() = write "\n"
let mutable internal colours = None
let internal setColourLevel c = if colours.IsNone then colours <- Some c
let internal getColour() = Option.defaultValue Colour8 colours
let colourReset = "\u001b[0m"
let private colour8BlackBG = function
| ConsoleColor.Black -> "\u001b[30,1m"
| ConsoleColor.DarkRed
| ConsoleColor.Red -> "\u001b[31m"
| ConsoleColor.DarkGreen
| ConsoleColor.Green -> "\u001b[32m"
| ConsoleColor.DarkYellow
| ConsoleColor.Yellow -> "\u001b[33m"
| ConsoleColor.DarkBlue
| ConsoleColor.Blue -> "\u001b[34m"
| ConsoleColor.DarkMagenta
| ConsoleColor.Magenta -> "\u001b[35m"
| ConsoleColor.DarkCyan
| ConsoleColor.Cyan -> "\u001b[36m"
| ConsoleColor.DarkGray
| ConsoleColor.Gray
| ConsoleColor.White -> "\u001b[37m"
| _ -> ""
let private colour8WhiteBG = function
| ConsoleColor.Black -> "\u001b[30m"
| ConsoleColor.DarkRed
| ConsoleColor.Red -> "\u001b[31m"
| ConsoleColor.DarkGreen
| ConsoleColor.Green -> "\u001b[32m"
| ConsoleColor.DarkYellow
| ConsoleColor.Yellow -> "\u001b[33m"
| ConsoleColor.DarkBlue
| ConsoleColor.Blue -> "\u001b[34m"
| ConsoleColor.DarkMagenta
| ConsoleColor.Magenta -> "\u001b[35m"
| ConsoleColor.DarkCyan
| ConsoleColor.Cyan -> "\u001b[36m"
| ConsoleColor.DarkGray
| ConsoleColor.Gray
| ConsoleColor.White -> "\u001b[30,1m"
| _ -> ""
let private colour256BlackBG =
sprintf "\u001b[%sm" << function
| ConsoleColor.Black -> "38;5;232"
| ConsoleColor.DarkGray -> "38;5;234"
| ConsoleColor.Gray -> "38;5;245"
| ConsoleColor.DarkRed -> "38;5;52"
| ConsoleColor.Red -> "38;5;1"
| ConsoleColor.DarkGreen -> "38;5;28"
| ConsoleColor.Green -> "38;5;40"
| ConsoleColor.DarkYellow -> "38;5;220"
| ConsoleColor.Yellow -> "38;5;11"
| ConsoleColor.DarkBlue -> "38;5;18"
| ConsoleColor.Blue -> "38;5;26"
| ConsoleColor.DarkMagenta -> "38;5;55"
| ConsoleColor.Magenta -> "38;5;165"
| ConsoleColor.DarkCyan -> "38;5;31"
| ConsoleColor.Cyan -> "38;5;39"
| ConsoleColor.White -> "38;5;255"
| _ -> ""
let private colour256WhiteBG =
sprintf "\u001b[%sm" << function
| ConsoleColor.Black -> "38;5;255"
| ConsoleColor.DarkGray -> "38;5;251"
| ConsoleColor.Gray -> "38;5;245"
| ConsoleColor.DarkRed -> "38;5;204"
| ConsoleColor.Red -> "38;5;1"
| ConsoleColor.DarkGreen -> "38;5;120"
| ConsoleColor.Green -> "38;5;40"
| ConsoleColor.DarkYellow -> "38;5;229"
| ConsoleColor.Yellow -> "38;5;11"
| ConsoleColor.DarkBlue -> "38;5;12"
| ConsoleColor.Blue -> "38;5;26"
| ConsoleColor.DarkMagenta -> "38;5;219"
| ConsoleColor.Magenta -> "38;5;165"
| ConsoleColor.DarkCyan -> "38;5;159"
| ConsoleColor.Cyan -> "38;5;39"
| ConsoleColor.White -> "38;5;232"
| _ -> ""
let private isBlackBG = Console.BackgroundColor = ConsoleColor.Black
|| int Console.BackgroundColor = -1
let colourText colourLevel colour =
match colourLevel with
| Colour0 -> String.Empty
| Colour8 -> if isBlackBG then colour8BlackBG colour else colour8WhiteBG colour
| Colour256 -> if isBlackBG then colour256BlackBG colour else colour256WhiteBG colour
let private foregroundColor = Console.ForegroundColor
module private WindowsConsole =
open Microsoft.FSharp.NativeInterop
[<DllImport("Kernel32")>]
extern void* private GetStdHandle(int _nStdHandle)
[<DllImport("Kernel32")>]
extern bool private GetConsoleMode(void* _hConsoleHandle, int* _lpMode)
[<DllImport("Kernel32")>]
extern bool private SetConsoleMode(void* _hConsoleHandle, int _lpMode)
/// https://superuser.com/questions/413073/windows-console-with-ansi-colors-handling
let enableVTMode () =
#if NETSTANDARD2_0
if not (RuntimeInformation.IsOSPlatform OSPlatform.Windows) then () else
#else
if not (Environment.OSVersion.Platform = PlatformID.Win32NT) then () else
#endif
let INVALID_HANDLE_VALUE = nativeint -1
let STD_OUTPUT_HANDLE = -11
let ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
let handle = GetStdHandle(STD_OUTPUT_HANDLE)
if handle <> INVALID_HANDLE_VALUE then
let mode = NativePtr.stackalloc<int> 1