Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageReference Include="NetEvolve.Arguments" />
<PackageReference Include="NetEvolve.Logging.Abstractions" />
<PackageReference Include="xunit.extensibility.core" />
<PackageReference Include="xunit.extensibility.execution" />
</ItemGroup>

</Project>
157 changes: 106 additions & 51 deletions src/NetEvolve.Logging.XUnit/XUnitLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@
using NetEvolve.Arguments;
using NetEvolve.Logging.Abstractions;
using Xunit.Abstractions;
using Xunit.Sdk;

/// <summary>
/// Represents a logger that writes messages to xunit output.
/// </summary>
public class XUnitLogger : ILogger, ISupportExternalScope
{
private readonly IXUnitLoggerOptions _options;
private readonly ITestOutputHelper _testOutputHelper;
private readonly TimeProvider _timeProvider;

private readonly List<LoggedMessage> _loggedMessages;

private readonly Action<string> _writeToLog;

private const int DefaultCapacity = 1024;

[ThreadStatic]
Expand All @@ -33,6 +35,43 @@ public class XUnitLogger : ILogger, ISupportExternalScope
/// <inheritdoc cref="IHasLoggedMessages.LoggedMessages"/>
public IReadOnlyList<LoggedMessage> LoggedMessages => _loggedMessages.AsReadOnly();

/// <summary>
/// Creates a new instance of <see cref="XUnitLogger"/>.
/// </summary>
/// <param name="messageSink">The <see cref="IMessageSink" /> to write the log messages to.</param>
/// <param name="timeProvider">The <see cref="TimeProvider" /> to use to get the current time.</param>
/// <param name="scopeProvider">The <see cref="IExternalScopeProvider" /> to use to get the current scope.</param>
/// <param name="options">The options to control the behavior of the logger.</param>
/// <returns>A cached or new instance of <see cref="XUnitLogger"/>.</returns>
public static XUnitLogger CreateLogger(
IMessageSink messageSink,
TimeProvider timeProvider,
IExternalScopeProvider? scopeProvider = null,
IXUnitLoggerOptions? options = null
)
{
Argument.ThrowIfNull(messageSink);

return new XUnitLogger(messageSink, timeProvider, scopeProvider, options);
}

/// <summary>
/// Creates a new instance of <see cref="XUnitLogger{T}"/>.
/// </summary>
/// <typeparam name="T">The type who's fullname is used as the category name for messages produced by the logger.</typeparam>
/// <param name="messageSink">The <see cref="IMessageSink" /> to write the log messages to.</param>
/// <param name="timeProvider">The <see cref="TimeProvider" /> to use to get the current time.</param>
/// <param name="scopeProvider">The <see cref="IExternalScopeProvider" /> to use to get the current scope.</param>
/// <param name="options">The options to control the behavior of the logger.</param>
/// <returns>A cached or new instance of <see cref="XUnitLogger"/>.</returns>
public static XUnitLogger<T> CreateLogger<T>(
IMessageSink messageSink,
TimeProvider timeProvider,
IExternalScopeProvider? scopeProvider = null,
IXUnitLoggerOptions? options = null
)
where T : notnull => new XUnitLogger<T>(messageSink, timeProvider, scopeProvider, options);

/// <summary>
/// Creates a new instance of <see cref="XUnitLogger"/>.
/// </summary>
Expand Down Expand Up @@ -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));
}

/// <inheritdoc cref="ILogger.BeginScope{TState}(TState)"/>
Expand All @@ -112,88 +171,84 @@ public void Log<TState>(
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<object?>) CreateMessage<TState>(
private (string, List<object?>) CreateMessage<TState>(
LogLevel logLevel,
TState state,
Exception? exception,
StringBuilder builder,
string message,
DateTimeOffset now
)
{
var scopes = new List<object?>();
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<KeyValuePair<string, object?>> 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<KeyValuePair<string, object?>> 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)
{
Expand Down
52 changes: 50 additions & 2 deletions src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
Expand All @@ -71,10 +71,58 @@ public ILogger CreateLogger(string categoryName)
public ILogger CreateLogger<T>()
where T : notnull =>
_loggers.GetOrAdd(
typeof(T).FullName!,
$"{typeof(T).FullName}_Default",
_ => XUnitLogger.CreateLogger<T>(_testOutputHelper, _timeProvider, _scopeProvider, this)
);

/// <inheritdoc cref="ILoggerProvider.CreateLogger(string)"/>
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)
);
}

/// <inheritdoc cref="ILoggerProvider.CreateLogger(string)"/>
public ILogger CreateLogger<T>(IMessageSink messageSink)
where T : notnull
{
Argument.ThrowIfNull(messageSink);

return _loggers.GetOrAdd(
$"{typeof(T).FullName}_IMessageSink",
_ => XUnitLogger.CreateLogger<T>(messageSink, _timeProvider, _scopeProvider, this)
);
}

/// <inheritdoc cref="ILoggerProvider.CreateLogger(string)"/>
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)
);
}

/// <inheritdoc cref="ILoggerProvider.CreateLogger(string)"/>
public ILogger CreateLogger<T>(ITestOutputHelper testOutputHelper)
where T : notnull
{
Argument.ThrowIfNull(testOutputHelper);

return _loggers.GetOrAdd(
$"{typeof(T).FullName}_ITestOutputHelper",
_ => XUnitLogger.CreateLogger<T>(testOutputHelper, _timeProvider, _scopeProvider, this)
);
}

/// <inheritdoc cref="ISupportExternalScope.SetScopeProvider(IExternalScopeProvider)"/>
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
{
Expand Down
8 changes: 8 additions & 0 deletions src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
public sealed class XUnitLogger<T> : XUnitLogger, ILogger<T>
where T : notnull
{
internal XUnitLogger(
IMessageSink messageSink,
TimeProvider timeProvider,
IExternalScopeProvider? scopeProvider,
IXUnitLoggerOptions? options
)
: base(messageSink, timeProvider, scopeProvider, options) { }

internal XUnitLogger(
ITestOutputHelper testOutputHelper,
TimeProvider timeProvider,
Expand Down