serilog-expressions icon indicating copy to clipboard operation
serilog-expressions copied to clipboard

Document on how to support CamelCase

Open luddskunk opened this issue 2 years ago • 1 comments

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!

luddskunk avatar Nov 18 '22 07:11 luddskunk

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!

nblumhardt avatar Nov 22 '22 00:11 nblumhardt