DEV Community

Timothy McGrath
Timothy McGrath

Posted on

Logging Scaffold for .NET Core / Serilog

Motivation

Logging is a cross-cutting concern that we want all of our services to perform consistently. We don't want each service to recreate the wheel for logging. We want the same format of data and we want it to go to the same places.

Goal

The goal is a simple library that sets up the standard for logging by writing to a local, rolling file and writing to an external source like DataDog. I don't want developers to have to think about setting up the correct logging, but just fall into success.

The defaults should configure appropriate log levels, but they should be overridable in appsettings.json. New sinks can be created but they will be appended to the existing default sinks.

It should also encapsulate all the necessary nuget packages for logging (as there are many needed to configure Serilog properly).

Using this log library should make adding consistent, standard logging simple.

Logging Library

Create a new .NET Standard library to contain the shared logging setup. The .csproj contains all the Serilog packages needed, which reduces the number of packages that need to be manually added to each app host.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <nullable>enable</nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <LangVersion>latest</LangVersion>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
        <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
        <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
        <PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
        <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
    </ItemGroup>
</Project>

Add a LogCore.cs file to the project to encapsulate all the logging setup:

public class LogCore
{
    public static void Configure(string appName)
    {
        var environment = GetEnvironment();

        var logConfig = ConfigureDefaults(environment);
        logConfig = ConfigureFile(logConfig, appName);
        // Add more logging sinks here...

        // Set the logger instance to the configured logger.
        Log.Logger = logConfig.CreateLogger();
    }

    private static string GetEnvironment()
    {
        // The environment variable is needed for some logging configuration.
        var environment = System.Environment.GetEnvironmentVariable(Environment);
        if (environment == null)
        {
            throw new NullReferenceException($"{Environment} environment variable is not set.");
        }

        return environment;
    }

    private static LoggerConfiguration ConfigureDefaults(string environment)
    {
        // Use the appsettings.json configuration to override minimum levels and add any additional sinks.
        var config = new ConfigurationBuilder()
            .AddJsonFile($"appsettings.json")
            .AddJsonFile($"appsettings.{environment}.json", optional: true)
            .Build();

        // Minimum levels will be overriden by the configuration file if they are an exact match.
        return new LoggerConfiguration()
            .MinimumLevel.Information()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
            .ReadFrom.Configuration(config);
    }

    private static LoggerConfiguration ConfigureFile(LoggerConfiguration logConfig, string appName)
    {
        var fileDirectory = $"c:\\logs\\{appName}\\";
        var hostName = System.Environment.MachineName.ToLower();

        // Add a default async rolling file sink.
        return logConfig
            .WriteTo.Async(a => a.File(
                formatter: new JsonFormatter(renderMessage: true),
                path: $"{fileDirectory}\\log-{hostName}-.json",  // Auto-appends the file number to the filename (log-webvm-001.json)
                rollingInterval: RollingInterval.Day,
                fileSizeLimitBytes: 50000000, // 50 MB file limit
                rollOnFileSizeLimit: true,
                retainedFileCountLimit: 10,
                buffered: false));
    }
}

LogCore configures all the default logging. This example sets up a rolling file writer with a standard name, location, and rollover limit. It also guarantees that the file logging is happening asynchronously.

LogCore also sets the default log levels for Microsoft libraries. Add a default log level for any internal libraries as well. The nice part is that each log level can be overriden from configuration as usual. The library sets the default log levels first, and then it adds the configuration settings which will add to or override any default settings. So, to modify the log levels, the following setting can be added:

"Serilog": {
    "MinimumLevel": {
        "Override": {
            "Microsoft": "Error"
        }

New sinks can also be added through configuration and they will be appended to the default list of sinks.

Integration

At the start of Program.cs, add the LogCore.Configure call:

public static class Program
{
    public const string ApplicationName = "[APP NAME HERE]";

    public static void Main(string[] args)
    {
        LogCore.Configure(ApplicationName);

Also add the .UseSerilog() call to the Host builder:

public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseSerilog()




Conclusion

The app now has standard, consistent logging without having to manually configure each sink and log level. This is great for cases where there are multiple logging outputs that each need their own configuration. Developers can focus on the value of the app instead of the cross-cutting concerns.

Let me know if you have any thoughts/suggestions to improve this!

Oldest comments (0)