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):
-
If you add an argument of the wrong type, you will get an 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!