Skip to content

Add support for Serilog.Extensions.Logging #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "Serilog.Sinks.Unity3D.Extensions.Logging",
"rootNamespace": "",
"references": [
"GUID:ebcad7e3972724c43aa595f17cf4d103"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"Serilog.dll",
"Serilog.Extensions.Logging.dll",
"Microsoft.Extensions.Logging.Abstractions.dll"
],
"autoReferenced": true,
"defineConstraints": [
"HAS_SERILOG_EXTENSIONS_LOGGING"
],
"versionDefines": [
{
"name": "org.nuget.serilog.extensions.logging",
"expression": "",
"define": "HAS_SERILOG_EXTENSIONS_LOGGING"
}
],
"noEngineReferences": false
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#nullable enable
using System;

namespace Serilog.Sinks.Unity3D
{
public static class SerilogUnityContextScopeExtensions
{
/// <summary>
/// <para>
/// Enables <see cref="Microsoft.Extensions.Logging.ILogger.BeginScope{TState}(TState)"/> to play nicely with <see cref="UnityEngine.Object"/>.
/// This way you can use e.g. <code>log.BeginScope(gameObject)</code> and the log entry will be clickable in Unity's console window.
/// </para>
/// <para>
/// To be used in conjunction with <see cref="SerilogUnityContextScopeExtensions.AddSerilogWithUnityObjectScope(Microsoft.Extensions.Logging.ILoggerFactory, ILogger?, bool)"/>
/// when you set up your factory:
/// <code>
/// new Microsoft.Extensions.Logging.LoggerFactory().AddSerilogWithUnityObjectScope(logger, dispose: true)
/// </code>
/// </para>
/// </summary>
/// <param name="loggerConfiguration"></param>
/// <returns></returns>
public static LoggerConfiguration EnableUnityObjectScope(this LoggerConfiguration loggerConfiguration)
{
return loggerConfiguration
.Destructure.With<UnityObjectDestructuringPolicy>();
}

/// <summary>
/// <para>
/// Add Serilog to the logging pipeline and enable <see cref="UnityEngine.Object"/> to be used with <see cref="Microsoft.Extensions.Logging.ILogger.BeginScope{TState}(TState)"/>
/// </para>
/// </summary>
/// <param name="factory">The logger factory to configure.</param>
/// <param name="logger">The Serilog logger; if not supplied, the static <see cref="Serilog.Log"/> will be used.</param>
/// <param name="dispose">When true, dispose <paramref name="logger"/> when the framework disposes the provider. If the
/// logger is not specified but <paramref name="dispose"/> is true, the <see cref="Log.CloseAndFlush()"/> method will be
/// called on the static <see cref="Log"/> class instead.</param>
/// <param name="unityObjectScopeTransformer">Can optionally be used to transform <see cref="UnityEngine.Object"/>s to
/// something else when calling <see cref="Microsoft.Extensions.Logging.ILogger.BeginScope{TState}(TState)"/> with an
/// <see cref="UnityEngine.Object"/> argument. Influences how this object gets represented in the logger's scope.</param>
/// <returns>Reference to the supplied <paramref name="factory"/>.</returns>
public static Microsoft.Extensions.Logging.ILoggerFactory AddSerilogWithUnityObjectScope(
this Microsoft.Extensions.Logging.ILoggerFactory factory,
Serilog.ILogger? logger = null,
bool dispose = false,
UnityObjectTransformerDelegate? unityObjectScopeTransformer = null)
{
if (factory == null) throw new ArgumentNullException(nameof(factory));

factory.AddProvider(new SerilogUnityContextScopeLoggerProvider(logger, dispose, unityObjectScopeTransformer));

return factory;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#nullable enable
using Serilog.Context;
using System;
using System.Threading;
using UnityEngine;

namespace Serilog.Sinks.Unity3D
{
/// <summary>
/// A wrapper around <see cref="Serilog.Extensions.Logging.SerilogLogger"/>.
/// Keeps track of <see cref="UnityEngine.Object"/>s used as scope items.
/// </summary>
public class SerilogUnityContextScopeLogger : Microsoft.Extensions.Logging.ILogger
{
private static SynchronizationContext? _synchronizationContext;

#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
_synchronizationContext = SynchronizationContext.Current;
}

private readonly Microsoft.Extensions.Logging.ILogger _logger;
private readonly UnityObjectTransformerDelegate? _unityObjectTransformer;
private AsyncLocal<UnityContextScope?>? _unityContextScope;

private UnityContextScope? CurrentScope
{
get => _unityContextScope?.Value;
set => (_unityContextScope ??= new AsyncLocal<UnityContextScope?>()).Value = value;
}

public SerilogUnityContextScopeLogger(Microsoft.Extensions.Logging.ILogger logger, UnityObjectTransformerDelegate? unityObjectTransformer)
{
_logger = logger;
_unityObjectTransformer = unityObjectTransformer;
}

public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (_unityContextScope?.Value != null)
{
var unityContext = _unityContextScope.Value.UnityContext;
if (ReferenceEquals(unityContext, null) == false)
{
LogWithUnityContext(logLevel, eventId, state, exception, formatter, unityContext);
return;
}
}

_logger.Log(logLevel, eventId, state, exception, formatter);
}

private void LogWithUnityContext<TState>(
Microsoft.Extensions.Logging.LogLevel logLevel,
Microsoft.Extensions.Logging.EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter,
UnityEngine.Object unityContext)
{
LogContext.PushProperty(UnityObjectEnricher.UnityContextKey, unityContext, destructureObjects: true);
_logger.Log(logLevel, eventId, state, exception, formatter);
}

public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
if (state is UnityEngine.Object unityContext)
{
IDisposable? scopeDisposable;

if (_unityObjectTransformer == null)
{
if (SynchronizationContext.Current != _synchronizationContext)
{
throw new NotSupportedException("BeginScope(UnityEngine.Object) can only be used from the main thread.");
}

scopeDisposable = _logger.BeginScope(unityContext);
}
else
{
scopeDisposable = _logger.BeginScope(_unityObjectTransformer(unityContext));
}

return CurrentScope = new UnityContextScope(this, scopeDisposable, unityContext);
}

return _logger.BeginScope(state);
}

private class UnityContextScope : IDisposable
{
private readonly SerilogUnityContextScopeLogger _logger;
private readonly IDisposable? _chainedDisposable;

private readonly WeakReference<UnityEngine.Object> _unityContextReference;
private bool _disposed;

public UnityContextScope? Parent { get; }

public UnityEngine.Object? UnityContext
{
get
{
if (_unityContextReference.TryGetTarget(out var result) == false)
return null;

return result;
}
}

public UnityContextScope(SerilogUnityContextScopeLogger logger, IDisposable? chainedDisposable, UnityEngine.Object unityContext)
{
_logger = logger;
_chainedDisposable = chainedDisposable;
_unityContextReference = new WeakReference<UnityEngine.Object>(unityContext);

Parent = logger.CurrentScope;
}

public void Dispose()
{
if (_disposed)
return;

_disposed = true;

// Just like in Serilog.Extensions.Logging.SerilogLoggerScope.Dispose():
// In case one of the parent scopes has been disposed out-of-order, don't
// just blindly reinstate our own parent.
for (var scan = _logger.CurrentScope; scan != null; scan = scan.Parent)
{
if (ReferenceEquals(scan, this))
_logger.CurrentScope = Parent;
}

_chainedDisposable?.Dispose();
}
}

public class UnityObjectToStringWrapper
{
private UnityEngine.Object _unityContext;
private string? _toString = null;

public UnityObjectToStringWrapper(UnityEngine.Object unityContext)
{
_unityContext = unityContext;
}

public override string ToString()
{
if (_toString != null)
return _toString;

_synchronizationContext?.Send(GetObjectName, null);
return _toString!;
}

private void GetObjectName(object state)
{
_toString = _unityContext.ToString();
}
}

}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#nullable enable
using Serilog.Extensions.Logging;

namespace Serilog.Sinks.Unity3D
{
public delegate object UnityObjectTransformerDelegate(UnityEngine.Object obj);

/// <summary>
/// A thin wrapper around <see cref="SerilogLoggerProvider"/>.
/// </summary>
public class SerilogUnityContextScopeLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider
{
private readonly SerilogLoggerProvider _innerProvider;
private readonly UnityObjectTransformerDelegate? _unityObjectTransformer;

public SerilogUnityContextScopeLoggerProvider(ILogger? logger, bool dispose, UnityObjectTransformerDelegate? unityObjectTransformer = null)
{
_innerProvider = new SerilogLoggerProvider(logger, dispose);
_unityObjectTransformer = unityObjectTransformer;
}

public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
{
return new SerilogUnityContextScopeLogger(_innerProvider.CreateLogger(categoryName), _unityObjectTransformer);
}

public void Dispose()
{
_innerProvider.Dispose();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#nullable enable
using Serilog.Core;
using Serilog.Events;
using System.Diagnostics.CodeAnalysis;

namespace Serilog.Sinks.Unity3D
{
/// <summary>
/// Makes sure we destructure <see cref="UnityEngine.Object"/>s as scalars, i.e. keeping their reference.
/// </summary>
internal class UnityObjectDestructuringPolicy : IDestructuringPolicy
{
public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result)
{
result = null;

if (value is UnityEngine.Object unityObject)
{
result = new ScalarValue(unityObject);
return true;
}

return false;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.