6

My Serilog configuration code looks like this:

Log.Logger = new LoggerConfiguration()
    .Enrich.WithExceptionDetails()
    .Enrich.FromLogContext()
    .MinimumLevel.Warning()
//  .MinimumLevel.Override("Microsoft", LogEventLevel.Verbose)
//  .MinimumLevel.Override("System", LogEventLevel.Verbose)
//  .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Verbose)
    .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate )
//  .WriteTo.File()
    .CreateLogger();

I'd like to change this configuration at runtime, unfortunately because Serilog uses a "fluent" style of API it makes it somewhat messy. For example, if I want to enable or disable console and file logging at runtime:

Boolean enableConsoleLogging = ...
Boolean enableFileLogging = ...

LoggerConfiguration builder = new LoggerConfiguration()
    .Enrich.WithExceptionDetails()
    .Enrich.FromLogContext()
    .MinimumLevel.Warning();

if( enableConsoleLogging )
{
    builder = builder
        .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate )
}

if( enableFileLogging )
{
    builder = builder
        .WriteTo.File( ... )
}
Log.Logger = builder.CreateLogger();

...which is not exactly elegant.

I know I could add my own If extension method (but I'm not keen on extending an existing API design like that, even if it does look prettier):

Log.Logger = new LoggerConfiguration()
    .Enrich.WithExceptionDetails()
    .Enrich.FromLogContext()
    .MinimumLevel.Warning()
    .If( enableConsoleLogging, b => b.WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate ) )
    .If( enableFileLogging, b => b.WriteTo.File( ... ) )
    .CreateLogger();


public static LoggerConfiguration If( this LoggerConfiguration cfg, Boolean test, Func<LoggerConfiguration,LoggerConfiguration> action )
{
    if( test ) return action( cfg );
    else       return cfg;
}

What alternatives exist to toggle different Serilog options at runtime? Are there any approaches that can be used with other "Fluent" APIs?

Dai
  • 141,631
  • 28
  • 261
  • 374
  • Methods on `LoggerConfiguration` return the same builder object, so there's no need to reassign builder with `builder = builder...` (even though this would technically future-proof it, there's zero chance we'll change this aspect of the configuration system at this point). HTH! – Nicholas Blumhardt Oct 25 '18 at 21:30
  • @NicholasBlumhardt Thank you for the clarification. I thought "Builder"-style APIs *necessarily* return a _new_ builder object with the updated state instead of mutating a shared instance. That said, I would have preferred it if Serilog used a conventional property-based configuration API. Are you accepting pull-requests? :D – Dai Oct 25 '18 at 21:32
  • :-) .. in Serilog's case, and in a few other familiar examples like _System.Collections.Immutable_, the "builder" part is the mutable side of the API, and the action of "building" freezes whatever you've configured it with into a final, immutable form. – Nicholas Blumhardt Oct 25 '18 at 21:41

2 Answers2

9

Serilog 2.9.0 introduces conditional sinks. Using .WriteTo.Conditional you specify the condition that defines if the sink will be written to or not.

e.g.

bool enableConsoleLogging = ...
bool enableFileLogging = ...

var builder = new LoggerConfiguration()
    .Enrich.WithExceptionDetails()
    .Enrich.FromLogContext()
    .MinimumLevel.Warning()
    .WriteTo.Conditional(evt => enableConsoleLogging, wt => wt.Console())
    .WriteTo.Conditional(evt => enableFileLogging, wt => wt.File(...));

Log.Logger = builder.CreateLogger();
// ...
C. Augusto Proiete
  • 24,684
  • 2
  • 63
  • 91
  • Clean! I am having issues with the JSON config. What would this look like if I wanted to write Errors to wt.File and Debug to wt.Console? – Mike W Nov 19 '20 at 00:10
  • @MikeW That's different than on/off, so you'd have to use either sub-loggers or `Serilog.Sinks.Map` - examples here: https://stackoverflow.com/a/62069498/211672 – C. Augusto Proiete Nov 19 '20 at 00:15
  • This makes a very easy way to turn the annoying {NewLine} into a conditional template by just checking evt=>evt.Exception!=null. I was getting so annoyed with all the blank newlines in my debug window. – jmichas Jun 13 '21 at 03:02
  • If put the condition inside curly braces, you can easily add breakpoints there too and step in at runtime. Example: WriteTo.Conditional(evt => { bool isWarning = evt.Level == LogEventLevel.Warning; return isWarning; }, wt => wt.Consolt()); – Tim Pickin Mar 25 '22 at 14:46
3

I think to make it elegant and still do it in code, you do have to extend the API and create your own extension methods that encapsulate the condition checks and update the builder with the correct sink and parameters.

Something like

Log.Logger = new LoggerConfiguration()
    .Enrich.WithExceptionDetails()
    .Enrich.FromLogContext()
    .MinimumLevel.Warning()
    .WriteToConsoleIfEnabled()  // <---
    .WriteToFileIfEnabled()     // <---
    .CreateLogger();

On a different note, have you considered using Serilog.Settings.AppSettings or Serilog.Settings.Configuration instead? The configuration in code gets much cleaner, and you can add/remove sinks in the configuration file as you wish...

Log.Logger = new LoggerConfiguration()
  .ReadFrom.AppSettings()
  .CreateLogger()

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="serilog:minimum-level" value="Verbose" />

    <add key="serilog:using:Console" value="Serilog.Sinks.Console" />
    <add key="serilog:write-to:Console" />

    <add key="serilog:using:RollingFile" value="Serilog.Sinks.RollingFile" />
    <add key="serilog:write-to:RollingFile.pathFormat" value="C:\myapp-{Date}.txt" />
    <add key="serilog:write-to:RollingFile.retainedFileCountLimit" value="10" />

    <!-- //etc... -->
  </appSettings>
</configuration>
C. Augusto Proiete
  • 24,684
  • 2
  • 63
  • 91
  • Thank you for the tip. I also see `serilog-settings-configuration` for .NET Core too. – Dai Oct 25 '18 at 22:50
  • Was wondering what the impl of the WriteToConsoleIfEnabled() would look like? The reason I ask is that I took your idea and ran with it, but what happens when the sink is disabled, what do you pass on to the next sink? – Stephen Patten Feb 26 '19 at 03:50
  • @StephenPatten I'm not sure I understand your question, but the implementation would be just a check to some config variable, environment variable, etc. to decide if we should write to the console and then adding (or not adding) the Console Sink to the configuration. `if (isConsoleSinkEnabled) configuration.WriteTo.Console(...)` – C. Augusto Proiete Mar 01 '19 at 00:10
  • Yeah, but you still don't get conditional logging, some things i want in email, somethings in txt logs, etc. Why can't we just create a base logger .ReadFrom.AppSettings() and then do a logger.ForContext(typeof(MyClass)).WriteTo.Email(); and then that context logger writes to Email addionally .. – Brunis May 28 '20 at 12:44
  • 1
    @Brunis The OP was asking for conditional logging _configuration_ at the time of the set up of the logging pipeline. You're asking about conditional logging at run-time. For that you can use sub-loggers or the Map sink. Take a look at this answer: https://stackoverflow.com/a/61102200/211672 – C. Augusto Proiete May 28 '20 at 13:20