United Kingdom: +44 (0)208 088 8978

Adding FSharp.Logf to a SAFE Stack app

We're hiring Software Developers

Click here to find out more

FSharp.Logf is a library that "implements printf-style logging functions for any Microsoft.Extensions.Logging.ILogger". There is the following example in its README

logfi ml "File %s{fileName} has %d{bytesLength} bytes" fileName bytes.Length

All the functions are of this form, where the final letter indicates the log level (information in this case). For the higher error levels, there are also functions prefixed with 'e', such as elogfe that also accept exceptions.

In this post, I'll show you how you can use this library in a SAFE Stack app. I'm using the v4.2.0 template.

Install package via Paket

Add FSharp.Logf to paket.dependencies and ./src/Server/paket.references, then run dotnet paket install.

Logging on the server

Create a logger in the main function in ./src/Server/Server.fs

    let logger =
        LoggerFactory
            .Create(fun builder -> builder.AddConsole().SetMinimumLevel(LogLevel.Information) |> ignore)
            .CreateLogger()

Pass it as an argument from the main function, through app and webApp to todosApi.

Update todosApi to use the new logger parameter and the logfi and logfe functions provided by FSharp.Logf:

let todosApi logger =
    { getTodos = fun () -> async { return Storage.todos |> List.ofSeq }
      addTodo =
        fun todo ->
            async {
                logfi logger "Adding Todo: %A{todo}" todo
                return
                    match Storage.addTodo todo with
                    | Ok () ->
                        logfi logger "Todo added: %A{todo}" todo
                        todo
                    | Error e ->
                        logfe logger "Error while adding Todo: %A{todo} %s{error message}" todo e
                        failwith e
            } }

You can see the full changes in the GitHub repo accompanying this post.

Straight out of the box, there are some nice features akin to what you get from printf:

  • If you forget an argument, you will get a warning (because you have a line with an expression that does not evaluate to unit):

    Code with missing argument and warning

  • If you add an argument of the wrong type, you will get an error:

    Code with wrong argument type and error

Interestingly, Rider tried to tell me that I could replace the format strings with interpolated strings (hence the green squiggles in the screenshots), but the library behaved differently after I made that change.

You can test this out by checking what's logged when you add a Todo from the browser. If you want to test the error case and are using a Chromium-based browser, you can: open your browser's dev tools; open the Network tab; send a regular Todo; right-click on the row for the corresponding HTTP request; select Copy > Copy as fetch; open the Console tab; paste the fetch in; modify the body so that the Todo's Description property is empty; press the return key. You should receive a 500 error, and see your error logged in the terminal where you are running your app from.

server: info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
server: Request starting HTTP/1.1 POST http://localhost:5000/api/ITodosApi/addTodo application/json;+charset=UTF-8 73
server: info: object[0]
server: Adding Todo: { Id = 7ac25a5a-047f-4d17-b363-113a549db279
server: Description = "Profit" }
server: info: object[0]
server: Todo added: { Id = 7ac25a5a-047f-4d17-b363-113a549db279
server: Description = "Profit" }
server: info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
server: Request finished HTTP/1.1 POST http://localhost:5000/api/ITodosApi/addTodo application/json;+charset=UTF-8 73 - 200 - application/json;+charset=utf-8 66.7297ms
server: info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
server: Request starting HTTP/1.1 POST http://localhost:5000/api/ITodosApi/addTodo application/json;+charset=UTF-8 67
server: info: object[0]
server: Adding Todo: { Id = 7ac25a5a-047f-4d17-b363-113a549db279
server: Description = "" }
server: fail: object[0]
server: Error while adding Todo: { Id = 7ac25a5a-047f-4d17-b363-113a549db279
server: Description = "" } Invalid todo{error message}
server: info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
server: Request finished HTTP/1.1 POST http://localhost:5000/api/ITodosApi/addTodo application/json;+charset=UTF-8 67 - 500 - application/json;+charset=utf-8 9.5057ms

Obviously, you're not gaining loads over what you can get with plain-old printf here, but the benefit would come by hooking into a different non-console logging provider.

Exercise for the reader - add to the client

FSharp.Logf has a fable-compatible version too: Fable.FSharp.Logf.

Conclusion

I like the small scope of FSharp.Logf's aims. It knows exactly what it's trying to help its users do—allow them to use format strings when creating logs—and provides a simple API for doing so. In my very limited experiment, it seemed to do a good job of that.

There's still some things I'd want to explore before considering using it in production, for example how well it works with our preferred logging library, Serilog. In addition, I'm wary of adding new dependencies, and I don't know that the utility this library offers warrants bringing on a new one.

2023-06-02 EDIT: John, the library's author has responded to these points. In particular, he uses Serilog in his own projects, so I imagine the integration is great. I just wanted to highlight here that I haven't tried it myself.

Nevertheless, I always like seeing new F# libraries as it's the sign of a healthy ecosystem, and this one could have an interesting future. Thanks John for putting it together!