'How to silence Serilog in integration tests with the new minimal hosting model of .NET 6

I have a .NET 6 web API project with existing integration tests for some of the API endpoints. The project uses Serilog for logging and everything was fine so far.

I migrated the code to the new minimal hosting model removing the Startup class in the process. I fixed the integration tests to work with the new model and everything is running so far. The only problem I have is, that the integration tests now spams log statements.

For Serilog I have the two staged setup, this is how Program.cs is looking like:

public partial class Program
{
  public static string ApplicationVersion => typeof(Program).Assembly
                                              .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
                                              .InformationalVersion;

  /// <summary>
  /// Hack to prevent duplicate logger initialization when integration tests run in parallel.
  /// </summary>
  public static bool IsIntegrationTestRun = false;

  public static int Main(string[] args)
  {
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";

    if (!IsIntegrationTestRun)
    {
      // extra logger only for app startup
      Log.Logger = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .CreateBootstrapLogger();
    }

    try
    {
      Log.Information("Starting <my application> v{version} in env {env}.", ApplicationVersion, env);

      var builder = WebApplication.CreateBuilder(args);

      builder.Configuration.AddJsonFile("appsettings.Local.json", true, true);

      // Actual logger for dependency injection
      builder.Host.UseSerilog((ctx, lc) =>
      {
        lc.ReadFrom.Configuration(ctx.Configuration);
      });

      // ...
      
      var app = builder.Build();

      // ...

      using (IServiceScope scope = app.Services.CreateScope())
      {
        var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
        dataContext.Database.Migrate();
      }

      app.UseSerilogRequestLogging(c =>
      {
        c.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
          diagnosticContext.Set("Host", httpContext.Request.Host.ToString());
          diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
        };
        c.GetLevel = LogLevelHelper.GetRequestLevel;
      });

      // ...

      app.Run();

      return 0;
    }
    catch (Exception ex)
    {
      Log.Fatal(ex, "Host terminated unexpectedly.");

      return 1;
    }
    finally
    {
      Log.CloseAndFlush();
    }
  }
}

This is my WebApplicationFactory:

[CollectionDefinition("WebApplicationFactory")]
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
  where TStartup : class
{
  protected override void ConfigureWebHost(IWebHostBuilder builder)
  {
    // Somewhat hacky but prevents duplicate logger initialization when integration tests run in parallel.
    Program.IsIntegrationTestRun = true;

    builder.ConfigureAppConfiguration((context, builder) =>
    {
      // Load custom appsettings for Test
      builder.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.Test.json"));

      // optional load personal settings included in gitignore
      builder.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.LocalTest.json"), true);
      builder.AddEnvironmentVariables();
    });
    
    // builder.ConfigureLogging(lb => lb.ClearProviders());
    Log.Logger = new LoggerConfiguration().MinimumLevel.Fatal().CreateLogger();
    
    // ...
  }
}

which is used like this:

[Collection("WebApplicationFactory")]
public class SomeTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
  private readonly CustomWebApplicationFactory<Program> _factory;

  public SomeTests(CustomWebApplicationFactory<Program> factory)
  {
    _factory = factory;
  }

  [Fact]
  public async Task Get_Some_ReturnsSomething()
  {
    // setup ...

    HttpClient client = _factory.CreateClient();
    client.DefaultRequestHeaders.Add("Authorization", RequestHelper.GetBearerAuthenticationHeaderValue(user));
    RequestHelper.AddStrangeHeader(client, user.StrangeKey);
    HttpResponseMessage response = await client.GetAsync("/api/some");

    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    var res = await RequestHelper.DeserializeResponse<List<SomeModel>>(response);
    Assert.Equal(SomeCount, res.Count);
  }
}

As you can see I have extended the appsettings.json pattern to use a local gitignored file for local development (to keep secrets out of the repostiory) and an extra appsettings.Test.json (and another git ignored appsettings.LocalTest.json with extra settings for tests like a different db connection).

When I run the integration tests console is spammed with log statements. Strangely it seems not everything is logged, for example I can't see any request logs. But I can see logs for database migration multiple times like the following:

[09:57:38 INF Microsoft.EntityFrameworkCore.Migrations] Applying migration '20210224073743_InitialSchema'

or this one

[09:57:40 DBG lJty8ESu24x-MY6n4EYr Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler] Successfully validated the token..

I have tried many things like settings the minimum log level to Fatal or directly replace Log.Logger with a new logger.

The application itself is using the injected ILogger instead of the static Log.Logger. Can anyone guide me how to solve this or what I could try next?

The logging seems to respect the settings from my appsettings.Test.json file, when I reduce the minimum level to debug I can see more logs getting printed on the test run. But why is the migration message logged even when I set the minimum level to Fatal?



Solution 1:[1]

I think I've managed to do this. In your CustomWebApplicationFactory, put this in your ConfigureWebHost:

[CollectionDefinition("WebApplicationFactory")]
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
  where TStartup : class
{
  protected override void ConfigureWebHost(IWebHostBuilder builder)
  {
#pragma warning disable CS0618
       builder.UseSerilog((_, _) => { });
#pragma warning restore CS0618
       // ... other customizations
        
       base.ConfigureWebHost(builder);
  }
}

It will complain about the method being obsolete, but this worked for me, it stops calling my original Serilog configuration, and simply will stop logging anything. I believe you can also use this to change the configuration if you wish.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mahmoud Ali