Search code examples
c#loggingtostringserilogdestructuring

Is there a way to provide custom property formatting with Serilog?


I have a class BackgroundTask that I have set up to log events when things happen, such as when the task completes. For example, for the Success case, I call Log.Verbose("{Task} completed successfully", this); My default ToString() includes the progress, but for a completed task we know it's 100% so I would like to ignore it. I know that with numeric types you can pass in a custom format specifier string (like "{IntProperty:p5}", which would render my int as a percentage with 5 decimal places), and I would like to be able to do the same, such as Log.Information("{Task:Name}", this), which would pass in "Name" into my ToString() method.

I've tried adding lots of different methods like adding ToString()'s, (taking in nothing, a string, a string and IFormatProvider), implementing IFormattable, forcing stringification etc and nothing seems to work. I can see that Serilog is correctly parsing my format string: (PropertyBinder.cs, ConstructNamedProperties() line 111) Debug view of serilog code

This calls ConstructProperty() which just ignores the Format property of the token, which explains why it's being ignored, but I was wondering if there was a way that would work that I haven't thought of.

PS Yes I'm aware I have several options I could do, but I'd rather not do these:

  1. Use a custom destructurer or manually pulling out the properties myself into an anonymous type - This essentially destroys the original object, which is exactly what I don't want (I still want the original value stored as a property). E.g. Log.Information("{Task}", new {Name = this.Name, Id = this.Id});
  2. Manually call ToString() with my own format string - Same as (1), this destroys the original, and means it won't be stored with all it's information. E.g. Log.Information("{Task}", this.ToString("Custom Format"));
  3. Create a property on my object like ToStringFormat before passing it into Serilog - This seems bad practice and just adds extra clutter, not to mention the concurrency issues. E.g. this.Format = "Custom FOrmat"; Log.Information("{Task}", this);

Solution

  • This is due to the split between capturing and formatting in the Serilog pipeline.

    In order for format strings like :X to be processed when rendering to a sink, the original object implementing IFormattable needs to be available.

    But, because sinks often process events asynchronously, Serilog can't be sure that any given logged object is thread-safe, and so any unknown types are captured at the time/site of logging using ToString().

    To get around this, you need to tell Serilog that your Point class is an (essentially immutable) value type with:

        .Destructure.AsScalar(typeof(Point))
    

    when the logger is configured. You can then implement IFormattable on Point and use {Point:X} etc. in templates.