diff --git a/SystemExtensions.NetStandard/Extensions/LoggingExtensions.cs b/SystemExtensions.NetStandard/Extensions/LoggingExtensions.cs new file mode 100644 index 0000000..d1894dd --- /dev/null +++ b/SystemExtensions.NetStandard/Extensions/LoggingExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; +using System.Logging; + +namespace System.Extensions +{ + public static class LoggingExtensions + { + public static ScopedLogger CreateScopedLogger(this ILogger logger, string methodName, string flowIdentifier) + { + return ScopedLogger.Create(logger, methodName, flowIdentifier); + } + } +} \ No newline at end of file diff --git a/SystemExtensions.NetStandard/Logging/ScopedLogger.cs b/SystemExtensions.NetStandard/Logging/ScopedLogger.cs new file mode 100644 index 0000000..2089771 --- /dev/null +++ b/SystemExtensions.NetStandard/Logging/ScopedLogger.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using System.Extensions; + +namespace System.Logging +{ + public struct ScopedLogger + { + private readonly ILogger logger; + private readonly string scope; + private readonly string flowId; + + private ScopedLogger(ILogger logger, string scope, string flowId) + { + this.logger = logger; + this.scope = scope; + this.flowId = flowId.IsNullOrWhiteSpace() ? string.Empty : $"[{flowId}] "; + } + + public ScopedLogger LogInformation(string message, params object[] parameters) + { + this.logger.LogInformation(this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogDebug(string message, params object[] parameters) + { + this.logger.LogDebug(this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogWarning(string message, params object[] parameters) + { + this.logger.LogWarning(this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogError(string message, params object[] parameters) + { + this.logger.LogError(this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogCritical(string message, params object[] parameters) + { + this.logger.LogCritical(this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogWarning(Exception e, string message, params object[] parameters) + { + this.logger.LogWarning(e, this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogError(Exception e, string message, params object[] parameters) + { + this.logger.LogError(e, this.CreateMessage(message), parameters); + return this; + } + + public ScopedLogger LogCritical(Exception e, string message, params object[] parameters) + { + this.logger.LogCritical(e, this.CreateMessage(message), parameters); + return this; + } + + private string CreateMessage(string message) + { + return $"{this.flowId}{this.scope}: {message}"; + } + + public static ScopedLogger Create(ILogger logger, string scope, string flowId) + { + return new ScopedLogger(logger, scope.ThrowIfNull(nameof(scope)), flowId.ThrowIfNull(nameof(flowId))); + } + } +} \ No newline at end of file diff --git a/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj b/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj index f1dfeb0..8bf7eec 100644 --- a/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj +++ b/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj @@ -7,7 +7,7 @@ LICENSE true System - 1.3.2 + 1.4 Alexandru Macocian https://github.com/AlexMacocian/SystemExtensions Extensions for the System namespace @@ -21,6 +21,7 @@ + diff --git a/SystemExtensions.Tests/Logging/Models/CachingLogger.cs b/SystemExtensions.Tests/Logging/Models/CachingLogger.cs new file mode 100644 index 0000000..502bbc6 --- /dev/null +++ b/SystemExtensions.Tests/Logging/Models/CachingLogger.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace SystemExtensions.NetStandard.Tests.Logging.Models +{ + public sealed class CachingLogger : ILogger + { + public List LogCache { get; } = new(); + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var message = formatter(state, exception); + this.LogCache.Add(message); + } + } +} \ No newline at end of file diff --git a/SystemExtensions.Tests/Logging/ScopedLoggerTests.cs b/SystemExtensions.Tests/Logging/ScopedLoggerTests.cs new file mode 100644 index 0000000..2889486 --- /dev/null +++ b/SystemExtensions.Tests/Logging/ScopedLoggerTests.cs @@ -0,0 +1,266 @@ +using System.Extensions; +using System.Linq; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SystemExtensions.NetStandard.Tests.Logging.Models; + +namespace System.Logging.Tests +{ + [TestClass] + public class ScopedLoggerTests + { + private const string Flow = "Flow"; + private const string Message = "Some message"; + private readonly CachingLogger cachingLogger = new(); + + [TestMethod] + public void CreateLogger_CreatesNewLogger() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.CreateLogger_CreatesNewLogger), Flow); + scopedLogger.Should().BeOfType>(); + } + + [TestMethod] + public void CreateLogger_NullScope_ThrowsArgumentNullException() + { + var action = new Action(() => + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(null, Flow); + }); + + + action.Should().Throw(); + } + + [TestMethod] + public void CreateLogger_NullFlow_ThrowsArgumentNullException() + { + var action = new Action(() => + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.CreateLogger_NullFlow_ThrowsArgumentNullException), null); + }); + + + action.Should().Throw(); + } + + [TestMethod] + public void LogInformation_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogInformation_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogInformation_LogsExpected)}: {Message}"; + + scopedLogger.LogInformation(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogInformation_EmptyFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogInformation_LogsExpected), string.Empty); + var expectedMessage = $"{nameof(this.LogInformation_LogsExpected)}: {Message}"; + + scopedLogger.LogInformation(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogInformation_WhitespaceFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogInformation_LogsExpected), " "); + var expectedMessage = $"{nameof(this.LogInformation_LogsExpected)}: {Message}"; + + scopedLogger.LogInformation(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogDebug_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogDebug_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogDebug_LogsExpected)}: {Message}"; + + scopedLogger.LogDebug(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogDebug_EmptyFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogDebug_EmptyFlow_LogsExpected), string.Empty); + var expectedMessage = $"{nameof(this.LogDebug_EmptyFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogDebug(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogDebug_WhitespaceFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogDebug_WhitespaceFlow_LogsExpected), " "); + var expectedMessage = $"{nameof(this.LogDebug_WhitespaceFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogDebug(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogWarning_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogWarning_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogWarning_LogsExpected)}: {Message}"; + + scopedLogger.LogWarning(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogWarning_EmptyFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogWarning_EmptyFlow_LogsExpected), string.Empty); + var expectedMessage = $"{nameof(this.LogWarning_EmptyFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogWarning(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogWarning_WhitespaceFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogWarning_WhitespaceFlow_LogsExpected), " "); + var expectedMessage = $"{nameof(this.LogWarning_WhitespaceFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogWarning(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogError_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogError_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogError_LogsExpected)}: {Message}"; + + scopedLogger.LogError(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogError_EmptyFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogError_EmptyFlow_LogsExpected), string.Empty); + var expectedMessage = $"{nameof(this.LogError_EmptyFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogError(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogError_WhitespaceFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogError_WhitespaceFlow_LogsExpected), " "); + var expectedMessage = $"{nameof(this.LogError_WhitespaceFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogError(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogCritical_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogCritical_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogCritical_LogsExpected)}: {Message}"; + + scopedLogger.LogCritical(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogCritical_EmptyFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogCritical_EmptyFlow_LogsExpected), string.Empty); + var expectedMessage = $"{nameof(this.LogCritical_EmptyFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogCritical(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogCritical_WhitespaceFlow_LogsExpected() + { + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogCritical_WhitespaceFlow_LogsExpected), " "); + var expectedMessage = $"{nameof(this.LogCritical_WhitespaceFlow_LogsExpected)}: {Message}"; + + scopedLogger.LogCritical(Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogWarning_WithException_LogsExpected() + { + var exception = new Exception(); + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogWarning_WithException_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogWarning_WithException_LogsExpected)}: {Message}"; + + scopedLogger.LogWarning(exception, Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogError_WithException_LogsExpected() + { + var exception = new Exception(); + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogError_WithException_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogError_WithException_LogsExpected)}: {Message}"; + + scopedLogger.LogError(exception, Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + + [TestMethod] + public void LogCritical_WithException_LogsExpected() + { + var exception = new Exception(); + var scopedLogger = this.cachingLogger.CreateScopedLogger(nameof(this.LogCritical_WithException_LogsExpected), Flow); + var expectedMessage = $"[{Flow}] {nameof(this.LogCritical_WithException_LogsExpected)}: {Message}"; + + scopedLogger.LogCritical(exception, Message); + + this.cachingLogger.LogCache.Should().HaveCount(1); + this.cachingLogger.LogCache.First().Should().Be(expectedMessage); + } + } +} \ No newline at end of file