diff --git a/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj index 2c4ae7a..04c1fe7 100644 --- a/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj +++ b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj @@ -22,6 +22,7 @@ + diff --git a/src/NetEvolve.Logging.XUnit/XUnitLogger.cs b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs index 809cf08..c2791e6 100644 --- a/src/NetEvolve.Logging.XUnit/XUnitLogger.cs +++ b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs @@ -8,6 +8,7 @@ using NetEvolve.Arguments; using NetEvolve.Logging.Abstractions; using Xunit.Abstractions; +using Xunit.Sdk; /// /// Represents a logger that writes messages to xunit output. @@ -15,11 +16,12 @@ public class XUnitLogger : ILogger, ISupportExternalScope { private readonly IXUnitLoggerOptions _options; - private readonly ITestOutputHelper _testOutputHelper; private readonly TimeProvider _timeProvider; private readonly List _loggedMessages; + private readonly Action _writeToLog; + private const int DefaultCapacity = 1024; [ThreadStatic] @@ -33,6 +35,43 @@ public class XUnitLogger : ILogger, ISupportExternalScope /// public IReadOnlyList LoggedMessages => _loggedMessages.AsReadOnly(); + /// + /// Creates a new instance of . + /// + /// The to write the log messages to. + /// The to use to get the current time. + /// The to use to get the current scope. + /// The options to control the behavior of the logger. + /// A cached or new instance of . + public static XUnitLogger CreateLogger( + IMessageSink messageSink, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider = null, + IXUnitLoggerOptions? options = null + ) + { + Argument.ThrowIfNull(messageSink); + + return new XUnitLogger(messageSink, timeProvider, scopeProvider, options); + } + + /// + /// Creates a new instance of . + /// + /// The type who's fullname is used as the category name for messages produced by the logger. + /// The to write the log messages to. + /// The to use to get the current time. + /// The to use to get the current scope. + /// The options to control the behavior of the logger. + /// A cached or new instance of . + public static XUnitLogger CreateLogger( + IMessageSink messageSink, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider = null, + IXUnitLoggerOptions? options = null + ) + where T : notnull => new XUnitLogger(messageSink, timeProvider, scopeProvider, options); + /// /// Creates a new instance of . /// @@ -82,11 +121,31 @@ private protected XUnitLogger( Argument.ThrowIfNull(timeProvider); ScopeProvider = scopeProvider ?? NullExternalScopeProvider.Instance; - _testOutputHelper = testOutputHelper; _timeProvider = timeProvider; _options = options ?? XUnitLoggerOptions.Default; _loggedMessages = []; + + _writeToLog = testOutputHelper.WriteLine; + } + + private protected XUnitLogger( + IMessageSink messageSink, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider, + IXUnitLoggerOptions? options + ) + { + Argument.ThrowIfNull(messageSink); + Argument.ThrowIfNull(timeProvider); + + ScopeProvider = scopeProvider ?? NullExternalScopeProvider.Instance; + _timeProvider = timeProvider; + _options = options ?? XUnitLoggerOptions.Default; + + _loggedMessages = []; + + _writeToLog = message => _ = messageSink.OnMessage(new DiagnosticMessage(message)); } /// @@ -112,88 +171,84 @@ public void Log( return; } - var builder = _builder; - _builder = null; - builder ??= new StringBuilder(DefaultCapacity); - try { var message = formatter(state, exception); var now = _timeProvider.GetLocalNow(); - (builder, var scopes) = CreateMessage( - logLevel, - state, - exception, - builder, - message, - now - ); + var (fullMessage, scopes) = CreateMessage(logLevel, state, exception, message, now); _loggedMessages.Add( new LoggedMessage(now, logLevel, eventId, message, exception, scopes) ); - _testOutputHelper.WriteLine(builder.ToString()); + + _writeToLog.Invoke(fullMessage); } catch { // Ignore exception. // Unfortunately, this can happen if the process is terminated before the end of the test. } - finally - { - _ = builder.Clear(); - if (builder.Capacity > DefaultCapacity) - { - builder.Capacity = DefaultCapacity; - } - _builder = builder; - } } - private (StringBuilder, List) CreateMessage( + private (string, List) CreateMessage( LogLevel logLevel, TState state, Exception? exception, - StringBuilder builder, string message, DateTimeOffset now ) { var scopes = new List(); - if (!_options.DisableTimestamp) - { - _ = builder - .Append(now.ToString(_options.TimestampFormat, CultureInfo.InvariantCulture)) - .Append(' '); - } + var builder = _builder; + _builder = null; + builder ??= new StringBuilder(DefaultCapacity); - if (!_options.DisableLogLevel) + try { - _ = builder.Append('[').Append(LogLevelToString(logLevel)).Append("] "); - } + if (!_options.DisableTimestamp) + { + _ = builder + .Append(now.ToString(_options.TimestampFormat, CultureInfo.InvariantCulture)) + .Append(' '); + } - _ = builder.Append(message); + if (!_options.DisableLogLevel) + { + _ = builder.Append('[').Append(LogLevelToString(logLevel)).Append("] "); + } - if (exception is not null) - { - _ = builder.Append('\n').Append(exception); - } + _ = builder.Append(message); - if ( - !_options.DisableAdditionalInformation - && state is IReadOnlyList> additionalInformation - ) - { - _ = builder.Append('\n').Append('\t').Append("Additional Information"); - foreach (var info in additionalInformation) + if (exception is not null) { - AddAdditionalInformation(builder, info); + _ = builder.Append('\n').Append(exception); } - } - ScopeProvider.ForEachScope(IterateScopes, builder); + if ( + !_options.DisableAdditionalInformation + && state is IReadOnlyList> additionalInformation + ) + { + _ = builder.Append('\n').Append('\t').Append("Additional Information"); + foreach (var info in additionalInformation) + { + AddAdditionalInformation(builder, info); + } + } - return (builder, scopes); + ScopeProvider.ForEachScope(IterateScopes, builder); + + return (builder.ToString(), scopes); + } + finally + { + _ = builder.Clear(); + if (builder.Capacity > DefaultCapacity) + { + builder.Capacity = DefaultCapacity; + } + _builder = builder; + } void IterateScopes(object? scope, StringBuilder state) { diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs index b920693..132f215 100644 --- a/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs @@ -62,7 +62,7 @@ public ILogger CreateLogger(string categoryName) Argument.ThrowIfNullOrWhiteSpace(categoryName); return _loggers.GetOrAdd( - categoryName, + $"{categoryName}_Default", name => XUnitLogger.CreateLogger(_testOutputHelper, _timeProvider, _scopeProvider, this) ); } @@ -71,10 +71,58 @@ public ILogger CreateLogger(string categoryName) public ILogger CreateLogger() where T : notnull => _loggers.GetOrAdd( - typeof(T).FullName!, + $"{typeof(T).FullName}_Default", _ => XUnitLogger.CreateLogger(_testOutputHelper, _timeProvider, _scopeProvider, this) ); + /// + public ILogger CreateLogger(string categoryName, IMessageSink messageSink) + { + Argument.ThrowIfNullOrWhiteSpace(categoryName); + Argument.ThrowIfNull(messageSink); + + return _loggers.GetOrAdd( + $"{categoryName}_IMessageSink", + name => XUnitLogger.CreateLogger(messageSink, _timeProvider, _scopeProvider, this) + ); + } + + /// + public ILogger CreateLogger(IMessageSink messageSink) + where T : notnull + { + Argument.ThrowIfNull(messageSink); + + return _loggers.GetOrAdd( + $"{typeof(T).FullName}_IMessageSink", + _ => XUnitLogger.CreateLogger(messageSink, _timeProvider, _scopeProvider, this) + ); + } + + /// + public ILogger CreateLogger(string categoryName, ITestOutputHelper testOutputHelper) + { + Argument.ThrowIfNullOrWhiteSpace(categoryName); + Argument.ThrowIfNull(testOutputHelper); + + return _loggers.GetOrAdd( + $"{categoryName}_ITestOutputHelper", + name => XUnitLogger.CreateLogger(testOutputHelper, _timeProvider, _scopeProvider, this) + ); + } + + /// + public ILogger CreateLogger(ITestOutputHelper testOutputHelper) + where T : notnull + { + Argument.ThrowIfNull(testOutputHelper); + + return _loggers.GetOrAdd( + $"{typeof(T).FullName}_ITestOutputHelper", + _ => XUnitLogger.CreateLogger(testOutputHelper, _timeProvider, _scopeProvider, this) + ); + } + /// public void SetScopeProvider(IExternalScopeProvider scopeProvider) { diff --git a/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs b/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs index 912608f..f129233 100644 --- a/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs +++ b/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs @@ -8,6 +8,14 @@ public sealed class XUnitLogger : XUnitLogger, ILogger where T : notnull { + internal XUnitLogger( + IMessageSink messageSink, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider, + IXUnitLoggerOptions? options + ) + : base(messageSink, timeProvider, scopeProvider, options) { } + internal XUnitLogger( ITestOutputHelper testOutputHelper, TimeProvider timeProvider,