Improve Logger With Destructuring of ExpandoObject (#6877)
* Initial implementation of log activity + base sink * Refactor logging implementation: replace `Elsa.ProcessLogging` with a new modular `Elsa.Logging` framework, introducing support for configurable log sinks, enhanced logging extensibility, and updated dependencies in consuming projects. * Enhance logging framework: introduce custom `NullableBoolConverter` and update JSON serialization/deserialization logic for log sink handling. * Update description for `Log` activity input: clarify target sinks configuration * Set default value of `SinkNames` input in `Log` activity to non-nullable collection * Set `DisplayName` for `Sinks` input in `Log` activity * Refactor logging framework: update `ILogSink` and `ILogSinkRouter` to support arguments and attributes, enhance `Log` activity to use updated interfaces, and add default category handling. * Refactor logging framework: simplify argument handling in `ILogSink` and `ILogSinkRouter`, update `Log` activity inputs, and improve message formatting in `MelLogSink`. * Update logging framework to simplify log sink creation, enhance category filtering, and refactor `ILogSink`/`ILogSinkRouter` interface methods. * Introduce modular logging framework enhancements: add `Console` and `Serilog` logging features, refactor `ILogSink` framework, and update projects to align with a modular architecture. * Refactor logging framework: introduce `AddCategoryFilters` extension, replace `DefaultCategory` handling with enhanced category filters, and update sink creation logic for consistency. * Refactor logging framework: rename `SinkOptions` to `LogSinkOptions`, standardize naming across log sink types, and update configuration and sink factory logic for consistency. * Enhance logging framework: add `ConfigureDefaults` methods, update `ILogSinkCatalog` to use `IServiceScopeFactory`, and improve logging configuration handling and defaults setup. * Introduce asynchronous log entry processing: add `ILogEntryQueue`, `LogEntryBackgroundWorker`, and related models to enable queue-based logging and background processing. Update `Log` activity to enqueue log entries for processing. * Add unit and integration tests for `Elsa.Logging.Core` library, refactor logger setup in `Elsa.Server.Web`, enhance logging configuration, and standardize `Directory.Packages.props` file. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add documentation comments to logging framework classes, interfaces, methods, and factories to enhance code readability and maintainability. Remove unused `CustomPurpleConsoleFormatter` class and `logs` folder from server project. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Make `LogEntryInstruction` and `LogEntryQueue` classes public and simplify return statement in `LogSinkCatalog.ListAsync` method. * Standardize terminology in `ILogSink` interface and `LoggerSink` implementation: rename `properties` to `attributes`. Update project files and solution structure to reflect integration test additions. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update documentation comments in `LoggingFeature` and `LogEntryInstruction` to clarify functionality and improve precision. * Add README for `Elsa.Logging` module with configuration examples, usage details, and extension guidance. * Add `Dictionary` UI hint to `InputUIHints` and update `Attributes` in `Log` activity to use it. * Update `Log` activity default category to "Process", add integration tests for logging, and enhance null safety in `ConfigurationLogSinkProvider`. * Remove `UseLoggingFramework` middleware from `Program.cs` to streamline workflow initialization. * Refactor `LoggerSink` to simplify logging logic and remove unused `FormatMessage` method. Enhance `Log` activity argument handling by introducing JSON parsing for string inputs. * Refactor logging tests to improve consistency and update variable initialization in `WorkflowExecutionContext` for cleaner syntax. * Replace mocked logger dependencies in tests with `TestLogger` and `TestLoggerFactory` for improved readability and maintainability. * Add `JsonDestructuringConsoleFormatter` for structured JSON logging and update logging configuration to support new formatter. * Refactor JSON converters: replace `NullableBoolConverter` with `NullableBooleanConverter` and `BooleanConverter` for improved readability and consistency. * Add error handling and logging to `LogEntryBackgroundWorker` Introduce exception handling with logging in `LogEntryBackgroundWorker` to capture and log errors during log entry processing. Added `ILogger` dependency for structured error reporting. --------- Co-authored-by: lucas.hipolito <lukhipolito@yahoo.com.br> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8645493b04
commit
ca85fe0f6d
|
|
@ -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",
|
||||
|
|
|
|||
29
src/modules/Elsa.Common/Converters/BooleanConverter.cs
Normal file
29
src/modules/Elsa.Common/Converters/BooleanConverter.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Common.Converters;
|
||||
|
||||
public class BooleanConverter : JsonConverter<bool>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Common.Converters;
|
||||
|
||||
public class NullableBooleanConverter : JsonConverter<bool?>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ConsoleLogSinkOption
|
|||
/// <inheritdoc/>
|
||||
public string Type => "Console";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogSink Create(string name, ConsoleLogSinkOptions options)
|
||||
{
|
||||
var factory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddCategoryFilters(options);
|
||||
|
||||
builder.Services.Configure<ConsoleFormatterOptions>(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<ConsoleLogSinkOption
|
|||
if (options.IncludeScopes is not null) o.IncludeScopes = options.IncludeScopes.Value;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
case "json":
|
||||
builder.AddConsoleFormatter<JsonDestructuringConsoleFormatter, ConsoleFormatterOptions>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A custom console formatter that formats log entries as JSON with destructured data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class JsonDestructuringConsoleFormatter() : ConsoleFormatter(FormatterName)
|
||||
{
|
||||
public const string FormatterName = "json-destructuring";
|
||||
public override void Write<TState>(in LogEntry<TState> 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<KeyValuePair<string,object?>>)
|
||||
if (logEntry.State is IEnumerable<KeyValuePair<string, object?>> 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<KeyValuePair<string, object?>> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class LogEntryBackgroundWorker(ILogEntryQueue queue, IServiceScopeFactory scopeFactory) : BackgroundService
|
||||
public class LogEntryBackgroundWorker(ILogEntryQueue queue, IServiceScopeFactory scopeFactory, ILogger<LogEntryBackgroundWorker> 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<ILogSinkRouter>();
|
||||
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<ILogSinkRouter>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Logging.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class NullableBoolConverter : JsonConverter<bool?>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,42 +14,13 @@ public sealed class LoggerSink(string name, ILoggerFactory factory) : ILogSink
|
|||
/// <inheritdoc/>
|
||||
public ValueTask WriteAsync(string name, LogLevel level, string message, object? arguments, IDictionary<string, object?>? 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<string, object?> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, object?>();
|
||||
var sinkNames = SinkNames.GetOrDefault(context) ?? new List<string>();
|
||||
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<ExpandoObject>(json) ?? new ExpandoObject();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Variable>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>(TState state) => null!;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
Calls.Add((logLevel, eventId, state!, exception, formatter));
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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<ILoggerFactory>();
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
loggerFactoryMock.Setup(f => f.CreateLogger(It.IsAny<string>())).Returns(loggerMock.Object);
|
||||
loggerMock.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).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<ILogSinkCatalog>();
|
||||
catalogMock.Setup(c => c.ListAsync(CancellationToken.None)).ReturnsAsync(new List<ILogSink>
|
||||
{
|
||||
|
|
@ -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<Func<object, Exception, string>>()!), 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);
|
||||
}
|
||||
}
|
||||
22
test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLogger.cs
Normal file
22
test/unit/Elsa.Logging.Core.UnitTests/Helpers/TestLogger.cs
Normal file
|
|
@ -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>(TState state) => null!;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
Calls.Add((logLevel, eventId, state!, exception, formatter));
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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<ILoggerFactory>();
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
loggerFactoryMock.Setup(f => f.CreateLogger(It.IsAny<string>())).Returns(loggerMock.Object);
|
||||
loggerMock.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).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<Func<object, Exception, string>>()), Times.Once);
|
||||
Assert.Single(testLogger.Calls);
|
||||
var call = testLogger.Calls[0];
|
||||
Assert.Equal(LogLevel.Information, call.level);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue