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 <sipkeschoorstra@outlook.com>
Co-authored-by: lukhipolito-nexxbiz <lucas.hipolito@nexxbiz.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lucas Hipólito 2025-09-04 10:57:48 +02:00 committed by GitHub
parent 17bb1d08c9
commit 90b4e7d6c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 158 additions and 35 deletions

View file

@ -4,7 +4,7 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<PropertyGroup>
<ElsaStudioVersion>3.5.0</ElsaStudioVersion>
<ElsaStudioVersion>3.5.1-preview.1211</ElsaStudioVersion>
<MicrosoftVersion>9.0.8</MicrosoftVersion>
</PropertyGroup>
<ItemGroup>

View file

@ -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;

View file

@ -47,12 +47,22 @@ public class Log : CodeActivity
/// </summary>
[Input(Description = "The log message to emit.")]
public Input<string> Message { get; set; } = new(string.Empty);
/// <summary>
/// Arguments for the templated string message.
/// </summary>
[Input(Description = "Values of named or indexed placeholders in the log message.")]
public Input<object?> Arguments { get; set; } = null!;
/// <summary>
/// Additional attributes to include in the log entry.
/// </summary>
[Input(Description = "Values of named or indexed placeholders in the log message.")]
public Input<object?> Arguments { get; set; } = null!;
[Input(
Description = "Flat dictionary of key/value pairs to include as attributes.",
DisplayName = "Attributes",
UIHint = InputUIHints.Dictionary
)]
public Input<IDictionary<string, object?>> Attributes { get; set; } = null!;
/// <summary>
/// The log level.
@ -66,15 +76,6 @@ public class Log : CodeActivity
[Input(Description = "The category. Defaults to 'Process'.", DefaultValue = "Process")]
public Input<string> Category { get; set; } = new("Process");
/// <summary>
/// Additional attributes to include in the log entry.
/// </summary>
[Input(
Description = "Flat dictionary of key/value pairs to include as attributes.",
UIHint = InputUIHints.Dictionary
)]
public Input<IDictionary<string, object?>> Attributes { get; set; } = null!;
/// <summary>
/// Target sinks to write to.
/// </summary>
@ -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<string, object?>();
var attributes = Attributes.GetOrDefault(context) ?? new Dictionary<string, object?>()!;
var sinkNames = SinkNames.GetOrDefault(context) ?? new List<string>();
var category = Category.GetOrDefault(context);
if (string.IsNullOrWhiteSpace(category)) category = "Process";

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Logging.Core\Elsa.Logging.Core.csproj" />
<ProjectReference Include="..\Elsa.Logging.Core\Elsa.Logging.Core.csproj"/>
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj"/>
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj"/>
</ItemGroup>
</Project>
</Project>

View file

@ -78,6 +78,12 @@ public class InputAttribute : Attribute
/// </summary>
public bool AutoEvaluate { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public Type? EvaluatorType { get; set; }
/// <summary>
/// A value indicating whether this input can be serialized as part of the workflow instance,
/// </summary>

View file

@ -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);

View file

@ -0,0 +1,6 @@
namespace Elsa.Workflows;
public interface IActivityInputEvaluator
{
Task<object?> EvaluateAsync(ActivityInputEvaluatorContext context);
}

View file

@ -91,9 +91,16 @@ public static partial class ActivityExecutionContextExtensions
}
else
{
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionEvaluator = context.GetRequiredService<IExpressionEvaluator>();
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();

View file

@ -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<IActivityStateFilterManager, DefaultActivityStateFilterManager>()
.AddScoped<IWorkflowInstanceVariableReader, DefaultWorkflowInstanceVariableReader>()
.AddScoped<IWorkflowInstanceVariableWriter, DefaultWorkflowInstanceVariableWriter>()
.AddScoped<DefaultActivityInputEvaluator>()
// Incident Strategies.
.AddTransient<IIncidentStrategy, FaultStrategy>()
@ -228,17 +230,17 @@ public class WorkflowsFeature : FeatureBase
// Instantiation strategies.
.AddScoped<IWorkflowActivationStrategy, AllowAlwaysStrategy>()
// UI hints.
// UI.
.AddScoped<IUIHintHandler, DropDownUIHintHandler>()
.AddScoped<IUIHintHandler, CheckListUIHintHandler>()
.AddScoped<IUIHintHandler, RadioListUIHintHandler>()
.AddScoped<IUIHintHandler, JsonEditorUIHintHandler>()
// UI property handlers.
.AddScoped<IPropertyUIHandler, StaticCheckListOptionsProvider>()
.AddScoped<IPropertyUIHandler, StaticRadioListOptionsProvider>()
.AddScoped<IPropertyUIHandler, StaticDropDownOptionsProvider>()
.AddScoped<IPropertyUIHandler, JsonCodeOptionsProvider>()
.AddScoped<DictionaryValueEvaluator>()
.AddSingleton<IActivityDescriptorModifier, DictionaryUIHintInputModifier>()
// Logger state generators.
.AddSingleton(WorkflowLoggerStateGenerator)

View file

@ -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<string, object>? uiSpecifications = default
Type? evaluatorType = null,
Type? storageDriverType = null,
PropertyInfo? propertyInfo = null,
IDictionary<string, object>? 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
/// <summary>
/// A string value that hints at what UI control might be used to render in a UI tool.
/// </summary>
public string UIHint { get; set; } = default!;
public string UIHint { get; set; } = null!;
/// <summary>
/// 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.
/// </summary>
public bool AutoEvaluate { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public Type? EvaluatorType { get; set; }
/// <summary>
/// A dictionary of UI specifications to be used by the UI.

View file

@ -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
);

View file

@ -0,0 +1,14 @@
using Elsa.Extensions;
namespace Elsa.Workflows;
public class DefaultActivityInputEvaluator : IActivityInputEvaluator
{
public async Task<object?> EvaluateAsync(ActivityInputEvaluatorContext context)
{
var wrappedInput = context.Input;
var evaluator = context.ExpressionEvaluator;
var expressionExecutionContext = context.ExpressionExecutionContext;
return await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext);
}
}

View file

@ -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);
}
}

View file

@ -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<DictionaryValueEvaluator> logger) : IActivityInputEvaluator
{
public async Task<object?> 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<string, object> dictionary && inputDescriptor.UIHint == InputUIHints.Dictionary)
{
var tempDictionary = new Dictionary<string, object?>(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<object>(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;
}
}