Serilog JSON config LoggingLevelSwitch access
Asked Answered
I

2

8

Configuring Serilog using a JSON config, it is possible to configure log level switches as such:

"LevelSwitches": {
  "$appLogLevel": "Debug",
  "$netLogLevel": "Information",
  "$sysLogLevel": "Error"
},
"MinimumLevel": {
  "ControlledBy": "$appLogLevel",
  "Override": {
    "Microsoft": "$netLogLevel",
    "System": "$sysLogLevel"
  }
}

the purpose of the switches (when instantiated in code) is to be accessed at a later time in order to change the minimum log levels during run-time. However when configured via the JSON config, I can't find a way to access those switch instances. Does anyone know how to access them?

Infest answered 17/7, 2018 at 20:16 Comment(0)
L
1

If you want to access your level switches from code, it probably means that you have a way to control them somehow, so you probably don't need them in the config file in the first place...

I do believe it makes more sense to keep that part entirely in the code and have the configuration partly in code and partly in the config file, so that would look like this :

// in C# code
var appLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Debug);
var netLevelSwitch= new LoggingLevelSwitch(LogEventLevel.Information);
var systemLevelSwitch= new LoggingLevelSwitch(LogEventLevel.Error);

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

Log.Logger = new LoggerConfiguration()
            // load config from config file ...
            .ReadFrom.Configuration(configuration)
            // ... and complete it in C# code
            .MinimumLevel.ControlledBy(appLevelSwitch )
            .MinimumLevel.Override("Microsoft", netLevelSwitch)
            .MinimumLevel.Override("System", systemLevelSwitch)
            .CreateLogger();

and in your config file

{
  "Serilog": {
    "Using":  ["Serilog.Sinks.Console"],
    "WriteTo": [
      { "Name": "Console" },
      { "Name": "File", "Args": { "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt" } }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Destructure": [
      { "Name": "With", "Args": { "policy": "Sample.CustomPolicy, Sample" } },
      { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } },
      { "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 100 } },
      { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } }
    ],
    "Properties": {
        "Application": "Sample"
    }
  }
}

For the sake of completeness, though, in order to access the defined control switches, you could do the following (be warned that this is kind of a hack !).

Write a configuration method (i.e. an extension method that can appear after .WriteTo.xxx) that accepts LoggingLevelSwitches as arguments and stores them as static members. That configuration method will introduce a dummy ILogEventSink that does nothing (and for performance sake, we can even specify restrictedToMinimumLevel: LogEventLevel.Fatal so that it is almost never called). Then invoke that extension method from the config file (Serilog.Settings.Configuration knows how to find extension methods and pass them the parameters) and voilà, you can now access the static switches from your code !

Here is what it would look like :

public static class LevelSwitches
{
    private static LoggingLevelSwitch _switch1;
    private static LoggingLevelSwitch _switch2;
    private static LoggingLevelSwitch _switch3;

    public static LoggingLevelSwitch Switch1 => _switch1 ?? throw  new InvalidOperationException("Switch1 not initialized !");
    public static LoggingLevelSwitch Switch2 => _switch2 ?? throw  new InvalidOperationException("Switch2 not initialized !");
    public static LoggingLevelSwitch Switch3 => _switch3 ?? throw  new InvalidOperationException("Switch3 not initialized !");

    public static LoggerConfiguration CaptureSwitches(
        this LoggerSinkConfiguration sinkConfig,
        LoggingLevelSwitch switch1,
        LoggingLevelSwitch switch2,
        LoggingLevelSwitch switch3)
    {
        _switch1 = switch1;
        _switch2 = switch2;
        _switch3 = switch3;

        return sinkConfig.Sink(
            restrictedToMinimumLevel: LogEventLevel.Fatal,
            logEventSink: new NullSink());
    }
}

public sealed class NullSink : ILogEventSink
{
    public void Emit(LogEvent logEvent)
    {
        // nothing here, that's a useles sink !
    }
}

Then in you json config file :

