Configure Serilog for Logging in Azure Functions

Even though the built-in logging in .NET Core is pretty good these days, Serilog takes it to the next level, making complex logging exceptionally easy.

Serilog allows us to not only log the output of the functions to the Console but also write to a Log file, Application Insights, Raygun, Sentry, or any number of locations, called Sinks.

You can take a look at the long list of available sinks at https://github.com/serilog/serilog/wiki/Provided-Sinks

In this post, we’ll just look at setting up a basic configuration for Serilog. You can then look further into any sinks that you might be interested in.

Azure Functions project set up

These instructions are for use with the Isolated Model for Azure Functions. From .NET 6 onwards, the isolated model has been the preferred model and will be the only model beyond .NET 8.

azure functions project create

Nuget Packages for Serilog

The first step once you’ve created your Azure Functions project is to add a few Nuget packages. Add the following Nuget packages.

Serilog
Serilog.Extensions.Logging
Serilog.Sinks.Console
Serilog.Sinks.File

Base Serilog Configuration

Once these have been added, we can switch over to the Program.cs to start wiring up the Serilog logging. The minimum configuration that will write log records to the Console, which is the out-of-the-box experience for an Azure Functions project is below.

using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureLogging((hostingContext, logging) =>
    {
        Log.Logger = new LoggerConfiguration()
            .WriteTo.Console(LogEventLevel.Debug)
            .CreateLogger();

        logging.AddSerilog(Log.Logger, true);
    })
    .Build();

host.Run();

Above we’ve created our LoggerConfiguration and added the Console sink to it, then created the Logger and added the Logger to the Logging Pipeline.

We can certainly improve on this though by using a few additional features of Serilog.

Adding a second Serilog Sink

We can add a second sink to log to a txt file and rotate our text files on a daily basis.

When we run the Azure Function app on Azure, as opposed to locally, we need to write the log files to the LogFiles folder path, not to the local execution directory.

So to accomplish this, we do a quick check to see if we’re running on Azure. Checking an Environment variable that will only exist on Azure will tell us if we’re running on Azure or locally.

We then use this to set the appropriate file name and location.

The Serilog file sink will automatically append a date stamp on the end of the filename when we set the rollingInterval.

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureLogging((hostingContext, logging) =>
    {
        var filepath = string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")) ?
                        "log.txt" :
                        @"D:\home\LogFiles\Application\log.txt";

        Log.Logger = new LoggerConfiguration()
            .WriteTo.Console(LogEventLevel.Debug)
            .WriteTo.File(filepath, LogEventLevel.Debug, rollingInterval: RollingInterval.Day)
            .CreateLogger();

        logging.AddSerilog(Log.Logger, true);
    })
    .Build();

host.Run();
azure functions serilog txt log files

Using Serilog Enrichment

But we can do better than this as well. We can add some additional enrichment to the logs for when we send the logs to more complex systems like Application Insights, Seq, Rayrun, and Sentry to name a few.

You can read more about Enrichment at https://github.com/serilog/serilog/wiki/Enrichment

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureLogging((hostingContext, logging) =>
    {
        var filepath = string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")) ?
                        "log.txt" :
                        @"D:\home\LogFiles\Application\log.txt";

        Log.Logger = new LoggerConfiguration()
            .Enrich.WithProperty("Application", "SHDev Blog Functions")
            .Enrich.FromLogContext()
            .WriteTo.Console(LogEventLevel.Debug)
            .WriteTo.File(filepath, LogEventLevel.Debug, rollingInterval: RollingInterval.Day)
            .CreateLogger();

        logging.AddSerilog(Log.Logger, true);
    })
    .Build();

host.Run();

Now this will give a nice logging experience and allow us to log to both the Console and to a File.

Minimum Logging Levels

But once you start adding in some triggers and binding to other services in your functions, you’ll see that the logging starts to explode with log entries from the included services.

Azure Storage services like Queues, Tables, and Blobs emit a LARGE number of extra logs, which we want to suppress.

To do this, we can set some Minimum Level overrides based on the Namespace or Type.

Below is what I’ve found works best for me. It shows me the important things like errors but hides the firehose of logging that I don’t need to see.

using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureLogging((hostingContext, logging) =>
    {
        var filepath = string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")) ?
                        "log.txt" :
                        @"D:\home\LogFiles\Application\log.txt";

        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .MinimumLevel.Override("Worker", LogEventLevel.Warning)
            .MinimumLevel.Override("Host", LogEventLevel.Warning)
            .MinimumLevel.Override("System", LogEventLevel.Error)
            .MinimumLevel.Override("Function", LogEventLevel.Debug)
            .MinimumLevel.Override("Azure.Storage", LogEventLevel.Error)
            .MinimumLevel.Override("Azure.Core", LogEventLevel.Error)
            .MinimumLevel.Override("Azure.Identity", LogEventLevel.Error)
            .Enrich.WithProperty("Application", "SHDev Blog Functions")
            .Enrich.FromLogContext()
            .WriteTo.Console(LogEventLevel.Debug)
            .WriteTo.File(filepath, LogEventLevel.Debug, rollingInterval: RollingInterval.Day)
            .CreateLogger();

        logging.AddSerilog(Log.Logger, true);
    })
    .Build();

host.Run();

This is the default setup that I use when creating a new Azure Functions project.

I might also look at adding in Application Insights or Raygun depending on the needs of the project.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top