serilog / serilog-expressions

An embeddable mini-language for filtering, enriching, and formatting Serilog events, ideal for use with JSON or XML configuration.
Apache License 2.0
190 stars 17 forks source link

Document on how to support CamelCase #86

Open luddskunk opened 1 year ago

luddskunk commented 1 year ago

Is your feature request related to a problem? Please describe. I'd like to format my JSON with camelcase properties, since I am using a backend for visualization of logs (Grafana Loki) which adheres to that standard. The possibility to do so is also mentioned in https://nblumhardt.com/2021/06/customize-serilog-json-output/

Describe the solution you'd like Use Serilog Expression to create CamelCase keys in JSON body.

Describe alternatives you've considered I tried creating my own custom resolver to do this, but I didn't manage to get it correctly.

new ExpressionTemplate(
    "{{ \"timestamp\": \"{UtcDateTime(@t)}\", \"message\": \"{@m}\", \"level\": \"{@l}\", \"exception\": \"{@x}\",\n" +
    " {#each name, value in @p} \"{IsCamelCase(name)}\": " +
    "{#if name = 'ExceptionDetail'}" +
    "{value:j},"+
    "{#else}"+
    "\"{value}\"," +
    "{#end}{#end} }}\n"
    , nameResolver: CamelCaseResolvers))

I think this also quite quickly became too complex from maintainability standpoint.

What I want to achieve

{
    "Timestamp": "2022-11-17T07:58:15.6253633Z",
    "Message": "An unhandled exception has occurred while executing the request.",
    "Level": "Error",
    "Exception": "Exception Text"
}

should be

{
    "timestamp": "2022-11-17T07:58:15.6253633Z",
    "message": "An unhandled exception has occurred while executing the request.",
    "level": "Error",
    "exception": "Exception Text"
}

Additional context As discussed with @nblumhardt on Twitter, I open my thread here.

Hope to get any good insights for finding a smart solution I might've overlooked!

nblumhardt commented 1 year ago

Here's my first attempt; MakeCamelCase deals with runs of leading capitals but could still need a few more test cases to shake out bugs :-)

using System.Diagnostics.CodeAnalysis;
using Serilog.Events;

namespace Sample;

public static class CamelCaseFunctions
{
    [return: NotNullIfNotNull("value")]
    public static LogEventPropertyValue? ToCamelCase(LogEventPropertyValue? value)
    {
        return value switch
        {
            null => null,
            DictionaryValue dictionaryValue => new DictionaryValue(dictionaryValue.Elements.Select(kvp => KeyValuePair.Create(kvp.Key, ToCamelCase(kvp.Value)))),
            ScalarValue scalarValue => scalarValue,
            SequenceValue sequenceValue => new SequenceValue(sequenceValue.Elements.Select(ToCamelCase)),
            StructureValue structureValue => new StructureValue(
                structureValue.Properties.Select(prop => new LogEventProperty(MakeCamelCase(prop.Name), ToCamelCase(prop.Value))),
                structureValue.TypeTag),
            _ => throw new ArgumentOutOfRangeException(nameof(value))
        };
    }

    static string MakeCamelCase(string s)
    {
        if (s.Length == 0) return s;

        var firstPreserved = s.Length + 1;
        for (var i = 1; i < s.Length; ++i)
        {
            if (char.IsUpper(s[i])) continue;
            firstPreserved = i;
            break;
        }

        return s[..(firstPreserved - 1)].ToLowerInvariant() + (firstPreserved <= s.Length ? s[(firstPreserved - 1)..] : "");
    }
}

Enable it with nameResolver: new StaticMemberNameResolver(typeof(CamelCaseFunctions)) and call it by wrapping toCamelCase() around any object literal in the template:

            .WriteTo.Console(new ExpressionTemplate(
                "{ toCamelCase({@t: UtcDateTime(@t), @mt, @l: if @l = 'Information' then undefined() else @l, @x, UITest: 42, FUN: 8, IPhone: 13, ..@p}) }\n",
                nameResolver: new StaticMemberNameResolver(typeof(CamelCaseFunctions))))

Would love to hear how you go!