Search code examples
c#serilog

Serilog - Remove enricher from Log Configuration at runtime?


I have a Serilog Enricher that can sometimes crash, as it's trying to do something that might not be possible to get an item added to a log entry.

When this happens, i can ignore the exception, sure. However, i'd like to notify in the logs that this happened, and remove the Enricher from the current configuration.

Initially, i was just going to log something when an exception is caught - but that is then Enriched by all enrichers, incuding the one that has just tried to log something. Thus, we get an infinite loop that will never resolve.

To get around this, i wanted to remove the Enricher from the LogContext in order to log an item with working enrichers, and then not worry about it any more.

Here is my example Enricher:

public class PotentiallyCrashingEnricher : ILogEventEnricher
    {
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
        {
            try
            {
                var myString = "Something i want to log"
                property = propertyFactory.CreateProperty("MyProperty", myString);
                logEvent.AddPropertyIfAbsent(property);
                throw new Exception("Oh no...");
            }
            catch (Exception e)
            {
                var oldContext = LogContext.Clone();
                LogContext.Reset();
                Log.ForContext(typeof(PotentiallyCrashingEnricher)).Information($"{e.Message}");
                LogContext.Push(oldContext);
            }
        }
    }

In the catch block you can see what I attempted - but I don't think this was ever supposed to work the way i've understood it. Is there a way that this can be done?


Solution

  • The LoggerConfiguration is a builder, and once we've called .CreateLogger() we can't really do much to change it. But fret not, it's not impossible to get around.


    Option 1: Rebuild the Config

    This is probably overkill, and very inflexible, but it does satisfy your comment of "i wanted to remove the Enricher from the LogContext in order to log an item with working enrichers, and then not worry about it any more"

    Since we can't remove the Enricher, we'll just rebuild the whole config without it.

    void BuildLogger(bool enrichWithCrashingThing = true) {
      var cfg = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Console() 
        //...etc
        ;
    
      if (enrichWithCrashingThing)
        cfg.Enrich.With<PotentiallyCrashingEnricher>();
    
      // (Re)create the logger
      Log.Logger = cfg.CreateLogger();
    }
    
    // .. On app start
    BuildLogger();
    

    And in your Enricher, upon catching the exception, just trigger a rebuild.

    catch (Exception e) {
      BuildLogger(false);
      Log.ForContext(this.GetType()).Error(e, "Enricher has crashed");
    }
    

    This will forever remove the Enricher from the global logger.


    Option 2: Conditional enrichment

    This is probably the better option. All you need is a global flag, and tell Serilog to check for it when enriching log events.

    var cfg = new LoggerConfiguration()
      .Enrich.FromLogContext()
      .Enrich.When(  // Conditional enrichment only when flagged as ok
        evt => PotentiallyCrashingEnricher.IsOk,
        enrich => enrich.With<PotentiallyCrashingEnricher>()
      )
      .WriteTo.Console()
      //...etc
      ;
    Log.Logger = cfg.CreateLogger();
    

    And your Enricher with the static flag:

    public class PotentiallyCrashingEnricher : ILogEventEnricher 
    {
      public static bool IsOk { get; private set; } = true; // Start as OK
    
      public void Enrich(LogEvent evt, ILogEventPropertyFactory factory) {
        try {
          //...
        }
        catch (Exception e) {
          IsOk = false; // Not OK!
          Log.ForContext(this.GetType()).Error(e, "Enricher has crashed");
          // We can re-enable the enricher again, if desired:
          // IsOk = true;
        }
      }
    }
    

    With this, Serilog will check the flag every time an event is logged.

    Yes, it's very slightly less efficient than the rebuild-and-forget Option 1 (at the minuscule cost of an if statement on every event). But if you never set IsOk back to true, the JIT will likely optimise it out eventually.

    And you have the option to turn it back on.