diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index d9a5a15be..88f86b20b 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -25,7 +25,11 @@ }, "DatabaseProvider": "Sqlite", "LoggingFramework": { - "Defaults": ["Console", "FilePretty", "FileJson"], + "Defaults": [ + "Console", + "FilePretty", + "FileJson" + ], "Sinks": [ { "Type": "Console", @@ -42,6 +46,17 @@ "DisableColors": true } }, + { + "Type": "Console", + "Name": "Datadog", + "Options": { + "MinLevel": "Information", + "Formatter": "json", + "TimestampFormat": "O", + "UseUtcTimestamp": true, + "DisableColors": true + } + }, { "Type": "Console", "Name": "ConsoleSystemd", diff --git a/src/modules/Elsa.Common/Converters/BooleanConverter.cs b/src/modules/Elsa.Common/Converters/BooleanConverter.cs new file mode 100644 index 000000000..253c65f64 --- /dev/null +++ b/src/modules/Elsa.Common/Converters/BooleanConverter.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elsa.Common.Converters; + +public class BooleanConverter : JsonConverter +{ + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.String: + var value = reader.GetString(); + if (bool.TryParse(value, out var b)) + return b; + break; + } + throw new JsonException($"Cannot convert {reader.TokenType} to bool"); + } + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Common/Converters/NullableBooleanConverter.cs b/src/modules/Elsa.Common/Converters/NullableBooleanConverter.cs new file mode 100644 index 000000000..7aed5ffdc --- /dev/null +++ b/src/modules/Elsa.Common/Converters/NullableBooleanConverter.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elsa.Common.Converters; + +public class NullableBooleanConverter : JsonConverter +{ + public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.String: + var value = reader.GetString(); + if (bool.TryParse(value, out var b)) + return b; + break; + case JsonTokenType.Null: + return null; + } + throw new JsonException($"Cannot convert {reader.TokenType} to bool?"); + } + + public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) + { + if (value.HasValue) + writer.WriteBooleanValue(value.Value); + else + writer.WriteNullValue(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Logging.Console/ConsoleLogSinkFactory.cs b/src/modules/Elsa.Logging.Console/ConsoleLogSinkFactory.cs index 78e816737..cbc8e511d 100644 --- a/src/modules/Elsa.Logging.Console/ConsoleLogSinkFactory.cs +++ b/src/modules/Elsa.Logging.Console/ConsoleLogSinkFactory.cs @@ -1,7 +1,9 @@ using Elsa.Logging.Contracts; using Elsa.Logging.Extensions; using Elsa.Logging.Sinks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; namespace Elsa.Logging.Console; @@ -20,13 +22,19 @@ public sealed class ConsoleLogSinkFactory : ILogSinkFactory public string Type => "Console"; - /// public ILogSink Create(string name, ConsoleLogSinkOptions options) { var factory = LoggerFactory.Create(builder => { builder.ClearProviders(); builder.AddCategoryFilters(options); + + builder.Services.Configure(cfo => + { + if (options.TimestampFormat is not null) cfo.TimestampFormat = options.TimestampFormat; + if (options.IncludeScopes is not null) cfo.IncludeScopes = options.IncludeScopes.Value; + if (options.UseUtcTimestamp is not null) cfo.UseUtcTimestamp = options.UseUtcTimestamp.Value; + }); var min = options.MinLevel ?? LogLevel.Information; @@ -49,14 +57,16 @@ public sealed class ConsoleLogSinkFactory : ILogSinkFactory(); builder.AddConsole(o => { - if (options.TimestampFormat is not null) o.TimestampFormat = options.TimestampFormat; - if (options.DisableColors is not null) o.DisableColors = options.DisableColors.Value; - if (options.IncludeScopes is not null) o.IncludeScopes = options.IncludeScopes.Value; + o.FormatterName = JsonDestructuringConsoleFormatter.FormatterName; }); break; + default: + builder.AddConsole(); + break; } builder.SetMinimumLevel(min); diff --git a/src/modules/Elsa.Logging.Console/ConsoleLogSinkOptions.cs b/src/modules/Elsa.Logging.Console/ConsoleLogSinkOptions.cs index 57002fed7..b146fe451 100644 --- a/src/modules/Elsa.Logging.Console/ConsoleLogSinkOptions.cs +++ b/src/modules/Elsa.Logging.Console/ConsoleLogSinkOptions.cs @@ -15,7 +15,7 @@ public sealed record ConsoleLogSinkOptions : LogSinkOptions // "Default" | "Simple" | "Systemd" public string Formatter { get; init; } = "Default"; public string? TimestampFormat { get; init; } - public bool? IncludeScopes { get; init; } + public bool? IncludeScopes { get; init; } = true; // Default console public bool? DisableColors { get; init; } @@ -24,4 +24,5 @@ public sealed record ConsoleLogSinkOptions : LogSinkOptions public LoggerColorBehavior? ColorBehavior { get; init; } public bool? SingleLine { get; init; } public bool? UseUtcTimestamp { get; init; } + public bool JsonIndented { get; set; } = true; } \ No newline at end of file diff --git a/src/modules/Elsa.Logging.Console/JsonDestructuringConsoleFormatter.cs b/src/modules/Elsa.Logging.Console/JsonDestructuringConsoleFormatter.cs new file mode 100644 index 000000000..ceff5f680 --- /dev/null +++ b/src/modules/Elsa.Logging.Console/JsonDestructuringConsoleFormatter.cs @@ -0,0 +1,84 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; + +namespace Elsa.Logging.Console; + +/// +/// A custom console formatter that formats log entries as JSON with destructured data. +/// +/// +/// This formatter outputs log information in a JSON format, including details such as log level, category, message, +/// state, and scopes. The formatter is designed to provide a structured and easily parseable log output for improved log analysis. +/// +public sealed class JsonDestructuringConsoleFormatter() : ConsoleFormatter(FormatterName) +{ + public const string FormatterName = "json-destructuring"; + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopes, TextWriter writer) + { + using var stream = new MemoryStream(); + using var json = new Utf8JsonWriter(stream, new() + { + Indented = false + }); + + json.WriteStartObject(); + json.WriteString("LogLevel", logEntry.LogLevel.ToString()); + json.WriteString("Category", logEntry.Category); + json.WriteString("Message", logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception)); + + // STATE (TState might be IReadOnlyList>) + if (logEntry.State is IEnumerable> kvs) + { + json.WriteStartObject("State"); + foreach (var kv in kvs) + { + if (kv.Key == "{OriginalFormat}") + { + json.WriteString(kv.Key, kv.Value?.ToString()); + continue; + } + + json.WritePropertyName(kv.Key); + JsonSerializer.Serialize(json, kv.Value); + } + + json.WriteEndObject(); + } + + // SCOPES + if (scopes is not null) + { + json.WriteStartArray("Scopes"); + scopes.ForEachScope((scope, state) => + { + switch (scope) + { + case IEnumerable> scopeKvs: + state.WriteStartObject(); + foreach (var kv in scopeKvs) + { + state.WritePropertyName(kv.Key); + JsonSerializer.Serialize(state, kv.Value); + } + + state.WriteEndObject(); + break; + default: + JsonSerializer.Serialize(state, scope); + break; + } + }, json); + json.WriteEndArray(); + } + + json.WriteEndObject(); + json.Flush(); + + // Write the JSON string to the TextWriter + writer.Write(Encoding.UTF8.GetString(stream.ToArray())); + writer.WriteLine(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Logging.Core/HostedServices/LogEntryBackgroundWorker.cs b/src/modules/Elsa.Logging.Core/HostedServices/LogEntryBackgroundWorker.cs index 0e1df3f31..fbea54b9b 100644 --- a/src/modules/Elsa.Logging.Core/HostedServices/LogEntryBackgroundWorker.cs +++ b/src/modules/Elsa.Logging.Core/HostedServices/LogEntryBackgroundWorker.cs @@ -2,6 +2,7 @@ using Elsa.Logging.Contracts; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Elsa.Logging.HostedServices; @@ -10,21 +11,28 @@ namespace Elsa.Logging.HostedServices; /// and routing them to appropriate log sinks. /// [UsedImplicitly] -public class LogEntryBackgroundWorker(ILogEntryQueue queue, IServiceScopeFactory scopeFactory) : BackgroundService +public class LogEntryBackgroundWorker(ILogEntryQueue queue, IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await foreach (var instruction in queue.DequeueAsync().WithCancellation(stoppingToken)) { - using var scope = scopeFactory.CreateScope(); - var router = scope.ServiceProvider.GetRequiredService(); - await router.WriteAsync( - instruction.SinkNames, - instruction.Category, - instruction.Level, - instruction.Message, - instruction.Arguments, - instruction.Attributes, stoppingToken); + try + { + using var scope = scopeFactory.CreateScope(); + var router = scope.ServiceProvider.GetRequiredService(); + await router.WriteAsync( + instruction.SinkNames, + instruction.Category, + instruction.Level, + instruction.Message, + instruction.Arguments, + instruction.Attributes, stoppingToken); + } + catch (Exception e) + { + logger.LogError(e, "An error occurred while processing a log entry {@LogEntry}", instruction); + } } } } diff --git a/src/modules/Elsa.Logging.Core/Providers/ConfigurationLogSinkProvider.cs b/src/modules/Elsa.Logging.Core/Providers/ConfigurationLogSinkProvider.cs index 42108c70b..c306fc9d5 100644 --- a/src/modules/Elsa.Logging.Core/Providers/ConfigurationLogSinkProvider.cs +++ b/src/modules/Elsa.Logging.Core/Providers/ConfigurationLogSinkProvider.cs @@ -1,9 +1,9 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Elsa.Common.Converters; using Elsa.Extensions; using Elsa.Logging.Contracts; using Elsa.Logging.Models; -using Elsa.Logging.Serialization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -21,7 +21,8 @@ public class ConfigurationLogSinkProvider : ILogSinkProvider PropertyNameCaseInsensitive = true, Converters = { - new NullableBoolConverter(), + new NullableBooleanConverter(), + new BooleanConverter(), new JsonStringEnumConverter() } }; diff --git a/src/modules/Elsa.Logging.Core/Serialization/NullableBoolConverter.cs b/src/modules/Elsa.Logging.Core/Serialization/NullableBoolConverter.cs deleted file mode 100644 index d875c6e29..000000000 --- a/src/modules/Elsa.Logging.Core/Serialization/NullableBoolConverter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Elsa.Logging.Serialization; - -/// -/// A custom JSON converter for nullable boolean values. -/// Provides functionality to serialize and deserialize nullable boolean values -/// in JSON, including support for the string representation of boolean values -/// ("true", "false") and handling of null cases. -/// -public class NullableBoolConverter : JsonConverter -{ - public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - switch (reader.TokenType) - { - // Handle real boolean - case JsonTokenType.True: - return true; - case JsonTokenType.False: - return false; - // Handle string "true"/"false" - case JsonTokenType.String: - { - var value = reader.GetString(); - if (bool.TryParse(value, out var b)) - return b; - break; - } - } - - // Handle null - return reader.TokenType == JsonTokenType.Null ? null : throw new JsonException($"Cannot convert {reader.TokenType} to bool?"); - } - - public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) - { - if (value.HasValue) - writer.WriteBooleanValue(value.Value); - else - writer.WriteNullValue(); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Logging.Core/Sinks/LoggerSink.cs b/src/modules/Elsa.Logging.Core/Sinks/LoggerSink.cs index b97b4b15d..af7cb0c01 100644 --- a/src/modules/Elsa.Logging.Core/Sinks/LoggerSink.cs +++ b/src/modules/Elsa.Logging.Core/Sinks/LoggerSink.cs @@ -14,42 +14,13 @@ public sealed class LoggerSink(string name, ILoggerFactory factory) : ILogSink /// public ValueTask WriteAsync(string name, LogLevel level, string message, object? arguments, IDictionary? attributes = null, CancellationToken cancellationToken = default) { - var logger = factory.CreateLogger(name); + var l = factory.CreateLogger(name); - if (!logger.IsEnabled(level)) + if (!l.IsEnabled(level)) return ValueTask.CompletedTask; - using var scope = attributes is null ? null : logger.BeginScope(attributes); - logger.Log(level, 0, arguments, null, (state, ex) => FormatMessage(message, state)); + using var scope = attributes is null ? null : l.BeginScope(attributes); + l.Log(level, 0, null, message, arguments); return ValueTask.CompletedTask; } - - private static string FormatMessage(string message, object? state) - { - // If the state is an array, use string.Format. - if (state is object[] { Length: > 0 } args) - return string.Format(message, args); - - // Otherwise, use string interpolation. No need to use StringBuilder here, since the message is not expected to be long. - var formattedMessage = message; - - // If the state is a dictionary, use string interpolation. - if (state is IDictionary dict) - { - foreach (var kvp in dict) - formattedMessage = formattedMessage.Replace($"{{{kvp.Key}}}", kvp.Value?.ToString()); - } - // Otherwise, use reflection to find properties on the state object. - else if (state is not null) - { - var props = state.GetType().GetProperties(); - foreach (var prop in props) - { - var value = prop.GetValue(state); - formattedMessage = formattedMessage.Replace($"{{{prop.Name}}}", value?.ToString()); - } - } - - return formattedMessage; - } } \ No newline at end of file diff --git a/src/modules/Elsa.Logging/Activities/Log.cs b/src/modules/Elsa.Logging/Activities/Log.cs index 5728441ba..355dc863d 100644 --- a/src/modules/Elsa.Logging/Activities/Log.cs +++ b/src/modules/Elsa.Logging/Activities/Log.cs @@ -1,4 +1,6 @@ +using System.Dynamic; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Text.Json.Serialization; using Elsa.Extensions; using Elsa.Logging.Contracts; @@ -90,6 +92,13 @@ public class Log : CodeActivity var message = Message.Get(context); var level = Level.Get(context); var arguments = Arguments.GetOrDefault(context); + + if (arguments is string argumentString) + { + // Could be JSON created from e.g., Liquid template. If so, parse it into an ExpandoObject. + arguments = TryParseJson(argumentString); + } + var attributes = Attributes.GetOrDefault(context) ?? new Dictionary(); var sinkNames = SinkNames.GetOrDefault(context) ?? new List(); var category = Category.GetOrDefault(context); @@ -112,4 +121,16 @@ public class Log : CodeActivity }; await queue.EnqueueAsync(instruction); } + + private object TryParseJson(string json) + { + try + { + return JsonSerializer.Deserialize(json) ?? new ExpandoObject(); + } + catch + { + return json; + } + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index ada56a486..d664b5e83 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -551,7 +551,7 @@ public partial class WorkflowExecutionContext : IExecutionContext var now = SystemClock.UtcNow; var id = IdentityGenerator.GenerateId(); var activityExecutionContext = new ActivityExecutionContext(id, this, parentContext, activity, activityDescriptor, now, tag, SystemClock, CancellationToken); - var variablesToDeclare = options?.Variables ?? Array.Empty(); + var variablesToDeclare = options?.Variables ?? []; var variableContainer = new[] { activityExecutionContext.ActivityNode @@ -614,7 +614,7 @@ public partial class WorkflowExecutionContext : IExecutionContext { // Filter out completed activity execution contexts, except for the root Workflow activity context, which stores workflow-level variables. // This will currently break scripts accessing activity output directly, but there's a workaround for that via variable capturing. - // We may ultimately restore direct output access, but in a different way. + // We may ultimately restore direct output access, but differently. return ActivityExecutionContexts.Where(x => !x.IsCompleted || x.ParentActivityExecutionContext == null); } diff --git a/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLogger.cs b/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLogger.cs new file mode 100644 index 000000000..beda56faa --- /dev/null +++ b/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLogger.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace Elsa.Logging.Core.IntegrationTests.Helpers; + +class TestLogger : ILogger +{ + public List<(LogLevel level, EventId eventId, object state, Exception? exception, Delegate formatter)> Calls { get; } = new(); + public bool IsEnabledCalled { get; private set; } + + public bool IsEnabled(LogLevel logLevel) + { + IsEnabledCalled = true; + return true; + } + + public IDisposable BeginScope(TState state) => null!; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + Calls.Add((logLevel, eventId, state!, exception, formatter)); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLoggerFactory.cs b/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLoggerFactory.cs new file mode 100644 index 000000000..230ef345f --- /dev/null +++ b/test/integration/Elsa.Logging.Core.IntegrationTests/Helpers/TestLoggerFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Logging; + +namespace Elsa.Logging.Core.IntegrationTests.Helpers; + +class TestLoggerFactory(ILogger logger) : ILoggerFactory +{ + public ILogger CreateLogger(string categoryName) => logger; + public void AddProvider(ILoggerProvider provider) { } + public void Dispose() { } +} \ No newline at end of file diff --git a/test/integration/Elsa.Logging.Core.IntegrationTests/LogSinkRouterTests.cs b/test/integration/Elsa.Logging.Core.IntegrationTests/LogSinkRouterTests.cs index 9c859fb4f..2a4372739 100644 --- a/test/integration/Elsa.Logging.Core.IntegrationTests/LogSinkRouterTests.cs +++ b/test/integration/Elsa.Logging.Core.IntegrationTests/LogSinkRouterTests.cs @@ -5,6 +5,7 @@ using Elsa.Logging.Contracts; using Elsa.Logging.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Elsa.Logging.Core.IntegrationTests.Helpers; using Moq; namespace Elsa.Logging.Core.IntegrationTests; @@ -14,11 +15,9 @@ public class LogSinkRouterTests [Fact] public async Task LogEntryInstruction_ShouldFlowThroughQueueAndRouterToSink() { - var loggerFactoryMock = new Mock(); - var loggerMock = new Mock(); - loggerFactoryMock.Setup(f => f.CreateLogger(It.IsAny())).Returns(loggerMock.Object); - loggerMock.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - var sink = new LoggerSink("TestSink", loggerFactoryMock.Object); + var testLogger = new TestLogger(); + var loggerFactory = new TestLoggerFactory(testLogger); + var sink = new LoggerSink("TestSink", loggerFactory); var catalogMock = new Mock(); catalogMock.Setup(c => c.ListAsync(CancellationToken.None)).ReturnsAsync(new List { @@ -38,7 +37,7 @@ public class LogSinkRouterTests Level = LogLevel.Information, Message = "Test message" }; - + await queue.EnqueueAsync(instruction); await foreach (var dequeued in queue.DequeueAsync()) { @@ -46,11 +45,10 @@ public class LogSinkRouterTests break; } - loggerMock.Verify(l => l.Log( - LogLevel.Information, - 0, - null, - null, - It.IsAny>()!), Times.Once); + // Assert that Log was called once with expected parameters + Assert.Single(testLogger.Calls); + var call = testLogger.Calls[0]; + Assert.Equal(LogLevel.Information, call.level); + Assert.Null(call.exception); } } \ No newline at end of file diff --git a/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLogger.cs b/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLogger.cs new file mode 100644 index 000000000..4fc697d5a --- /dev/null +++ b/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLogger.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace Elsa.Logging.Core.UnitTests.Helpers; + +class TestLogger : ILogger +{ + public List<(LogLevel level, EventId eventId, object state, Exception? exception, Delegate formatter)> Calls { get; } = new(); + public bool IsEnabledCalled { get; private set; } + + public bool IsEnabled(LogLevel logLevel) + { + IsEnabledCalled = true; + return true; + } + + public IDisposable BeginScope(TState state) => null!; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + Calls.Add((logLevel, eventId, state!, exception, formatter)); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLoggerFactory.cs b/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLoggerFactory.cs new file mode 100644 index 000000000..8ee648a74 --- /dev/null +++ b/test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLoggerFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Logging; + +namespace Elsa.Logging.Core.UnitTests.Helpers; + +class TestLoggerFactory(ILogger logger) : ILoggerFactory +{ + public ILogger CreateLogger(string categoryName) => logger; + public void AddProvider(ILoggerProvider provider) { } + public void Dispose() { } +} \ No newline at end of file diff --git a/test/unit/Elsa.Logging.Core.UnitTests/LoggerSinkTests.cs b/test/unit/Elsa.Logging.Core.UnitTests/LoggerSinkTests.cs index fecfc436e..8125c6997 100644 --- a/test/unit/Elsa.Logging.Core.UnitTests/LoggerSinkTests.cs +++ b/test/unit/Elsa.Logging.Core.UnitTests/LoggerSinkTests.cs @@ -1,6 +1,6 @@ +using Elsa.Logging.Core.UnitTests.Helpers; using Elsa.Logging.Sinks; using Microsoft.Extensions.Logging; -using Moq; namespace Elsa.Logging.Core.UnitTests; @@ -9,17 +9,12 @@ public class LoggerSinkTests [Fact] public async Task WriteAsync_ShouldLogMessage() { - var loggerFactoryMock = new Mock(); - var loggerMock = new Mock(); - loggerFactoryMock.Setup(f => f.CreateLogger(It.IsAny())).Returns(loggerMock.Object); - loggerMock.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - var sink = new LoggerSink("TestLogger", loggerFactoryMock.Object); + var testLogger = new TestLogger(); + var loggerFactory = new TestLoggerFactory(testLogger); + var sink = new LoggerSink("TestLogger", loggerFactory); await sink.WriteAsync("TestLogger", LogLevel.Information, "Test message", null, null); - loggerMock.Verify(l => l.Log( - LogLevel.Information, - 0, - null, - null, - It.IsAny>()), Times.Once); + Assert.Single(testLogger.Calls); + var call = testLogger.Calls[0]; + Assert.Equal(LogLevel.Information, call.level); } } \ No newline at end of file