serilog / serilog-settings-configuration

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

Control level switch dynamically #376

Closed dev-vinicius-andrade closed 1 year ago

dev-vinicius-andrade commented 1 year ago

Hello,

Currently I'm trying to control the level switch from the app dynamically. Here is my serilog section from appsettings.json

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "ControlledBy": "$controlSwitch",
      "Override": {
        "Microsoft": "Error",
        "System.Net.Http.HttpClient": "Error"
      }
    },
    "LevelSwitches": { "controlSwitch": "Verbose" },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "controlLevelSwitch": "$controlSwitch"
        }
      },
    ]
  }
}

Together with it I've made the changes in my program startup to use the level switch.

IDinamicallyLogSwitcherService ConfigureSerilog(IHostBuilder hostBuilder, ILoggingBuilder loggerBuilder)
{
    Serilog.Debugging.SelfLog.Enable(Console.Error);
    var dinamicallyLogSwitcherService = DinamicallyLogSwitcherService.Create();
    loggerBuilder.ClearProviders();
    hostBuilder.UseSerilog((context, configuration) =>
    {

        var options = new ConfigurationReaderOptions
        {
            OnLevelSwitchCreated = (switchName, levelSwitch) => dinamicallyLogSwitcherService.AddSwitch(switchName, levelSwitch),
        };
        configuration.ReadFrom.Configuration(context.Configuration, options);
    });
    return dinamicallyLogSwitcherService;
}

var builder = WebApplication.CreateBuilder(args);
var dinamicallyLogSwitcherService = ConfigureSerilog(builder.Host, builder.Logging);
builder.Services.AddSingleton(dinamicallyLogSwitcherService);

// Add services to the container.
builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHostedService<Worker>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

And Also created an interface to provide the contract to switch the log level.

public interface IDinamicallyLogSwitcherService
{
    IEnumerable<string> GetAllSwitchNames();
    LoggingLevelSwitch? this[string switchName] => GetSwitch(switchName);
    void Switch(string switchName, LoggingLevelSwitch levelSwitch);
    void AddSwitch(string switchName, LoggingLevelSwitch levelSwitch);
    LoggingLevelSwitch? GetSwitch(string switchName);
}

And A Service that implements this interface.

internal class DinamicallyLogSwitcherService:IDinamicallyLogSwitcherService
{
    private readonly IDictionary<string, LoggingLevelSwitch> _switches;

    private DinamicallyLogSwitcherService(IDictionary<string, LoggingLevelSwitch>? switches=null)
    {
        _switches = switches ?? new Dictionary<string, LoggingLevelSwitch>();
    }

    public static IDinamicallyLogSwitcherService Create(IDictionary<string, LoggingLevelSwitch>? switches = null) =>
        new DinamicallyLogSwitcherService(switches);

    public IEnumerable<string> GetAllSwitchNames()
    {
        return !_switches.Any() ? Enumerable.Empty<string>() : _switches.Keys;
    }

    public void Switch(string switchName, LoggingLevelSwitch levelSwitch)
    {
        if (_switches.ContainsKey(switchName))
            _switches[switchName] = levelSwitch;
    }

    public void AddSwitch(string switchName, LoggingLevelSwitch levelSwitch)
    {
        if (!_switches.ContainsKey(switchName))
            _switches.Add(switchName, levelSwitch);
    }

    public LoggingLevelSwitch? GetSwitch(string switchName)
    {
        return !_switches.ContainsKey(switchName) ? null : _switches[switchName];
    }
}

Then I've created a controller responsible to change the log level

 [ApiController]
    [Route("[controller]")]
    public class LogLevelController : ControllerBase
    {
        private readonly ILogger<LogLevelController> _logger;
        private readonly IDinamicallyLogSwitcherService _dinamicallyLogSwitcherService;

        public LogLevelController(ILogger<LogLevelController> logger, IDinamicallyLogSwitcherService dinamicallyLogSwitcherService)
        {
            _logger = logger;
            _dinamicallyLogSwitcherService = dinamicallyLogSwitcherService;
        }

        [HttpPost("{switchName}/{logLevel}")]
        public IActionResult Post(string switchName,  LogLevel logLevel)
        {
            switch (logLevel)
            {
                case LogLevel.Trace:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Verbose));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.Debug:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Debug));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.Information:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Information));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.Warning:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Warning));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.Error:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Error));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.Critical:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Fatal));
                    return Ok($"Switched {switchName} to {logLevel}");
                case LogLevel.None:
                    _dinamicallyLogSwitcherService.Switch(switchName, new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Fatal));
                    return Ok($"Switched {switchName} to {logLevel}");
                default:
                    return BadRequest($"Log level {logLevel} is not valid");
            }
        }
        [HttpGet("{switchName}")]
        public IActionResult Get(string switchName)
        {
            return Ok(_dinamicallyLogSwitcherService.GetSwitch(switchName));
        }
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(_dinamicallyLogSwitcherService.GetAllSwitchNames());
        }
    }

The controller shows me the log level changed, but It not reflects the changes. Am I missing something?

0xced commented 1 year ago

Your IDinamicallyLogSwitcherService.Switch() method is incorrect. It should take a LogEventLevel instead of a LoggingLevelSwitch and the implementation should read like this:

public void Switch(string switchName, LogEventLevel level)
{
    if (_switches.TryGetValue(switchName, out var levelSwitch))
        levelSwitch.MinimumLevel = level;
}

Then, in your LogLevelController, all calls must be updated like this:

[HttpPost("{switchName}/{logLevel}")]
public IActionResult Post(string switchName,  LogLevel logLevel)
{
    switch (logLevel)
    {
        case LogLevel.Trace:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Verbose);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.Debug:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Debug);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.Information:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Information);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.Warning:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Warning);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.Error:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Error);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.Critical:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Fatal);
            return Ok($"Switched {switchName} to {logLevel}");
        case LogLevel.None:
            _dinamicallyLogSwitcherService.Switch(switchName, LogEventLevel.Fatal);
            return Ok($"Switched {switchName} to {logLevel}");
        default:
            return BadRequest($"Log level {logLevel} is not valid");
    }
}

You may want to have a look at my WebSample branch where I have fully functional sample project very similar to yours but with a few improvements. For example, the log level switches (and log filter switches) are exposed as IReadOnlyDictionary instead of IDictionary in the ILogSwitchesAccessor interface, making it impossible to use incorrectly. Also, passing a non existing switch name produces an error. And finally, you can test logs directly in swagger with the logs/test route.

0xced commented 1 year ago

Closing since the question has been answered.