3

Is there a way to tell Serilog not to serialize null-valued properties of objects logged using the @ (destructuring) operator? I found a couple of posts on the topic (actually, I found more, but these two seemed more relevant with respect to the question and answers):

(1) Ignore null values when destructuring in Serilog is from 2019 and the only answer suggests there is no way to do it. (2) Ignore null properties from being written to output? is also from 2019, but it suggests that there may be a way to do it using a custom ILogEventEnricher, but there are no pointers on how to actually do it (it also refers to json formatting ignore null properties,l but I'm not sure if this post refers to formatting objects or using JSON formatter to produce log entries in JSON format).

We are currently using Console sink with the Serilog.Templates.ExpressionTemplate, Serilog.Expressions formatter (we use it in combination with a custom enrichers that escape new lines because Azure does not like new lines in log entries), and the File sink with the default formatter. To ignore null values in the output, we use a custom extension method ToJson(), to serialize the objects as simple string values (and we're using JSON.NET):

public static string? ToJson
(
    this object data
)
{
    JsonSerializer json = new()
    {
        // Do not change the following line or it may fail to serialize hierarchical data.
        PreserveReferencesHandling = PreserveReferencesHandling.Objects, 
        NullValueHandling = NullValueHandling.Ignore,
        DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffZ",
        Formatting = Formatting.None,
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        TypeNameHandling = TypeNameHandling.None,
    };

    StringWriter textWriter = new();

    json.Serialize(textWriter, data);

    return textWriter.ToString();
}

So, if we have an object like:

{"propA":"valueA","propB":null,"propC":"valueC"}

and we log it as:

logger.LogDebug("Data: {data}", data.ToJson());

the output will be:

Data: {"propA":"valueA","propC":"valueC"}

But if we log is as:

logger.LogDebug("Data: {@data}", data);

the output will be:

Data: {"propA":"valueA","propB":null,"propC":"valueC"}

How do we make logging with the @ (destructuring) operator not serialize properties with null values?

2
  • I found this comment from 2019 on the closed issue json formatting ignore null properties #1286: there's currently no plan to introduce any customizability into the JSON formatters, since the JsonValueFormatter class makes it easy to create new formatters with whatever behavior is required. Copying the code for RenderedCompactJsonFormatter into your app as a new class, and modifying its internal behavior, is the way to go, here. Commented Apr 15, 2024 at 20:00
  • I saw it, but RenderCompactJsonFormatter seems (and I may be wrong, but that's how I read it) to apply to JSON output (i.e. the whole log entry, not just an object). Also, as I mentioned, we already use the ExpressionTemplate formatter, so how would we add nother formatter on top of the one we're using? Maybe I'm missing something obvious, but there are no details on how to actually achieve this. Commented Apr 15, 2024 at 20:23

3 Answers 3

2

I had a similar need and was surprised that this wasn't an out of the box capability. Makes me wonder if I'm looking at things wrong but following the guidance in the closed issue, I created a new formatter. I didn't want to copy & modify the existing one (and then have to worry about patching), favoring to derive from the existing one instead. Its use of private members makes me think the developer didn't consider this possibility. 🤷‍♀️

This just filters out the null scalar values from structures, I think it does what you're looking for. There is a small performance hit of course but it shouldn't be too bad.

namespace Serilog.Formatting.Compact
{
    public class CompactJsonExtantFormatter : CompactJsonFormatter
    {
        public CompactJsonExtantFormatter(JsonValueFormatter valueFormatter = null) :
            base(valueFormatter ?? new JsonExtantValueFormatter(typeTagName: "$type"))
        { }
    }
    public class JsonExtantValueFormatter : JsonValueFormatter
    {
        public JsonExtantValueFormatter(string typeTagName) :
            base(typeTagName)
        { }

        protected override bool VisitStructureValue(TextWriter state, StructureValue structure)
        {
            List<LogEventProperty> extantProperties = new List<LogEventProperty>();
            foreach (var property in structure.Properties)
            {
                if (!(property.Value is ScalarValue scalarValue) || !(scalarValue.Value is null))
                {
                    extantProperties.Add(property);
                }
            }

            if (extantProperties.Count == structure.Properties.Count)
            {
                return base.VisitStructureValue(state, structure);
            }

            return base.VisitStructureValue(state, new StructureValue(extantProperties, structure.TypeTag));
        }
    }
}

To use your example, give class Data:

public class Data
{
    public string propA { get; set; } = "valueA";
    public string propB { get; set; }
    public string propC { get; set; } = "valueC";
} 

When logging such an object as a property:

var data = new Data();
Log.Debug("Data: {@data}", data);
data.propB = "valueB";
Log.Debug("Data: {@data}", data);

Serilog, using the above formatter, will skip data.propB when the value is null.

{"@t":"2024-05-16T20:46:43.2929590Z","@mt":"Data: {@data}","@l":"Debug","data":{"propA":"valueA","propC":"valueC","$type":"Data"}}
{"@t":"2024-05-16T20:46:43.3607135Z","@mt":"Data: {@data}","@l":"Debug","data":{"propA":"valueA","propB":"valueB","propC":"valueC","$type":"Data"}}
Sign up to request clarification or add additional context in comments.

4 Comments

I assume there must be some code or configuration to tell the app to consume the new formatter, right?
Yes. Configuration can be done in different ways with Serilog but wherever you would have put CompactJsonFormatter, you put your class instead. github.com/serilog/serilog-formatting-compact
I think this example illustrates how to output log events in the JSON format (i.e. the whole record is in JSON). And my question about converting objects to JSON when using a placeholder with @ notation (so, the complete log event entry in not in JSON).
I updated the answer with an example which shows how this addresses the question about Serilog and deconstruction. If you are using one of the Text formatters, you could use the same technique to derive from it.
2

If adding more packages is not an issue, Destructurama is your friend.

From their README:

Ignore null properties can be globally applied during logger configuration without need to apply attributes:

var log = new LoggerConfiguration()
  .Destructure.UsingAttributes(x => x.IgnoreNullProperties = true)
  ...

Comments

1

Based on @Donald Byrd's answer, I have created a more optimized version. There's no need to recreate the entire structure—just ignore default or empty values.

public class CompactJsonExtantFormatter : CompactJsonFormatter
{
    /// <inheritdoc />
    public CompactJsonExtantFormatter(JsonValueFormatter? valueFormatter = null) :
        base(valueFormatter ?? new JsonExtantValueFormatter(typeTagName: "$type"))
    { }
}
/// <inheritdoc />
public class JsonExtantValueFormatter : JsonValueFormatter
{
    private readonly string? _typeTagName;

    /// <inheritdoc />
    public JsonExtantValueFormatter(string typeTagName) :
        base(typeTagName)
    {
        _typeTagName = typeTagName;
    }
    
    /// <inheritdoc />
    protected override bool VisitStructureValue(TextWriter state, StructureValue structure)
    {
        state.Write('{');
        char? delim = null;
        foreach (var prop in structure.Properties)
        {
            if (IsDefaultValue(prop.Value))
                continue;
            if (delim != null)
                state.Write(delim.Value);
            delim = ',';
            WriteQuotedJsonString(prop.Name, state);
            state.Write(':');
            Visit(state, prop.Value);
        }
        if (_typeTagName != null && structure.TypeTag != null)
        {
            if (delim != null)
                state.Write(delim.Value);
            WriteQuotedJsonString(_typeTagName, state);
            state.Write(':');
            WriteQuotedJsonString(structure.TypeTag, state);
        }
        state.Write('}');
        return false;
    }

    private static bool IsDefaultValue(LogEventPropertyValue value)
    {
        return value switch
        {
            ScalarValue { Value: null } => true,
            ScalarValue { Value: string s } when string.IsNullOrEmpty(s) => true,
            ScalarValue { Value: 0 } => true,
            ScalarValue { Value: 0L } => true,
            ScalarValue { Value: 0.0 } => true,
            ScalarValue { Value: 0.0f } => true,
            ScalarValue { Value: 0m } => true,
            ScalarValue { Value: false } => true,
            SequenceValue seq => seq.Elements.Count == 0,
            DictionaryValue seq => seq.Elements.Count == 0,
            StructureValue structVal => structVal.Properties.Count == 0,
            _ => false
        };
    }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.