Skip to content

Instantly share code, notes, and snippets.

@Horusiath
Last active June 21, 2023 18:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Horusiath/6f4f7f0efcab9607fbad587d20ec4968 to your computer and use it in GitHub Desktop.
Save Horusiath/6f4f7f0efcab9607fbad587d20ec4968 to your computer and use it in GitHub Desktop.
Fun with concepts about composeable logging.
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