Search code examples
c#.netdatetimeserilog

How to format all datetime entries using ExpressionTemplate in Serilog?


I'm currently using ExpressionTemplate for custom log formatting, but can't find proper way to format all entries with datetime type.

builder.Host.UseSerilog((context, services, configuration) => 
{
    configuration.ReadFrom.Configuration(context.Configuration);
    configuration.ReadFrom.Services(services);
    configuration.Enrich.FromLogContext();
    configuration.WriteTo.Console(new ExpressionTemplate("{ {Time:ToString(UtcDateTime(@t),'yyyy-MM-dd HH:mm:ss.ff'), @mt, @r, @l, @x, ..@p} }\n"));
});

...

using(var scope = app.Services.CreateScope())
{
    var logger = scope.ServiceProvider.GetService<ILogger<Program>>();
    logger.LogInformation("{@Now}, {@UtcNow}", DateTime.Now, DateTime.UtcNow);
}

//{"Time":"2023-01-12 02:48:35.44","@mt":"{@Now}, {@UtcNow}","@l":"Information","Now":"2023-01-12T11:48:35.0203770+09:00","UtcNow":"2023-01-12T02:48:35.4406010Z","SourceContext":"Program"}

As you see, 'Time' is properly formatted using ToString, but the others are not, I want it to be like.. {"Time":"2023-01-12 02:48:35.44","@mt":"{@Now}, {@UtcNow}","@l":"Information","Now":"**2023-01-12 02:48:35.44**","UtcNow":"**2023-01-12 02:48:35.44**","SourceContext":"Program"}

I tried IFormatProvider to fix this,

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, services, configuration) => 
{
    configuration.ReadFrom.Configuration(context.Configuration);
    configuration.ReadFrom.Services(services);
    configuration.Enrich.FromLogContext();
    configuration.WriteTo.Console(new ExpressionTemplate("{ {Time:ToString(UtcDateTime(@t),'yyyy-MM-dd HH:mm:ss.ff'), @mt, @r, @l, @x, ..@p} }\n", new CustomDateFormat()));
});


public class CustomDateFormat : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      if (formatType == typeof(ICustomFormatter))
         return this;
      else
         return null;
   }

   public string Format(string fmt, object arg, IFormatProvider formatProvider)
   {
        if (arg is DateTime dt) return dt.ToString("yyyy-MM-dd HH:mm:ss.ff");

        if (arg is DateOnly dateOnly) return dateOnly.ToString("yyyy-MM-dd");

        if (arg is TimeOnly timeOnly) return timeOnly.ToString("HH:mm:ss.fff");

        //is timespan
        return ((TimeSpan)arg).ToString();
   }
}

But results were same. How can I format all datetime entries using ExpressionTemplate?


Solution

  • You can use ILogEventEnricher and create your own DateTimeFormatter Enricher.

    public class DateTimeFormatter : ILogEventEnricher
    {
        private readonly string _dateTimeFormat;
        public DateTimeFormatter(string dateTimeFormat)
        {
            _dateTimeFormat = dateTimeFormat;
        }
    
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
        {
            var properties = logEvent.Properties.ToList();
            foreach (var property in properties)
            {
                if (property.Value is ScalarValue scalarValue && scalarValue.Value is DateTime dateTime)
                {
                    logEvent.RemovePropertyIfPresent(property.Key);
                    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(property.Key, dateTime.ToString(_dateTimeFormat)));
                }
            }
        }
    }
    

    And pass the DateTimeFormatter in the LoggerConfiguration like this.

    builder.Host.UseSerilog((context, services, configuration) =>
    {
        configuration.ReadFrom.Configuration(context.Configuration);
        configuration.ReadFrom.Services(services);
        configuration.Enrich.FromLogContext();
        configuration.Enrich.With(new DateTimeFormatter("yyyy-MM-dd HH:mm:ss"));
        configuration.WriteTo.Console();
    });
    
    logger.Information("Now: {@Now}, UtcNow: {@UtcNow}", DateTime.Now, DateTime.UtcNow);