Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nCubed opened this issue Sep 2, 2020 · 4 comments

Comments

@nCubed
Copy link

nCubed commented Sep 2, 2020

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.

@mikejr83
Copy link

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
Copy link

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
Copy link
Contributor

Could be closed?

@mickelsonmichael
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants