Search code examples
c#unity-game-engineserilog

Serilog access original object in LogEvent that was passed to logging statement


I am using Serilog in Unity3D. I have a simple sink that writes Serilog logging statements to Debug.LogFormat of Unity:

public class UnityLogEventSink : ILogEventSink
{
    public void Emit(LogEvent logEvent)
    {
        // QUESTION: How to pass a UnityEngine.Object to Serilog logging statement such that it is available here?
        UnityEngine.Object contextObject = null;
            
        using (StringWriter stringBuffer = new StringWriter())
        {
            GetTextFormatter().Format(logEvent, stringBuffer);
            LogType logType = GetUnityLogType(logEvent);
            string logString = stringBuffer.ToString().Trim();
            Debug.LogFormat(logType, LogOption.NoStacktrace, contextObject, logString);
        }
    }
    
    // GetTextFormatter, GetUnityLogType etc. are defined here ...
}

Now I want to pass a GameObject to the Serilog logging statement, such that I can access this GameObject in my sink. (Calling Debug.LogFormat with a GameObject will highlight the object in the Unity Editor when the log message is clicked. I want that.)

// Example what I have in mind (not working):
logger.ForContext("unityObject", gameObject).Information("This is an info with context");

I tried to wrap the GameObject in a ScalarValue, and a custom LogEventPropertyValue, but the GameObject is still converted to a string (happens in Serilog's PropertyValueConverter.cs).

I need the original GameObject instance for Debug.LogFormat. Is there a way to preserve the GameObject reference so I can use it in my sink?

As a workaround, I could store the reference in a static map and log a string property with the key for the map. This way I could fetch the instance from that map later in the sink. But this is working around Serilog. Is there a better solution that utilizes Serilog?


Solution

  • Nick answered this very question for me in https://github.com/serilog/serilog/issues/1124

    public class ScalarValueEnricher : ILogEventEnricher
    {
        protected readonly LogEventProperty _prop;
    
        public ScalarValueEnricher(string name, object value)
        {
            _prop = new LogEventProperty(name, new ScalarValue(value));
        }
    
        public void Enrich(LogEvent evt, ILogEventPropertyFactory _) =>
             evt.AddPropertyIfAbsent(_prop);
    }
    

    (Here it is in context, in F#)

    Could also create a Unity specific subclass:

    public class UnityObjectEnricher : ScalarValueEnricher
    {
        public static readonly string unityObjectPropertyName = "unityObject";
        public UnityObjectEnricher(UnityEngine.Object value)
            : base(unityObjectPropertyName, value)
        {
        }
    }
    

    This property can then be accessed in the sink:

    private UnityEngine.Object GetUnityEngineContextObject(LogEvent logEvent)
    {
        if (logEvent.Properties.TryGetValue(UnityObjectEnricher.unityObjectPropertyName, out LogEventPropertyValue logEventPropertyValue))
        {
            if (logEventPropertyValue is ScalarValue scalarValue)
                return scalarValue.Value as UnityEngine.Object;
        }
        return null;
    }
    

    Use it like:

    // Note: using LogContext requires Serilog configuration ".Enrich.FromLogContext()"
    using (LogContext.Push(new UnityObjectEnricher(gameObject)))
    {
        logger.Information("This is an info with context");
    }
    

    Or:

    logger.ForContext(new UnityObjectEnricher(gameObject)).Information("This is another info with context");