Sooner or later printfn style of logging becomes too cumbersome and you start to search for a logging library. For F# developers the most obvious choice is Logary, but very soon you find out that your Logary logging code is even less readable. In this article you will find F# tricks, which helped me to create a neat abstraction for logging in Semagle Framework.

Logary is perfect for libraries because it does not require you to reference Logary library. You only need to add Facade.fs dependency to your paket dependencies file:

github logary/logary src/Logary.Facade/Facade.fs

and add a replacement target to your FAKE build script:

Target "LoggingFile" (fun _ ->
    ReplaceInFiles [ "namespace Logary.Facade", "namespace Semagle.Logging" ]
                   [ "paket-files/logary/logary/src/Logary.Facade/Facade.fs" ]
)

Thus, it was easy to configure and add logger to my code:

open Semagle.Logging
...
Global.initialise { Global.defaultConfig with
                    getLogger = (fun name -> Targets.create Info name) }
...
let logger = Log.create "C_SMO"

However, I needed synchronous and low overhead logging, i.e., evaluation of the logged message only if the message level is above the minimal configured level. With Logary my simple printfn "iteration = %d, objective = %f" k (objective n) became:

logger.debugWithBP (fun level ->
  Message.event level (sprintf "iteration = %d, objective = %f" k (objective n))) |> Hopac.run

Some developers may disagree with me, but I think that logging should not pollute the code and should be simple and readable. So, I started to search for ways to simplify the logging code.

First, I found that I can create a builder with custom operations:

type LoggerBuilder(logger : Logger) =
    let log (level : LogLevel) (message : unit -> string) =
        logger.log level (fun level -> message() |> event level) |> Hopac.run |> ignore

    member builder.Yield (()) = ()

    ...

    [<CustomOperation("debug")>]
    member builder.debug (_ : unit, message : unit -> string) =
        log Debug message

    ...

So, I was able to write a much more readable code:

let logger = LoggerBuilder(Log.create "C_SMO")
...
logger { debug (fun _ -> sprintf "iteration = %d, objective = %f" k (objective n)) }

Second, I found the “magic” annotation [<ProjectionParameter>]:

type LoggerBuilder(logger : Logger) =
    let log (level : LogLevel) (message : unit -> string) =
        logger.log level (fun level -> message() |> event level) |> Hopac.run |> ignore

    member builder.Yield (()) = ()

    ...

    [<CustomOperation("debug")>]
    member builder.debug (_ : unit, [<ProjectionParameter>] message : unit -> string) =
        log Debug message

So, now I can write:

logger { debug (sprintf "iteration = %d, objective = %f" k (objective n)) }

Therefore, now my logging code is as simple as printfn, but also it is lazily evaluated and easily distinguished from other code.

If you find this DSL useful, you can download LoggerBulder code here and adapt it to your needs.