F# and Serilog

Read Time: 5 minutes

Today I’ll discuss two topics. The primary topic is implementing logging by leveraging Serilog with F#. The secondary topic is F# and .NET Core version 2.0. Due to the recent release of .NET Core 2.0 (now with better F# support), I thought this would be a good time to show how to implement an F# .NET Core project.

Going forward I’ll assume you have .NET Core 2.0 installed. If you don’t, head out to the .NET Core Downloads page. Select SDK for your platform. Tangential, but you can also get here by going to dot.net, then navigating to Downloads and .NET Core.

Getting started, create the project. For this post, the example will be a console app. I also have a preference for the Paket package manager. This isn’t strictly necessary, but the benefits are worth the adjustment. To do this I need to convert the project to use Paket. If you want to stick with the default nuget, that’s fine, you’ll just need to do ignore a couple commands and mentally map paket add to nuget install.

1
2
3
dotnet new console --language F# --name SerilogExample
cd SerilogExample
paket convert-from-nuget

Next, add the required packages for configuration and logging. Since I will use config file based configuration, I use the ConfigurationBuilder that .NET Core apps commonly use. At this point I should note that Serilog has additional Sinks as logging targets. Check them out if you want more than plain logfiles.

1
2
3
4
5
6
paket add Microsoft.Extensions.ConfigurationBuilder --project SerilogExample.fsproj
paket add Microsoft.Extensions.ConfigurationBuilder.Json --project SerilogExample.fsproj
paket add Serilog --project SerilogExample.fsproj
paket add Serilog.Settings.Configuration --project SerilogExample.fsproj
paket add Serilog.Sinks.Literate --project SerilogExample.fsproj
paket add Serilog.Sinks.RollingFile --project SerilogExample.fsproj

Time to open up Program.fs and get to work. First, add the necessary namespaces.

1
2
3
4
5
6
7
8
9
open System

open Microsoft.Extensions.Configuration

open Serilog
open Serilog.Configuration
open Serilog.Events
open Serilog.Formatting.Json
open Serilog.Sinks

The logger object can be created in a couple different ways. The first way I’ll examine is being entirely configuration file driven. To do this I need a Configuration object loaded from a json formatted file.

1
2
3
4
5
6
[<EntryPoint>]
let main argv =
let configuration =
ConfigurationBuilder()
.AddJsonFile("config.json")
.Build();

Before going any further, the configuration file needs created. Add a config.json file to the root of the project and populate as below. Other application-specific configuration can be in here as well, but the Serilog settings need to be under the Serilog section. This example will only log messages with a log level of Information and higher. The Serilog LiterateConsole is used for outputting to STDOUT. RollingFile is used for the logfile creation (including a templated name, max file size, and max file count).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "LiterateConsole",
"Args:": {}
},
{
"Name": "RollingFile",
"Args": {
"pathFormat": "log-{Date}.log",
"fileSizeLimitBytes": 10000000,
"retainedFileCountLimit": 100
}
}
]
}
}

Now that a configuration file is created, there is an additional need to address. By default the config file does not get copied to the bin directory on compilation. To ensure the config file ends up where desired (and needed), the .fsproj file needs modified. Adding config.json as a content file, copies it to the output directory when building the project.

1
2
3
4
5
6
<ItemGroup>
<Compile Include="Program.fs" />
<Content Include="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

By this point, most of the work has been done. All that is left to do is feed the configuration object to the Serilog constructor. Logger created.

1
2
3
4
use logger = 
LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger()

Using the logger is straight forward. If you haven’t used Serilog before, it also supports string interpolation.

1
2
3
4
5
6
let foo = "bar"

logger.Debug("A debug message.")
logger.Information("An info message. Details: {foo}", foo)
logger.Error("An error message.")
logger.Warning("A warning message.")

Note, the logging level is set to Information in the config file, so the debug message doesn’t display. Here is how it looks in the console and logfile.

Logger console output

Logger logfile output

Done, right? Not so fast. Serilog does not need to be entirely config file driven. I’m going to replace the previous logger creation with the below code. There are a couple difference. First, I make the log level Debug. More interestly I leverage Serilog’s ability for structured logging.

1
2
3
4
5
6
use logger = 
LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.LiterateConsole()
.WriteTo.RollingFile(new JsonFormatter(), "log-{Date}.log")
.CreateLogger()

Now I run the same logger calls, and here is how it looks in the console and logfile. You can see the debug messages, but more importantly, the log file now has structured events.

Logger console output

Logger logfile output

Hopefully you have found these Serilog examples useful when integrating logging into your new .NET Core F# applications. Until next time.