Skip to content

Commit

Permalink
feat: Support of IMessageSink (#17)
Browse files Browse the repository at this point in the history
* feat: Added functionallity for `IMessageSink`

* fix: Added missing using
  • Loading branch information
samtrion authored May 23, 2024
1 parent a8f33a1 commit 7650cc1
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 53 deletions.
1 change: 1 addition & 0 deletions src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj
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

0 comments on commit 7650cc1

Please sign in to comment.