From 90b4e7d6c5e914514fd7be6a0d600b2cfc54ee3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Hip=C3=B3lito?= Date: Thu, 4 Sep 2025 10:57:48 +0200 Subject: [PATCH] Initial implementation of log activity + base sink (#6859) * Initial implementation of log activity + base sink * Update src/modules/Elsa.ProcessLogging/Extensions/ModuleExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 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. * Log Activity * Auto stash before merge of "feat/6854-process-log-activity" and "patch/3.5.1" * 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. * Handling Dictionary component values with expressions * Small improvements * Increasing studio version * Introduce custom activity input evaluators and support evaluator type registration. (#6889) * Introduce custom activity input evaluators and support evaluator type registration. Added `DefaultActivityInputEvaluator` and `DictionaryValueEvaluator` for custom input evaluation logic. Modified `InputAttribute` and `InputDescriptor` to support specifying evaluator types. Updated relevant activity, service, and feature registrations to integrate the new evaluators. * Remove legacy Dictionary UI hint implementation and refactor `DictionaryValueEvaluator` integration. * Removed obsolete `Dictionary` UI hint-related classes. * Introduced `DictionaryUIHintInputModifier` for centralized modifier logic. * Relocated `DictionaryValueEvaluator` to the appropriate namespace and updated feature registrations. * Fix formatting of `Category` property in `Log` activity. * Refactor input evaluation: replace `ActivatorUtilities` with `GetRequiredService` and update `DictionaryValueEvaluator` to handle dynamic types. * Remove unused logging-related imports from `Program.cs`. * Update src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add logging to `DictionaryValueEvaluator` for missing property warnings. --------- Co-authored-by: Sipke Schoorstra Co-authored-by: lukhipolito-nexxbiz Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Directory.Packages.props | 2 +- src/apps/Elsa.Server.Web/Program.cs | 2 - src/modules/Elsa.Logging/Activities/Log.cs | 29 +++++----- src/modules/Elsa.Logging/Elsa.Logging.csproj | 6 +- .../Attributes/InputAttribute.cs | 6 ++ .../Contexts/ActivityInputEvaluatorContext.cs | 12 ++++ .../Contracts/IActivityInputEvaluator.cs | 6 ++ ...cutionContextExtensions.InputEvaluation.cs | 11 +++- .../Features/WorkflowsFeature.cs | 8 ++- .../Models/InputDescriptor.cs | 22 +++++--- .../Services/ActivityDescriber.cs | 6 +- .../Services/DefaultActivityInputEvaluator.cs | 14 +++++ .../DictionaryUIHintInputModifier.cs | 14 +++++ .../Dictionary/DictionaryValueEvaluator.cs | 55 +++++++++++++++++++ 14 files changed, 158 insertions(+), 35 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs create mode 100644 src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs create mode 100644 src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs create mode 100644 src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs create mode 100644 src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6af29175e..990f42ecb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - 3.5.0 + 3.5.1-preview.1211 9.0.8 diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index b973b255d..64fbe93c4 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -69,8 +69,6 @@ using JetBrains.Annotations; using Medallion.Threading.FileSystem; using Medallion.Threading.Postgres; using Medallion.Threading.Redis; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; diff --git a/src/modules/Elsa.Logging/Activities/Log.cs b/src/modules/Elsa.Logging/Activities/Log.cs index 355dc863d..653fc8ff6 100644 --- a/src/modules/Elsa.Logging/Activities/Log.cs +++ b/src/modules/Elsa.Logging/Activities/Log.cs @@ -47,12 +47,22 @@ public class Log : CodeActivity /// [Input(Description = "The log message to emit.")] public Input Message { get; set; } = new(string.Empty); - + + /// + /// Arguments for the templated string message. + /// + [Input(Description = "Values of named or indexed placeholders in the log message.")] + public Input Arguments { get; set; } = null!; + /// /// Additional attributes to include in the log entry. /// - [Input(Description = "Values of named or indexed placeholders in the log message.")] - public Input Arguments { get; set; } = null!; + [Input( + Description = "Flat dictionary of key/value pairs to include as attributes.", + DisplayName = "Attributes", + UIHint = InputUIHints.Dictionary + )] + public Input> Attributes { get; set; } = null!; /// /// The log level. @@ -66,15 +76,6 @@ public class Log : CodeActivity [Input(Description = "The category. Defaults to 'Process'.", DefaultValue = "Process")] public Input Category { get; set; } = new("Process"); - /// - /// Additional attributes to include in the log entry. - /// - [Input( - Description = "Flat dictionary of key/value pairs to include as attributes.", - UIHint = InputUIHints.Dictionary - )] - public Input> Attributes { get; set; } = null!; - /// /// Target sinks to write to. /// @@ -98,8 +99,8 @@ public class Log : CodeActivity // 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 attributes = Attributes.GetOrDefault(context) ?? new Dictionary()!; var sinkNames = SinkNames.GetOrDefault(context) ?? new List(); var category = Category.GetOrDefault(context); if (string.IsNullOrWhiteSpace(category)) category = "Process"; diff --git a/src/modules/Elsa.Logging/Elsa.Logging.csproj b/src/modules/Elsa.Logging/Elsa.Logging.csproj index b4f0b319f..beb7ec790 100644 --- a/src/modules/Elsa.Logging/Elsa.Logging.csproj +++ b/src/modules/Elsa.Logging/Elsa.Logging.csproj @@ -1,4 +1,4 @@ - + @@ -8,9 +8,9 @@ - + - + \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs index 98335c6b0..982f409d0 100644 --- a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs +++ b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs @@ -78,6 +78,12 @@ public class InputAttribute : Attribute /// public bool AutoEvaluate { get; set; } = true; + /// + /// Specifies the type of a custom evaluator to use for evaluating the input property value. + /// The evaluator type determines how the value for the property is resolved at runtime. + /// + public Type? EvaluatorType { get; set; } + /// /// A value indicating whether this input can be serialized as part of the workflow instance, /// diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs new file mode 100644 index 000000000..99e1a4572 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs @@ -0,0 +1,12 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public record ActivityInputEvaluatorContext( + ActivityExecutionContext ActivityExecutionContext, + ExpressionExecutionContext ExpressionExecutionContext, + InputDescriptor InputDescriptor, + Input Input, + IExpressionEvaluator ExpressionEvaluator); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs new file mode 100644 index 000000000..ec94417a4 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows; + +public interface IActivityInputEvaluator +{ + Task EvaluateAsync(ActivityInputEvaluatorContext context); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index f47aa314d..db798e5cf 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -91,9 +91,16 @@ public static partial class ActivityExecutionContextExtensions } else { - var evaluator = context.GetRequiredService(); + var expressionEvaluator = context.GetRequiredService(); var expressionExecutionContext = context.ExpressionExecutionContext; - value = wrappedInput?.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue; + var inputEvaluatorType = inputDescriptor.EvaluatorType ?? typeof(DefaultActivityInputEvaluator); + + if (wrappedInput?.Expression != null) + { + var inputEvaluator = (IActivityInputEvaluator)context.GetRequiredService(inputEvaluatorType); + var inputEvaluatorContext = new ActivityInputEvaluatorContext(context, expressionExecutionContext, inputDescriptor, wrappedInput, expressionEvaluator); + value = await inputEvaluator.EvaluateAsync(inputEvaluatorContext); + } } var memoryReference = wrappedInput?.MemoryBlockReference(); diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index cc3f81aad..0608b8d92 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -22,6 +22,7 @@ using Elsa.Workflows.Serialization.Helpers; using Elsa.Workflows.Serialization.Serializers; using Elsa.Workflows.Services; using Elsa.Workflows.UIHints.CheckList; +using Elsa.Workflows.UIHints.Dictionary; using Elsa.Workflows.UIHints.Dropdown; using Elsa.Workflows.UIHints.JsonEditor; using Elsa.Workflows.UIHints.RadioList; @@ -187,6 +188,7 @@ public class WorkflowsFeature : FeatureBase .AddScoped() .AddScoped() .AddScoped() + .AddScoped() // Incident Strategies. .AddTransient() @@ -228,17 +230,17 @@ public class WorkflowsFeature : FeatureBase // Instantiation strategies. .AddScoped() - // UI hints. + // UI. .AddScoped() .AddScoped() .AddScoped() .AddScoped() - - // UI property handlers. .AddScoped() .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddSingleton() // Logger state generators. .AddSingleton(WorkflowLoggerStateGenerator) diff --git a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs index 8c7f24fe0..9d699e9f1 100644 --- a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs @@ -21,19 +21,20 @@ public class InputDescriptor : PropertyDescriptor bool isWrapped, string uiHint, string displayName, - string? description = default, - string? category = default, + string? description = null, + string? category = null, float order = 0, - object? defaultValue = default, + object? defaultValue = null, string? defaultSyntax = "Literal", bool isReadOnly = false, bool isBrowsable = true, bool isSerializable = true, bool isSynthetic = false, bool autoEvaluate = true, - Type? storageDriverType = default, - PropertyInfo? propertyInfo = default, - IDictionary? uiSpecifications = default + Type? evaluatorType = null, + Type? storageDriverType = null, + PropertyInfo? propertyInfo = null, + IDictionary? uiSpecifications = null ) { Name = name; @@ -50,6 +51,7 @@ public class InputDescriptor : PropertyDescriptor DefaultSyntax = defaultSyntax; IsReadOnly = isReadOnly; AutoEvaluate = autoEvaluate; + EvaluatorType = evaluatorType; StorageDriverType = storageDriverType; IsSynthetic = isSynthetic; IsBrowsable = isBrowsable; @@ -66,7 +68,7 @@ public class InputDescriptor : PropertyDescriptor /// /// A string value that hints at what UI control might be used to render in a UI tool. /// - public string UIHint { get; set; } = default!; + public string UIHint { get; set; } = null!; /// /// The category to which this input belongs. Can be used by UI to e.g. render different inputs in different tabs. @@ -104,6 +106,12 @@ public class InputDescriptor : PropertyDescriptor /// True if the expression should be evaluated automatically, false otherwise. Defaults to true. /// public bool AutoEvaluate { get; set; } = true; + + /// + /// Specifies the type of a custom evaluator to use for evaluating the input property value. + /// The evaluator type determines how the value for the property is resolved at runtime. + /// + public Type? EvaluatorType { get; set; } /// /// A dictionary of UI specifications to be used by the UI. diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 9533f4652..4810b915f 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -150,8 +150,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken); - return new InputDescriptor - ( + return new( inputAttribute?.Name ?? propertyInfo.Name, wrappedPropertyType, propertyInfo.GetValue, @@ -169,7 +168,8 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve inputAttribute?.IsSerializable ?? true, false, autoEvaluate, - default, + inputAttribute?.EvaluatorType, + null, propertyInfo, uiSpecification ); diff --git a/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs new file mode 100644 index 000000000..ca89d369f --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs @@ -0,0 +1,14 @@ +using Elsa.Extensions; + +namespace Elsa.Workflows; + +public class DefaultActivityInputEvaluator : IActivityInputEvaluator +{ + public async Task EvaluateAsync(ActivityInputEvaluatorContext context) + { + var wrappedInput = context.Input; + var evaluator = context.ExpressionEvaluator; + var expressionExecutionContext = context.ExpressionExecutionContext; + return await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs new file mode 100644 index 000000000..21e104d4b --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs @@ -0,0 +1,14 @@ +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.UIHints.Dictionary; + +public class DictionaryUIHintInputModifier : IActivityDescriptorModifier +{ + public void Modify(ActivityDescriptor descriptor) + { + var dictionaryInputs = descriptor.Inputs.Where(x => x.UIHint == InputUIHints.Dictionary).ToList(); + + foreach (var dictionaryInput in dictionaryInputs) + dictionaryInput.EvaluatorType = typeof(DictionaryValueEvaluator); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs new file mode 100644 index 000000000..0efcffb67 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Microsoft.Extensions.Logging; + +namespace Elsa.Workflows.UIHints.Dictionary; + +public class DictionaryValueEvaluator(ILogger logger) : IActivityInputEvaluator +{ + public async Task EvaluateAsync(ActivityInputEvaluatorContext context) + { + var wrappedInput = context.Input; + var evaluator = context.ExpressionEvaluator; + var expressionExecutionContext = context.ExpressionExecutionContext; + var inputDescriptor = context.InputDescriptor; + var defaultValue = inputDescriptor.DefaultValue; + var value = wrappedInput.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue; + if (value is IDictionary dictionary && inputDescriptor.UIHint == InputUIHints.Dictionary) + { + var tempDictionary = new Dictionary(dictionary.Count); + foreach (var dict in dictionary) + { + if (dict.Value is not JsonElement json) + { + // Not a JSON object, so just use the value as-is. + tempDictionary[dict.Key] = dict.Value; + continue; + } + + // JSON object, so extract the type and value properties. + var hasType = json.TryGetProperty("type", out var typeProperty); + var hasValue = json.TryGetProperty("value", out var valueProperty); + + if (!hasType || !hasValue) + { + // Skip this entry or handle as needed (e.g., log, throw, etc.) + logger.LogWarning("Dictionary entry is missing type or value property: {Json}", JsonSerializer.Serialize(json)); + continue; + } + + // Evaluate the expression. + var expression = new Expression(typeProperty.ToString(), valueProperty.ToString()); + var val = await evaluator.EvaluateAsync(expression, expressionExecutionContext); + + // Add the evaluated value to the dictionary. + tempDictionary[dict.Key] = val; + } + + // Replace the original dictionary with the evaluated one. + value = tempDictionary; + } + + return value; + } +} \ No newline at end of file