serilog-settings-configuration icon indicating copy to clipboard operation
serilog-settings-configuration copied to clipboard

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

Open nCubed opened this issue 4 years ago • 4 comments

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)

  • The Default Minimum log level is updated from info to verbose by the Development json file (this is correct)
  • The only sink being written to is the Console sink from the Development json file (not sure if correct)
  • The file sink is ignored in the other json file (not sure if correct)

Maybe Expected Behavior?

  • The sinks from each WriteTo sections should be added

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.

nCubed avatar Sep 02 '20 19:09 nCubed

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.

mikejr83 avatar Oct 29 '20 15:10 mikejr83

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.

dglozano avatar Nov 24 '21 11:11 dglozano