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.