Lee Crabtree
Lee Crabtree

Reputation: 1256

How do I get arbitrary objects to serialize as JSON in Serilog using ILogger scopes?

I'm creating logs using the normal ILogger logger, and am attempting to output to Serilog. The actual output works, but when I add an arbitrary object through ILogger<T>.BeginScope, it shows up in the log as "TestApp.Program+TestLoggableType" instead of the JSON representation.

I've got the Serilog template setup like this:

return loggerConfiguration.WriteTo.Console(new ExpressionTemplate(
  "{ { timestamp:@t, message:@m, messageTemplate:@mt, level:@l, exception:@x, context:@p } }\n"
));

And here is the type I'm adding to the scope:

public class TestLoggableType
{
  public string Name { get; }
  public int Thing { get; }

  public TestLoggableType(string name, int thing)
  {
    Name = name;
    Thing = thing;
  }
}

And I'm adding it to the scopes like this:

Dictionary<string, object?> scopes = new()
{
  { "context", new TestLoggableType("Blah", 1) }
};

using (logger.BeginScope(scopes))
{
  logger.LogError(testException, "Error");
}

With all that setup, I get a log message like this:

{"timestamp":"2023-09-27T11:24:18.0184980-05:00","message":"Error","messageTemplate":"Error","level":"Error","exception":"System.Exception: Message","context":{"SourceContext":"Microsoft.Extensions.Hosting.IHost","context":"TestApp.Program+TestLoggableType"}}

I've tried setting up a user-defined function, but the property value is already a string by the time my function sees it, so I have no way to serialize the object.

Am I missing something? This seems like something that should be pretty simple, but I just can't figure it out looking through the docs. I can't give the expression template to JsonFormatter or anything, so I'm stumped as to how to control serialization of arbitrary types.

EDIT

Here is the full text of my little example project.

public record TestLoggableRecord(string Name, int Value);

public class TestLoggableType
{
    public string Name { get; }
    public int Thing { get; }

    public TestLoggableType(string name, int thing)
    {
        Name = name;
        Thing = thing;
    }
}

public class CustomException : Exception
{
    public object? Context { get; }
    public bool IsRetryable { get; }

    public CustomException(string message, object? context = null, bool isRetryable = false)
        : base(message)
    {
        Context = context;
        IsRetryable = isRetryable;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        Exception testException = new("Message", new TestLoggableRecord("Blah", 1));
    
        IHost host = Host.CreateDefaultBuilder()
            .UseSerilog((_, loggerConfiguration) =>
            {
                loggerConfiguration
                    .Enrich.FromLogContext()
                    .WriteTo
                    .Console(new ExpressionTemplate("{ { timestamp:@t, message:@m, messageTemplate:@mt, level:@l, exception:@x, context:@p } }\n"));
            })
            .Build();

        ILogger<IHost> logger = (ILogger<IHost>)host.Services.GetService(typeof(ILogger<IHost>))!;

        Dictionary<string, object?> scopes = new()
        {
            { "context", new TestLoggableType("Blah", 1) }
        };

        using (logger.BeginScope(scopes))
        {
            logger.LogError(testException, "Error");
        }
    }
}

In addition, I'm using Serilog.Expressions/3.4.1, Serilog.Extensions.Hosting/7.0.0, and Serilog.Sinks.Console/4.1.0.

The output from this is:

{"timestamp":"2023-09-27T14:10:35.2964020-05:00","message":"Error","messageTemplate":"Error","level":"Error","exception":"JsonNodeSerialize.Program+CustomException: Message","context":{"SourceContext":"Microsoft.Extensions.Hosting.IHost","context":"JsonNodeSerialize.Program+TestLoggableType"}}

I also just noticed that it didn't serialize the extra fields on the exception, either.

Upvotes: 1

Views: 976

Answers (1)

user22711245
user22711245

Reputation: 36

Prefix the key in your dictionary with an ampersand (@) to get Serilog to destructure the value, just like you would in a log message template:

Dictionary<string, object?> scopes = new()
{
    { "@context", new TestLoggableType("Blah", 1) }
};

I'll be darned if I can find the reference now, but I'm pretty sure it's documented somewhere.

Upvotes: 2

Related Questions