"LevelSwitches": {
  "$appLogLevel": "Debug",
  "$netLogLevel": "Information",
  "$sysLogLevel": "Error"
},
"MinimumLevel": {
  "ControlledBy": "$appLogLevel",
  "Override": {
    "Microsoft": "$netLogLevel",
    "System": "$sysLogLevel"
  }
},
"WriteTo":[
  {
    "Name": CaptureSwitches"", 
    "Args": {
      "switch1": "$appLogLevel",
      "switch2": "$netLogLevel",
      "switch3": "$sysLogLevel",
    }
  }
]

(you may need a "Using" directive with the name of the assembly containing the LevelSwitches class)

Configure your logger from the config file

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

    var logger = new LoggerConfiguration()
        .ReadFrom.Configuration(configuration)
        .CreateLogger();

From that point, you should be able to access the switches through LevelSwitches.Switch1, LevelSwitches.Switch2 and LevelSwitches.Switch3.

Larousse answered 21/7, 2018 at 14:32 Comment(0)
I
1

My current project required highly configurable logging as well as the ability to adjust any of those configured log levels at runtime.

So I had actually already written a work-around (but in a more generalized way), by simply processing the "MinimalLevel" section of the config manually in my Program.cs, like this:

Requires a static dictionary for later reference:

public static Dictionary<String, LoggingLevelSwitch> LogLevel = null;

And a code block to bind the LoggingLevelSwitches:

//Configure logger (optional)
  if (appConfig.GetSection("Serilog").Exists()) {
  //Configure Serilog
    LoggerConfiguration logConfig = new LoggerConfiguration().ReadFrom.Configuration(appConfig);
  //If Serilog config parsed okay acquire LoggingLevelSwitches
    LogLevel = LoadLoggingLevelSwitches(appConfig);
  //Bind LoggingLevelSwitches        
    foreach (String name in LogLevel.Keys) {
      if (String.Equals(name, "Default", StringComparison.InvariantCultureIgnoreCase)) {
        logConfig.MinimumLevel.ControlledBy(LogLevel[name]);
      } else {
        logConfig.MinimumLevel.Override(name, LogLevel[name]);
      }
    }
  //Build logger from config
    Log.Logger = logConfig.CreateLogger();
  }

which utilizes a routine that instantiates all those switches (based on the config file):

public static Dictionary<String, LoggingLevelSwitch> LoadLoggingLevelSwitches(IConfiguration cfg) {
  Dictionary<String, LoggingLevelSwitch> levels = new Dictionary<String, LoggingLevelSwitch>(StringComparer.InvariantCultureIgnoreCase);
//Set default log level
  if (cfg.GetSection("Serilog:MinimumLevel:Default").Exists()) {
    levels.Add("Default", new LoggingLevelSwitch((LogEventLevel)Enum.Parse(typeof(LogEventLevel), cfg.GetValue<String>("Serilog:MinimumLevel:Default"))));
  }
//Set log level(s) overrides
  if (cfg.GetSection("Serilog:MinimumLevel:Override").Exists()) {
    foreach (IConfigurationSection levelOverride in cfg.GetSection("Serilog:MinimumLevel:Override").GetChildren()) {
      levels.Add(levelOverride.Key, new LoggingLevelSwitch((LogEventLevel)Enum.Parse(typeof(LogEventLevel), levelOverride.Value)));
    }
  }      
  return levels;
}

I have separate class that handles applying runtime logging level changes via these switches, but this was the easiest way to get anything and everything I needed, however...

after writing all that code and then finding out there was a way to just add the switches directly from the config with the"LevelSwitches" section, I realized I was probably doubling up on the work. Because obviously Serilog needs to be instantiating and binding it's own switches defined in the config... it just doesn't appear to give a nice and easy way to access them so I can use them later. Which is counter intuitive because the whole point of a LoggingLevelSwitch is to reference it later at runtime.

Seems if the switches are allowed to be created through the config, we should be given an easy way to access them. Perhaps I should add this as a feature request over on the Serilog GitHub.

Infest answered 21/7, 2018 at 21:19 Comment(1)
actually declaring switches through config was added to support (for now) the Seq sink, that can actually accept an existing Level Switch and control it remotely (see github.com/serilog/serilog-sinks-seq#dynamic-log-level-control ). You could open a feature request though, that could be an interesting discussion :)Larousse

© 2022 - 2024 — McMap. All rights reserved.