Last active
June 21, 2023 18:23
-
-
Save Horusiath/6f4f7f0efcab9607fbad587d20ec4968 to your computer and use it in GitHub Desktop.
Fun with concepts about composeable logging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open System | |
open System.IO | |
open System.Threading.Tasks | |
(* | |
Some notes: | |
1. TextWriter is overbloated. Ideally this should be something like Go's io.Writer interface. Notice that Go doesn't | |
differentiate between async/non-async functions (it doesn't have to), which makes API even simpler. | |
2. While very rough, this snippet already provides a lot of what mature logging frameworks (like Serilog) can do: | |
- Every log level can point to different sinks | |
- TextWriters can be combined via `Writer.combine`, so output can be send to multiple sinks. | |
- Support for different log levels - if you don't want to log DEBUG messages, use `TextWriter.Null`. | |
- Buffering can be also done as `TextWriter`. | |
- TextWriter (sink) can be anything: StringBuilder (eg. for testing), Console, File, NetworkStream, | |
possibly remote http service - most of which are supported out-of-the box by the standard library. | |
- Some libraries (like Newtonsoft.Json) can serialize their content directly to TextWriters, so you can provide | |
structured output without any adapter packages. | |
*) | |
[<Interface>] | |
type ILogger = | |
abstract Debug: TextWriter | |
abstract Info: TextWriter | |
abstract Warn: TextWriter | |
abstract Error: TextWriter | |
module Writer = | |
/// Returns `TextWriter` that will simultaneously write to both provided writers. | |
let combine (w1: TextWriter) (w2: TextWriter) = | |
{ new TextWriter() with | |
member _.Encoding = w1.Encoding | |
override _.WriteLine(s: string) = w1.WriteLine(s); w2.WriteLine(s) | |
override _.WriteLineAsync(s: string) = Task.WhenAll(w1.WriteLineAsync(s), w2.WriteLineAsync(s)) } | |
/// Returns `TextWriter`, which will map incoming log line producing new one and passing it to wrapped `writer`. | |
let map (f: string -> string) (writer: TextWriter) = | |
{ new TextWriter() with | |
member _.Encoding = writer.Encoding | |
override _.WriteLine(s: string) = writer.WriteLine(f s) | |
override _.WriteLineAsync(s: string) = writer.WriteLineAsync(f s) } | |
/// Adds metadata for input log lines: |{LogTime}|{name}|{logLevel}| {Message} |. In this example using `|` to push | |
/// composability to the max. | |
/// 1. `awk -F "|"` and now you can interpret the columns in bash scripts. | |
/// 2. Preprend with `|--|--|--|--|` and now you can copy paste logs to markdown files and display them in table format. | |
let fmt logLevel name writer = | |
map (fun line -> String.Format("|{0:O}|{1}|{2}| {3} | ", DateTimeOffset.Now, name, logLevel, line)) writer | |
let consoleLog name = | |
{ new ILogger with | |
member _.Debug = Writer.fmt "DEBUG" name TextWriter.Null // this is equivalent of LogLevel.Info | |
member _.Info = Writer.fmt "INFO" name Console.Out | |
member _.Warn = Writer.fmt "WARN" name Console.Out | |
member _.Error = Writer.fmt "ERROR" name Console.Error } | |
let main () = | |
let log = consoleLog "MyController" | |
log.Info.WriteLine("Hello, {0}!", "Alice") | |
// |2020-04-22T19:00:10.7547403+02:00|MyController|INFO| Hello, Alice! | | |
0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment