diff --git a/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj b/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj
index 45456a13..8272d996 100644
--- a/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj
+++ b/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj
@@ -1,6 +1,7 @@
+
diff --git a/src/Foundatio.TestHarness/Locks/LockTestBase.cs b/src/Foundatio.TestHarness/Locks/LockTestBase.cs
index 01d7513a..1b987aab 100644
--- a/src/Foundatio.TestHarness/Locks/LockTestBase.cs
+++ b/src/Foundatio.TestHarness/Locks/LockTestBase.cs
@@ -271,7 +271,7 @@ private Task DoLockedWorkAsync(ILockProvider locker)
public virtual async Task WillThrottleCallsAsync()
{
- Log.MinimumLevel = LogLevel.Trace;
+ Log.DefaultMinimumLevel = LogLevel.Trace;
Log.SetLogLevel(LogLevel.Information);
Log.SetLogLevel(LogLevel.Trace);
diff --git a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs
index 1bf7a243..be043688 100644
--- a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs
+++ b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs
@@ -631,7 +631,7 @@ public virtual async Task WillNotWaitForItemAsync()
public virtual async Task WillWaitForItemAsync()
{
- Log.MinimumLevel = LogLevel.Trace;
+ Log.DefaultMinimumLevel = LogLevel.Trace;
var queue = GetQueue();
if (queue == null)
return;
@@ -792,7 +792,7 @@ await queue.StartWorkingAsync(w =>
public virtual async Task WorkItemsWillTimeoutAsync()
{
- Log.MinimumLevel = LogLevel.Trace;
+ Log.DefaultMinimumLevel = LogLevel.Trace;
Log.SetLogLevel("Foundatio.Queues.RedisQueue", LogLevel.Trace);
var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: TimeSpan.FromMilliseconds(50));
if (queue == null)
@@ -1313,7 +1313,7 @@ protected async Task CanDequeueWithLockingImpAsync(CacheLockProvider distributed
await queue.DeleteQueueAsync();
await AssertEmptyQueueAsync(queue);
- Log.MinimumLevel = LogLevel.Trace;
+ Log.DefaultMinimumLevel = LogLevel.Trace;
using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log });
queue.AttachBehavior(new MetricsQueueBehavior(metrics, loggerFactory: Log));
diff --git a/src/Foundatio.Xunit/Foundatio.Xunit.csproj b/src/Foundatio.Xunit/Foundatio.Xunit.csproj
index f881eaee..5b974843 100644
--- a/src/Foundatio.Xunit/Foundatio.Xunit.csproj
+++ b/src/Foundatio.Xunit/Foundatio.Xunit.csproj
@@ -8,6 +8,7 @@
-
+
+
diff --git a/src/Foundatio.Xunit/Logging/LogEntry.cs b/src/Foundatio.Xunit/Logging/LogEntry.cs
index 6f68b732..85f51762 100644
--- a/src/Foundatio.Xunit/Logging/LogEntry.cs
+++ b/src/Foundatio.Xunit/Logging/LogEntry.cs
@@ -6,7 +6,7 @@ namespace Foundatio.Xunit;
public class LogEntry
{
- public DateTime Date { get; set; }
+ public DateTimeOffset Date { get; set; }
public string CategoryName { get; set; }
public LogLevel LogLevel { get; set; }
public object[] Scopes { get; set; }
diff --git a/src/Foundatio.Xunit/Logging/LoggingExtensions.cs b/src/Foundatio.Xunit/Logging/LoggingExtensions.cs
new file mode 100644
index 00000000..526434a8
--- /dev/null
+++ b/src/Foundatio.Xunit/Logging/LoggingExtensions.cs
@@ -0,0 +1,84 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace Foundatio.Xunit;
+
+public static class LoggingExtensions
+{
+ public static TestLogger GetTestLogger(this IServiceProvider serviceProvider)
+ {
+ return serviceProvider.GetRequiredService();
+ }
+
+ public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, ITestOutputHelper outputHelper,
+ Action configure = null)
+ {
+
+ var options = new TestLoggerOptions {
+ WriteLogEntryFunc = logEntry =>
+ {
+ outputHelper.WriteLine(logEntry.ToString(false));
+ }
+ };
+
+ configure?.Invoke(options);
+
+ return builder.AddTestLogger(options);
+ }
+
+ public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, Action configure)
+ {
+ var options = new TestLoggerOptions();
+ configure?.Invoke(options);
+ return builder.AddTestLogger(options);
+ }
+
+ public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, TestLoggerOptions options = null)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ var loggerProvider = new TestLoggerProvider(options);
+ builder.AddProvider(loggerProvider);
+ builder.Services.TryAddSingleton(loggerProvider.Log);
+
+ return builder;
+ }
+
+ public static ILoggerFactory AddTestLogger(this ILoggerFactory factory, Action configure = null)
+ {
+ if (factory == null)
+ throw new ArgumentNullException(nameof(factory));
+
+ var options = new TestLoggerOptions();
+ configure?.Invoke(options);
+
+ factory.AddProvider(new TestLoggerProvider(options));
+
+ return factory;
+ }
+
+ public static TestLogger ToTestLogger(this ITestOutputHelper outputHelper, Action configure = null)
+ {
+ if (outputHelper == null)
+ throw new ArgumentNullException(nameof(outputHelper));
+
+ var options = new TestLoggerOptions();
+ options.WriteLogEntryFunc = logEntry =>
+ {
+ outputHelper.WriteLine(logEntry.ToString());
+ };
+
+ configure?.Invoke(options);
+
+ var testLogger = new TestLogger(options);
+
+ return testLogger;
+ }
+
+ public static ILogger ToTestLogger(this ITestOutputHelper outputHelper, Action configure = null)
+ => outputHelper.ToTestLogger(configure).CreateLogger();
+}
diff --git a/src/Foundatio.Xunit/Logging/TestLogger.cs b/src/Foundatio.Xunit/Logging/TestLogger.cs
index 2d525cad..11554a43 100644
--- a/src/Foundatio.Xunit/Logging/TestLogger.cs
+++ b/src/Foundatio.Xunit/Logging/TestLogger.cs
@@ -1,112 +1,128 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
using System.Threading;
using Foundatio.Utility;
using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
namespace Foundatio.Xunit;
-internal class TestLogger : ILogger
+public class TestLogger : ILoggerFactory
{
- private readonly TestLoggerFactory _loggerFactory;
- private readonly string _categoryName;
+ private readonly Dictionary _logLevels = new();
+ private readonly Queue _logEntries = new();
+ private int _logEntriesWritten;
- public TestLogger(string categoryName, TestLoggerFactory loggerFactory)
+ public TestLogger(Action configure = null)
{
- _loggerFactory = loggerFactory;
- _categoryName = categoryName;
+ Options = new TestLoggerOptions();
+ configure?.Invoke(Options);
}
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ public TestLogger(ITestOutputHelper output, Action configure = null)
{
- if (!_loggerFactory.IsEnabled(_categoryName, logLevel))
- return;
-
- object[] scopes = CurrentScopeStack.Reverse().ToArray();
- var logEntry = new LogEntry
- {
- Date = SystemClock.UtcNow,
- LogLevel = logLevel,
- EventId = eventId,
- State = state,
- Exception = exception,
- Formatter = (s, e) => formatter((TState)s, e),
- CategoryName = _categoryName,
- Scopes = scopes
+ Options = new TestLoggerOptions {
+ WriteLogEntryFunc = logEntry =>
+ {
+ output.WriteLine(logEntry.ToString(false));
+ }
};
- switch (state)
- {
- //case LogData logData:
- // logEntry.Properties["CallerMemberName"] = logData.MemberName;
- // logEntry.Properties["CallerFilePath"] = logData.FilePath;
- // logEntry.Properties["CallerLineNumber"] = logData.LineNumber;
-
- // foreach (var property in logData.Properties)
- // logEntry.Properties[property.Key] = property.Value;
- // break;
- case IDictionary logDictionary:
- foreach (var property in logDictionary)
- logEntry.Properties[property.Key] = property.Value;
- break;
- }
+ configure?.Invoke(Options);
+ }
- foreach (object scope in scopes)
- {
- if (!(scope is IDictionary scopeData))
- continue;
+ public TestLogger(TestLoggerOptions options)
+ {
+ Options = options ?? new TestLoggerOptions();
+ }
- foreach (var property in scopeData)
- logEntry.Properties[property.Key] = property.Value;
- }
+ public TestLoggerOptions Options { get; }
- _loggerFactory.AddLogEntry(logEntry);
+ [Obsolete("Use DefaultMinimumLevel instead.")]
+ public LogLevel MinimumLevel
+ {
+ get => Options.DefaultMinimumLevel;
+ set => Options.DefaultMinimumLevel = value;
}
- public bool IsEnabled(LogLevel logLevel)
+ public LogLevel DefaultMinimumLevel
{
- return _loggerFactory.IsEnabled(_categoryName, logLevel);
+ get => Options.DefaultMinimumLevel;
+ set => Options.DefaultMinimumLevel = value;
}
- public IDisposable BeginScope(TState state)
+ public int MaxLogEntriesToStore
{
- if (state == null)
- throw new ArgumentNullException(nameof(state));
-
- return Push(state);
+ get => Options.MaxLogEntriesToStore;
+ set => Options.MaxLogEntriesToStore = value;
}
- public IDisposable BeginScope(Func scopeFactory, TState state)
+ public int MaxLogEntriesToWrite
{
- if (state == null)
- throw new ArgumentNullException(nameof(state));
+ get => Options.MaxLogEntriesToWrite;
+ set => Options.MaxLogEntriesToWrite = value;
+ }
- return Push(scopeFactory(state));
+ public IReadOnlyList LogEntries => _logEntries.ToArray();
+
+
+ public void Clear()
+ {
+ lock (_logEntries)
+ {
+ _logEntries.Clear();
+ Interlocked.Exchange(ref _logEntriesWritten, 0);
+ }
}
- private static readonly AsyncLocal _currentScopeStack = new();
+ internal void AddLogEntry(LogEntry logEntry)
+ {
+ lock (_logEntries)
+ {
+ _logEntries.Enqueue(logEntry);
+
+ if (_logEntries.Count > Options.MaxLogEntriesToStore)
+ _logEntries.Dequeue();
+ }
+
+ if (Options.WriteLogEntryFunc == null || _logEntriesWritten >= Options.MaxLogEntriesToWrite)
+ return;
+
+ try
+ {
+ Options.WriteLogEntry(logEntry);
+ Interlocked.Increment(ref _logEntriesWritten);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
- private sealed class Wrapper
+ public ILogger CreateLogger(string categoryName)
{
- public ImmutableStack