From cc4d26c47d5db8cb20e9eb51c4849f96de66c26e Mon Sep 17 00:00:00 2001 From: Cort Schaefer Date: Tue, 4 Feb 2025 09:54:18 -0700 Subject: [PATCH] add extension methods for ILogger to be able to push properties like serilog does without serilog; add more unit tests --- .../Cortside.Common.Logging.csproj | 10 +++ .../LoggingExtensions.cs | 15 +++++ .../Cortside.Common.Testing.Tests.csproj | 1 + .../Logging/LogEvent/LogEventLoggerTest.cs | 67 +++++++++++++++++++ .../Extensions/RandomExtensions.cs | 2 +- .../Logging/LogEvent/LogEventLogger.cs | 19 ++++-- .../LogEventLoggerFactoryExtensions.cs | 20 ++++++ .../LogEvent/LogEventLoggerProvider.cs | 22 ++++++ .../Xunit/XunitLoggerFactoryExtensions.cs | 7 +- src/Cortside.Common.sln | 6 ++ 10 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 src/Cortside.Common.Logging/Cortside.Common.Logging.csproj create mode 100644 src/Cortside.Common.Logging/LoggingExtensions.cs create mode 100644 src/Cortside.Common.Testing.Tests/Logging/LogEvent/LogEventLoggerTest.cs create mode 100644 src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerFactoryExtensions.cs create mode 100644 src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerProvider.cs diff --git a/src/Cortside.Common.Logging/Cortside.Common.Logging.csproj b/src/Cortside.Common.Logging/Cortside.Common.Logging.csproj new file mode 100644 index 0000000..542d3a7 --- /dev/null +++ b/src/Cortside.Common.Logging/Cortside.Common.Logging.csproj @@ -0,0 +1,10 @@ + + + + net6.0 + + + + + + diff --git a/src/Cortside.Common.Logging/LoggingExtensions.cs b/src/Cortside.Common.Logging/LoggingExtensions.cs new file mode 100644 index 0000000..75e2c23 --- /dev/null +++ b/src/Cortside.Common.Logging/LoggingExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Cortside.Common.Logging { + public static class LoggerExtensions { + public static IDisposable PushProperty(this ILogger logger, string name, object value) { + return logger.BeginScope(new Dictionary { { name, value } }); + } + + public static IDisposable PushProperties(this ILogger logger, Dictionary properties) { + return logger.BeginScope(properties); + } + } +} diff --git a/src/Cortside.Common.Testing.Tests/Cortside.Common.Testing.Tests.csproj b/src/Cortside.Common.Testing.Tests/Cortside.Common.Testing.Tests.csproj index cc6c09f..8d2541f 100644 --- a/src/Cortside.Common.Testing.Tests/Cortside.Common.Testing.Tests.csproj +++ b/src/Cortside.Common.Testing.Tests/Cortside.Common.Testing.Tests.csproj @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/src/Cortside.Common.Testing.Tests/Logging/LogEvent/LogEventLoggerTest.cs b/src/Cortside.Common.Testing.Tests/Logging/LogEvent/LogEventLoggerTest.cs new file mode 100644 index 0000000..33ae116 --- /dev/null +++ b/src/Cortside.Common.Testing.Tests/Logging/LogEvent/LogEventLoggerTest.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cortside.Common.Logging; +using Cortside.Common.Testing.Logging.LogEvent; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Cortside.Common.Testing.Tests.Logging.Xunit { + public class LogEventLoggerTest { + private readonly ILoggerFactory loggerFactory; + + public LogEventLoggerTest(ITestOutputHelper output) { + // Create a logger factory with a debug provider + loggerFactory = LoggerFactory.Create(builder => { + builder + .SetMinimumLevel(LogLevel.Trace) + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("Cortside.Common", LogLevel.Trace) + .AddLogEvent(); + }); + } + + [Fact] + public void TestLogger() { + // Create a logger with the category name of the current class + var logger = loggerFactory.CreateLogger(); + + Assert.NotNull(logger); + + // Log some messages with different log levels and message templates + logger.LogTrace("This is a trace message."); + logger.LogDebug("This is a debug message."); + logger.LogInformation("Hello {Name}!", "World"); + logger.LogWarning("This is a warning message."); + logger.LogError("This is an error message."); + logger.LogCritical("This is a critical message."); + + // Use structured logging to capture complex data + var person = new Person { Name = "Alice", Age = 25 }; + logger.LogInformation("Created a new person: {@Person}", person); + + // Use exception logging to capture the details of an exception + try { + throw new Exception("Something went wrong."); + } catch (Exception ex) { + logger.LogError(ex, "An exception occurred."); + } + + // Use the logger to capture a log event + Assert.Equal(8, LogEventLogger.LogEvents.Count); + + using (logger.PushProperties(new Dictionary() { + ["UserId"] = "xxx", + ["ExtraProperty"] = "yyy", + })) { + logger.LogDebug("logged messaged that should have 2 properties with it"); + } + + // 10, adding 1 for the actual log and 1 for the being scope + Assert.Equal(10, LogEventLogger.LogEvents.Count); + Assert.Equal("UserId=xxxExtraProperty=yyy", LogEventLogger.LogEvents.First(x => x.LogLevel == LogLevel.None).Message); + } + } +} diff --git a/src/Cortside.Common.Testing/Extensions/RandomExtensions.cs b/src/Cortside.Common.Testing/Extensions/RandomExtensions.cs index 84e50e9..60aacde 100644 --- a/src/Cortside.Common.Testing/Extensions/RandomExtensions.cs +++ b/src/Cortside.Common.Testing/Extensions/RandomExtensions.cs @@ -1,7 +1,7 @@ using System; namespace Cortside.Common.Testing.Extensions { - static class RandomExtensions { + public static class RandomExtensions { /// /// Returns a random long from min (inclusive) to max (exclusive) /// diff --git a/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLogger.cs b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLogger.cs index 5cbc906..1d968fa 100644 --- a/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLogger.cs +++ b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLogger.cs @@ -4,11 +4,12 @@ using Microsoft.Extensions.Logging; namespace Cortside.Common.Testing.Logging.LogEvent { - public class LogEventLogger : ILogger { - public List LogEvents { get; } + public class LogEventLogger : ILogger { + private readonly string name; + public static List LogEvents { get; } = new List(); - public LogEventLogger() { - LogEvents = new List(); + public LogEventLogger(string name) { + this.name = name; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -24,6 +25,16 @@ public bool IsEnabled(LogLevel logLevel) { } public IDisposable BeginScope(TState state) { + var s = string.Empty; + if (state is IEnumerable>) { + var context = state as IEnumerable>; + foreach (var kp in context) { + s += kp.Key + "=" + kp.Value.ToString(); + } + + LogEvents.Add(new LogEvent() { LogLevel = LogLevel.None, Message = s }); + } + return NullScope.Instance; } } diff --git a/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerFactoryExtensions.cs b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerFactoryExtensions.cs new file mode 100644 index 0000000..ba5860e --- /dev/null +++ b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerFactoryExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Cortside.Common.Testing.Logging.LogEvent { + /// + /// Extension methods for the class. + /// + public static class LogEventLoggerFactoryExtensions { + /// + /// Adds a debug logger named 'Debug' to the factory. + /// + /// The extension method argument. + public static ILoggingBuilder AddLogEvent(this ILoggingBuilder builder) { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(new LogEventLoggerProvider())); + + return builder; + } + } +} diff --git a/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerProvider.cs b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerProvider.cs new file mode 100644 index 0000000..0a1c4c2 --- /dev/null +++ b/src/Cortside.Common.Testing/Logging/LogEvent/LogEventLoggerProvider.cs @@ -0,0 +1,22 @@ +using Cortside.Common.Testing.Logging.Xunit; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Cortside.Common.Testing.Logging.LogEvent { + /// + /// The provider for the . + /// + [ProviderAlias("LogEvent")] + public class LogEventLoggerProvider : ILoggerProvider { + private readonly ITestOutputHelper output; + + /// + public ILogger CreateLogger(string name) { + return new LogEventLogger(name); + } + + /// + public void Dispose() { + } + } +} diff --git a/src/Cortside.Common.Testing/Logging/Xunit/XunitLoggerFactoryExtensions.cs b/src/Cortside.Common.Testing/Logging/Xunit/XunitLoggerFactoryExtensions.cs index e6482d7..87b0c1a 100644 --- a/src/Cortside.Common.Testing/Logging/Xunit/XunitLoggerFactoryExtensions.cs +++ b/src/Cortside.Common.Testing/Logging/Xunit/XunitLoggerFactoryExtensions.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -namespace Cortside.Common.Testing.Logging.Xunit { +namespace Cortside.Common.Testing.Logging.Xunit { /// /// Extension methods for the class. /// diff --git a/src/Cortside.Common.sln b/src/Cortside.Common.sln index cdab138..6f3705d 100644 --- a/src/Cortside.Common.sln +++ b/src/Cortside.Common.sln @@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cortside.Common.Correlation EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cortside.Common.Correlation.Tests", "Cortside.Common.Correlation.Tests\Cortside.Common.Correlation.Tests.csproj", "{58003B10-A4C3-4EF7-A5B8-5A7C927406FB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortside.Common.Logging", "Cortside.Common.Logging\Cortside.Common.Logging.csproj", "{ECE6C743-FCA4-47CA-97A2-645BAB61E1BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -167,6 +169,10 @@ Global {58003B10-A4C3-4EF7-A5B8-5A7C927406FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {58003B10-A4C3-4EF7-A5B8-5A7C927406FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {58003B10-A4C3-4EF7-A5B8-5A7C927406FB}.Release|Any CPU.Build.0 = Release|Any CPU + {ECE6C743-FCA4-47CA-97A2-645BAB61E1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECE6C743-FCA4-47CA-97A2-645BAB61E1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECE6C743-FCA4-47CA-97A2-645BAB61E1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECE6C743-FCA4-47CA-97A2-645BAB61E1BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE