diff --git a/Elsa.sln b/Elsa.sln
index de7cd82fb..239e86f9b 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -373,6 +373,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.ServerAndStudio.Web",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\apps\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{169A82A5-2DB3-40EA-801E-14C08D743DF7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.OpenTelemetry", "src\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj", "{2CDF3E1C-267D-4198-B1C7-7E1F548FC120}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -885,6 +887,10 @@ Global
{169A82A5-2DB3-40EA-801E-14C08D743DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{169A82A5-2DB3-40EA-801E-14C08D743DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{169A82A5-2DB3-40EA-801E-14C08D743DF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1039,6 +1045,7 @@ Global
{690B0274-291F-4D9E-BA76-54EFF7D3E4BC} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1}
{060FD0BA-BD78-48E1-A8A7-4906A5AD5E39} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1}
{169A82A5-2DB3-40EA-801E-14C08D743DF7} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1}
+ {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/docker/ElsaServer-Datadog.Dockerfile b/docker/ElsaServer-Datadog.Dockerfile
index 3af4d992a..207e69e5d 100644
--- a/docker/ElsaServer-Datadog.Dockerfile
+++ b/docker/ElsaServer-Datadog.Dockerfile
@@ -36,7 +36,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
rm -rf /var/lib/apt/lists/*
# Set PYTHONNET_PYDLL environment variable
-ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so
+ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
# Copy the tracer from build target
COPY --from=build /tmp/datadog-dotnet-apm.deb /tmp/datadog-dotnet-apm.deb
diff --git a/docker/ElsaServer.Dockerfile b/docker/ElsaServer.Dockerfile
index 5c6ca57c1..219a46a74 100644
--- a/docker/ElsaServer.Dockerfile
+++ b/docker/ElsaServer.Dockerfile
@@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
rm -rf /var/lib/apt/lists/*
# Set PYTHONNET_PYDLL environment variable
-ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so
+ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
EXPOSE 80/tcp
EXPOSE 443/tcp
diff --git a/docker/ElsaServerAndStudio.Dockerfile b/docker/ElsaServerAndStudio.Dockerfile
index bcfdcba90..2de69c82e 100644
--- a/docker/ElsaServerAndStudio.Dockerfile
+++ b/docker/ElsaServerAndStudio.Dockerfile
@@ -29,7 +29,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
rm -rf /var/lib/apt/lists/*
# Set PYTHONNET_PYDLL environment variable
-ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so
+ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
EXPOSE 80/tcp
EXPOSE 443/tcp
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index 7622f41c1..1e3e878f0 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -5,6 +5,7 @@
+
diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index 3ea373136..c77607f0e 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -21,13 +21,17 @@ using Elsa.MongoDb.Modules.Alterations;
using Elsa.MongoDb.Modules.Identity;
using Elsa.MongoDb.Modules.Management;
using Elsa.MongoDb.Modules.Runtime;
+using Elsa.OpenTelemetry.Middleware;
using Elsa.Server.Web;
using Elsa.Tenants.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Api;
using Elsa.Workflows.Management.Compression;
using Elsa.Workflows.Management.Stores;
+using Elsa.Workflows.Pipelines.ActivityExecution;
+using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.Runtime.Distributed.Extensions;
+using Elsa.Workflows.Runtime.Extensions;
using Elsa.Workflows.Runtime.Stores;
using JetBrains.Annotations;
using Medallion.Threading.FileSystem;
@@ -133,6 +137,11 @@ services
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
+ .UseWorkflows(workflows =>
+ {
+ workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing());
+ workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing());
+ })
.UseWorkflowManagement(management =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
diff --git a/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj
new file mode 100644
index 000000000..8660d883e
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+ Provides OpenTelemetry sources for tracing workflow and activity execution.
+
+ elsa module open-telemetry
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs
new file mode 100644
index 000000000..67201c44d
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs
@@ -0,0 +1,11 @@
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+
+namespace Elsa.OpenTelemetry.Features;
+
+public class OpenTelemetryFeature(IModule module) : FeatureBase(module)
+{
+ public override void Configure()
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml b/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml
new file mode 100644
index 000000000..00e1d9a1c
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs b/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs
new file mode 100644
index 000000000..302659456
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs
@@ -0,0 +1,8 @@
+using System.Diagnostics;
+
+namespace Elsa.OpenTelemetry.Helpers;
+
+public class ElsaOpenTelemetry
+{
+ public static readonly ActivitySource ActivitySource = new("Elsa.Workflows");
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs
new file mode 100644
index 000000000..4c8e1e1aa
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics;
+using Elsa.OpenTelemetry.Helpers;
+using Elsa.Workflows;
+using Elsa.Workflows.Pipelines.ActivityExecution;
+using Elsa.Workflows.Pipelines.WorkflowExecution;
+using JetBrains.Annotations;
+using Activity = System.Diagnostics.Activity;
+using ActivityKind = System.Diagnostics.ActivityKind;
+
+namespace Elsa.OpenTelemetry.Middleware;
+
+[UsedImplicitly]
+public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
+{
+ public async ValueTask InvokeAsync(ActivityExecutionContext context)
+ {
+ var activity = context.Activity;
+ using var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"ActivityExecution {context.ActivityDescriptor.TypeName}", ActivityKind.Internal, Activity.Current?.Context ?? default);
+ span?.AddTag("activity.nodeId", activity.NodeId);
+ span?.AddTag("activity.type", activity.Type);
+ span?.AddTag("activity.name", activity.Name);
+ span?.AddTag("activityInstance.id", context.Id);
+ span?.AddTag("activityInstance.originalStatus", context.Status.ToString());
+ span?.AddEvent(new ActivityEvent("Executing"));
+ await next(context);
+ span?.AddEvent(new ActivityEvent("Executed"));
+ span?.AddTag("activityInstance.newStatus", context.Status.ToString());
+ }
+}
+
+[UsedImplicitly]
+public static class OpenTelemetryTracingActivityExecutionMiddlewareExtensions
+{
+ /// Installs the component in the workflow execution pipeline.
+ public static IActivityExecutionPipelineBuilder UseActivityExecutionTracing(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert(0);
+}
diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs
new file mode 100644
index 000000000..4876aded3
--- /dev/null
+++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics;
+using Elsa.OpenTelemetry.Helpers;
+using Elsa.Workflows;
+using Elsa.Workflows.Pipelines.WorkflowExecution;
+using JetBrains.Annotations;
+using Activity = System.Diagnostics.Activity;
+using ActivityKind = System.Diagnostics.ActivityKind;
+
+namespace Elsa.OpenTelemetry.Middleware;
+
+[UsedImplicitly]
+public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next)
+{
+ public override async ValueTask InvokeAsync(WorkflowExecutionContext context)
+ {
+ var workflowInstanceId = context.Id;
+ var workflow = context.Workflow;
+ using var activity = ElsaOpenTelemetry.ActivitySource.StartActivity($"WorkflowExecution {workflow.WorkflowMetadata.Name}", ActivityKind.Internal, Activity.Current?.Context ?? default);
+ activity?.AddTag("workflowInstance.id", workflowInstanceId);
+ activity?.AddTag("workflowDefinition.definitionId", workflow.Identity.DefinitionId);
+ activity?.AddTag("workflowDefinition.version", workflow.Identity.Version);
+ activity?.AddTag("tenantId", workflow.Identity.TenantId);
+ activity?.AddTag("workflowInstance.originalStatus", context.Status.ToString());
+ activity?.AddTag("workflowInstance.originalSubStatus", context.SubStatus.ToString());
+ activity?.AddEvent(new ActivityEvent("Executing"));
+ await Next(context);
+ activity?.AddEvent(new ActivityEvent("Executed"));
+ activity?.AddTag("workflowInstance.newStatus", context.Status.ToString());
+ activity?.AddTag("workflowInstance.newSubStatus", context.SubStatus.ToString());
+ }
+}
+
+[UsedImplicitly]
+public static class OpenTelemetryWorkflowExecutionMiddlewareExtensions
+{
+ /// Installs the component in the workflow execution pipeline.
+ public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionTracing(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert(0);
+}
diff --git a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs
index 7f78ccd3b..03fc7c966 100644
--- a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs
+++ b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs
@@ -1,4 +1,5 @@
using Elsa.WorkflowContexts.Middleware;
+using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Elsa.Workflows.Pipelines.WorkflowExecution;
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
index 6094ed7e5..bb786f283 100644
--- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs
@@ -1,4 +1,4 @@
-namespace Elsa.Workflows.Contracts;
+namespace Elsa.Workflows;
public interface IActivityExecutionMiddleware
{
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs
index 7c7968451..3d60e9f68 100644
--- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs
@@ -1,6 +1,6 @@
using Elsa.Workflows.Pipelines.ActivityExecution;
-namespace Elsa.Workflows.Contracts;
+namespace Elsa.Workflows;
///
/// Builds an activity execution pipeline.
@@ -19,6 +19,9 @@ public interface IActivityExecutionPipelineBuilder
/// The current .
IActivityExecutionPipelineBuilder Use(Func middleware);
+ /// Inserts the middleware component at the specified index.
+ IActivityExecutionPipelineBuilder Insert(int index, Func middleware);
+
///
/// Constructs the final delegate that invokes each installed middleware component.
///
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs
index a2f37c9b0..f0de095d9 100644
--- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs
@@ -1,41 +1,30 @@
using Elsa.Workflows.Pipelines.WorkflowExecution;
-namespace Elsa.Workflows.Contracts;
+namespace Elsa.Workflows;
-///
/// Builds a workflow execution pipeline.
-///
public interface IWorkflowExecutionPipelineBuilder
{
- ///
/// A general-purpose dictionary of values that can be used by middleware components.
- ///
public IDictionary