serilog / serilog-settings-configuration

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

Configuring Console with empty OutputTemplate overrides the use of a custom formatter #321

Open LordMike opened 2 years ago

LordMike commented 2 years ago

I'm trying to make an app that has a default console output format (With an OutputTemplate), but is otherwise overridable by configs (environment variables in my case).

I've made an example app to show my issue. I have a config like this:

{
  "Logging": {
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "OutputTemplate": "My custom template"
        }
      }
    ]
  }
}

I can then use it:

var config = new ConfigurationBuilder()
    .AddJsonFile("config.json")
    .Build();

var logger = new LoggerConfiguration()
    .ReadFrom.Configuration(config, "Logging")
    .CreateLogger();

logger.Information("Hello world");

Which outputs

My custom template

So far so good. Now I want my users to use environment variables (or something else) to override the configs, and potentially use a custom formatter, like RenderedCompactJsonFormatter. So we add in environment variables to our config builder, and add the following:

Logging__WriteTo__0__Args__formatter__type: Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact

But this doesn't override the OutputTemplate, it still outputs My custom template - if I remove the OutputTemplate from the config json (using the default from the Console sink), the environment variable works - it switches to json rendering.

I've also tried overriding the template with a blank string, like:

Logging__WriteTo__0__Args__OutputTemplate: ""

.. but this just changes the output template to a blank string - it's still used.

From what I read, Microsoft.Extensions.Configuration doesn't support unsetting values, you cannot f.ex. configure a specific key to null, and have it removed from the system. The closest you can get, is overriding a value with a blank string, and have that be a convention to be "unset". For example, this blog post also highlights the issue:

I mentioned overriding with an empty string as a “fake way” to remove configuration. Specifying null as a value, even in JSON config, doesn’t work. It reads the value and uses an empty string as the value instead of null. Further, there’s no XML analogy to null, nor are there such analogies in most other providers.

Given everything in the config system is a key/value string pair, the closest you can get, then, is to set things to empty strings. When you read values, check for String.IsNullOrEmpty() before using the value.

Are there any tips on how I can provide a custom default template, but let users override my config using Microsofts configuration system?

I think I'm going with skipping the custom format by default, in order to let users override the formatter - but I think there might be a general issue here :).

nblumhardt commented 2 years ago

Microsoft.Extensions.Configuration is a bit tricky at times :thinking:

Using Serilog.Expressions and an expression template, through the formatter argument:

https://github.com/serilog/serilog-settings-configuration/pull/281

will let you switch between JSON and plain text output by modifying the same configuration settings (formatter.template).

HTH!

jorgepsmatos commented 6 days ago

I have this issue too and wanted to avoid Serilog.Expressions, any solution?

0xced commented 6 days ago

From what I read, Microsoft.Extensions.Configuration doesn't support unsetting values.

Indeed, Enable removal of a key or subtree was filed on Oct 16, 2015 and is still unaddressed as of today. And with only 6 thumbs up, I doubt a 7th will have any impact. 🙁

The problem is that Serilog.Settings.Configuration must choose which method to call given a set of parameters. For the Console sink, when given outputTemplate = "" and formatter = "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact" the method that takes the output template is selected because there's one matching argument for each but outputTemplate is a string and formatter is not:

https://github.com/serilog/serilog-settings-configuration/blob/v8.0.2/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs#L473-L479

var matchingArgs = m.GetParameters().Where(p => ParameterNameMatches(p.Name, suppliedArgumentNames)).ToList();

// Prefer the configuration method with most number of matching arguments and of those the ones with
// the most string type parameters to predict best match with least type casting
return new Tuple<int, int>(
    matchingArgs.Count,
    matchingArgs.Count(p => p.ParameterType == typeof(string)));

Theoretically it could be possible to make Serilog.Settings.Configuration choose the method that takes the ITextFormatter by specifying one more matching parameter but both methods have the same parameters (restrictedToMinimumLevel, levelSwitch and standardErrorFromLevel)

So your best chance is probably to use Serilog.Expressions as proposed by @nblumhardt.