serilog / serilog-settings-configuration

A Serilog configuration provider that reads from Microsoft.Extensions.Configuration
Apache License 2.0
446 stars 129 forks source link

Are "WriteTo" sections additive or replacement when using multiple appsettings json files? #234

Open nCubed opened 4 years ago

nCubed commented 4 years ago

When using multiple appsettings.json files, the "WriteTo" section appears to a full replacement by the overriding appsetting json file.

.Net Core 3.1
Serilog.AspNetCore Version="3.4.0"
Serilog.Enrichers.Environment Version="2.1.3"
Serilog.Enrichers.Process Version="2.0.1"
Serilog.Enrichers.Thread Version="3.1.0"
Serilog.Exceptions Version="5.6.0"
Serilog.Extensions.Logging Version="3.0.1"
Serilog.Settings.Configuration Version="3.1.0"
Serilog.Sinks.Async Version="1.4.0"
Serilog.Sinks.ColoredConsole Version="3.0.1"
Serilog.Sinks.Console Version="3.1.1"
Serilog.Sinks.File Version="4.1.0"

Sample config / setup...

Program

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostContext, config) =>
        {
            string env = hostContext.HostingEnvironment.EnvironmentName;

            config.AddJsonFile("appsettings.serilog.json", false, true)
                .AddJsonFile($"appsettings.serilog.{env}.json", false, true);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((hostContext, loggerConfig) =>
        {
            loggerConfig.ReadFrom.Configuration(hostContext.Configuration);
        });
}

appsettings.serilog.json

{
  "Serilog": {
    "Using": [
      "Serilog.Enrichers.Thread",
      "Serilog.Exceptions",
      "Serilog.Sinks.Async",
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File"
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "System": "Warning",
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Warning",
        "System.Net.Http": "Information"
      }
    },
    "Enrich": [
      "WithExceptionDetails",
      "WithThreadId"
    ],
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "App_Data/logs/log-.txt",
          "rollingInterval": "Day",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffff} [{Level:u3}] [{ThreadId}] {Message:lj} {NewLine}{Exception}"
        }
      }
    ]
  }
}

appsettings.serilog.Development.json

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Verbose",
      "Override": {
        "Microsoft.Hosting.Lifetime": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "Async",
        "Args": {
          "configure": [
            {
              "Name": "Console",
              "Args": {
                "restrictedToMinimumLevel": "Information",
                "outputTemplate": "{Timestamp:HH:mm:ss} [{Level:u3}] [{ThreadId}] {Message:lj} {NewLine}{Exception}"
              }
            }
          ]
        }
      }
    ]
  }
}

Current Behavior (when running under .net core development environment)

Maybe Expected Behavior?

Summary I think the general question can be resolved with: How can we define a sink (WriteTo) so that it is only defined in one appsetting file and is picked up by the overriding appsetting config json file? Or do we need to define duplicate sinks in each appsetting for each environment we want the sink to be included?

After writing this, I am about 99% sure this is expected behavior for the overriding appsetting json to overwrite the entire WriteTo section.

mikejr83 commented 3 years ago

I think I'm having a related issue.

I've built a helper assembly to apply the Serilog configuration the same way in all of our microservices. An extension method for the HostBuilder uses the ConfigureAppConfiguration on the IHostBuilder to add an embedded JSON stream to the configuration builder as well as optional file providers for specific Serilog overrides.

Assembly assembly = typeof(ConfigurationBuilderExtensions).Assembly;

configurationBuilder.AddJsonStream(assembly.GetManifestResourceStream($"{typeof(ConfigurationBuilderExtensions).Namespace}.serilog.configuration.web.json"));

Stream stream = assembly.GetManifestResourceStream($"{typeof(ConfigurationBuilderExtensions).Namespace}.serilog.configuration.web.{(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production").ToLowerInvariant()}.json");
if (stream != null)
{
    configurationBuilder.AddJsonStream(stream);
}

return configurationBuilder
    .AddJsonFile(Path.Combine(contentRootPath, "serilog.configuration.web.json"), optional: true, reloadOnChange: true)
    .AddJsonFile(Path.Combine(contentRootPath, $"serilog.configuration.web.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json"),
        optional: true, reloadOnChange: true)
    .AddEnvironmentVariables();

This should give me a hierarchical set of configuration where I can define in the embedded JSON a WriteTo like this:

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Console" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { 
        "Name": "Console"
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
         } 
      }
    ]
  }
}

In my testing app I can then include a file serilog.configuration.web.json that looks like:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Verbose"
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
        }
      }
    ]
  }
}

Based on my understanding of how the configuration builder works this would give me an IConfiguration that contains a single WriteTo element with its Args.formatter value set to "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact".

Testing this:

.UseSerilog((context, serviceProvider, loggerConfiguration) =>
{
   var formatterVal = context.Configuration.GetValue<string>("Serilog:WriteTo:0:Args:formatter")
    loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext();
})

The formatterVal is resolved to "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"

This is expected.

However, the console formatter at runtime is the standard console logger with the "AnsiConsoleTheme" enabled.

I did some digging in the code and found the GetMethods call. I plucked it into my code and gave it a reference to the WriteTo configuration section. The result value returned by the method has a single key "Console". The key's value has two keys, one for the formatter and one for the theme. It seems that the theme value is overriding the formatter.

I'm not sure if this is expected behavior, some oddity with how the configuration works, or something else.

dglozano commented 2 years ago

Wild guess, but I think the problem might be that you are defining your sinks as an array in WriteTo. An array in the JSON configuration will be translated to multiple properties using the the automatic index.

Therefore, if you have this

"WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "App_Data/logs/log-.txt",
          "rollingInterval": "Day",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffff} [{Level:u3}] [{ThreadId}] {Message:lj} {NewLine}{Exception}"
        }
      }
    ]

it would be equivalent as having this

"WriteTo:0": 
      {
        "Name": "File",
        "Args": {
          "path": "App_Data/logs/log-.txt",
          "rollingInterval": "Day",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffff} [{Level:u3}] [{ThreadId}] {Message:lj} {NewLine}{Exception}"
        }
      }

Then, in your *.development.json you are defining the same property again, WriteTo:0 (since it is also an array with a single element) and this is replacing the one being written in the base appsetings.json.

You could try defining your WriteTo using names instead of the index as explained in the Readme and the sample.

sungam3r commented 1 year ago

Could be closed?

mickelsonmichael commented 1 year ago

I'd like to expand a bit on this with a similar scenario that also causes some pain points, if I could. It may be appropriate as a new issue but they seem similar enough

If you define a MinimumLevel section using a simple string

"MinimumLevel": "Information"

Then attempt to override that configuration in another JSON file (e.g. appsettings.Development.json) in a different format

"MinimumLevel": {
  "Default": "Debug"
}

The second value is ignored and the minimum level will be strictly "Information".

Ideally, the library could detect duplicate MinimumLevel and MinimumLevel:Default configurations and defer to the most recently defined option. But it would also be worthwhile to simply document cases like this where the override behavior is not intuitive (at least for some).

Worth noting also that the Overrides are still respected, it's just the Default level that is the issue.