KuraiAndras / Serilog.Sinks.Unity3D

Serilog Sink for Unity 3D Debug log
MIT License
45 stars 9 forks source link

Add support for Serilog.Extensions.Logging #12

Open krisrok opened 9 months ago

krisrok commented 9 months ago

Based on the discussion in #9, here's support for using UnityEngine.Objects with BeginScope() and using the latest pushed scope as context when sending following log entries to Unity's console window.

This way, log entries are clickable and e.g. GameObjects get pinged in the editor.

On key design choice from a user's perspective was to avoid additonal APIs or dependencies. User code only needs to reference Microsoft.Extensions.Logging.ILogger and work with the abstractions. Everything else is done upfront via configuration.

Example

Config:

Serilog.Core.Logger logger = new Serilog.LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .WriteTo.Unity3D()
    .EnableUnityObjectScope() // Important to add!
    .CreateLogger();

var loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory()
    .AddSerilogWithUnityObjectScope(logger, dispose: true); // Also important! Use this instead of .AddSerilog(...)

Usage:

// Grab a logger manually for the example's sake. Usually done via DI.
var log = loggerFactory.CreateLogger("SomeCategory");

using (var scope = log.BeginScope(gameObject))
{
    log.LogInformation($"You can click me to ping {gameObject.name}"); // Produces a clickable log entry inside this scope
}

log.LogInformation($"Now you can _not_ click me to ping {gameObject.name}");

Async/await support

It also works with async/await code as long as it is managed by Unity's main thread. UniTask should work fine, too.

async Task LogFromTask(UnityEngine.Object unityContext, string msg)
{
    using var scope = log.BeginScope(unityContext);

    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(TimeSpan.FromMilliseconds(300));
        log.LogInformation(msg);
    }
}

var t1 = LogFromTask(gameObject, "T1");
var t2 = LogFromTask(Camera.main, "T2");

await Task.WhenAll(t1, t2); // Both task log in parallel with separate scopes

No thread safety for UnityEngine.Objects

This stems from the known Unity limitation: Access to all/most members of its managed-lifecycle objects is limited to the main thread.

So when calling e.g. BeginScope(gameObject) on other threads the library throws an NotSupportedException to keep it from failing somewhere else down the line (like in enrichers or sinks).

Everything else works fine from other threads.

Optional integration

The new functionality lives in a separate assembly (Serilog.Sinks.Unity3D.Extensions.Logging). The assembly definition makes sure it only compiles when the dependencies are in place, namely the package org.nuget.serilog.extensions.logging (which in turn depends on Serilog and Microsoft's abstractions).

If the dependencies are not found, the base sink (Serilog.Sinks.Unity3D) still works just like before.

Installation via manifest.json

{
  "scopedRegistries": [
    {
      "name": "Unity NuGet",
      "url": "https://unitynuget-registry.azurewebsites.net",
      "scopes": [
        "org.nuget"
      ]
    }
  ],
  "dependencies": {
    "org.nuget.serilog.extensions.logging": "8.0.0",
    "com.serilog.sinks.unity3d": "https://github.com/krisrok/Serilog.Sinks.Unity3D.git?path=/Serilog.Sinks.Unity3D/Assets/Serilog.Sinks.Unity3D#3.0.0-ext",
    ...
Rabadash8820 commented 7 months ago

I'm not a maintainer, but as a listener on #9, this looks pretty cool! A few thoughts:

Pinging @KuraiAndras for visibility on this PR.

KuraiAndras commented 7 months ago

The earliest when I can take a look is around sunday