Add OpenTelemetry support for tracing workflow execution

This commit introduces the Elsa.OpenTelemetry module to provide OpenTelemetry sources for tracing workflow and activity execution. Additionally, it updates the program to include default workflow and activity execution pipelines with tracing middleware, and improves pipeline builder extensions to support middleware insertion.
This commit is contained in:
Sipke Schoorstra 2024-09-23 11:35:43 +02:00
parent 53172cded9
commit 79b5bfc378
21 changed files with 238 additions and 35 deletions

View file

@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- patch/3.2.x
- blueberry
jobs:
push_to_registry:
@ -29,7 +29,7 @@ jobs:
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-server-and-studio-v3-2-1-preview
elsaworkflows/elsa-server-and-studio-v3-2-1-blueberry
flavor: |
latest=true
# generate Docker tags based on the following events/attributes

View file

@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- patch/3.2.x
- blueberry
jobs:
push_to_registry:
@ -29,7 +29,7 @@ jobs:
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-server-v3-2-1-preview
elsaworkflows/elsa-server-v3-2-1-blueberry
flavor: |
latest=true
# generate Docker tags based on the following events/attributes

View file

@ -3,7 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- patch/3.2.x
- blueberry
jobs:
push_to_registry:
@ -29,7 +29,7 @@ jobs:
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-studio-v3-2-1-preview
elsaworkflows/elsa-studio-v3-2-1-blueberry
flavor: |
latest=true
# generate Docker tags based on the following events/attributes

View file

@ -3,18 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- 'main'
- 'feature/*'
- 'feat/*'
- 'issue/*'
- 'bug/*'
- 'enhancement/*'
- 'enh/*'
- 'patch/*'
- 'fix/*'
- 'perf/*'
- 'hotfix/*'
- 'chore/*'
- 'blueberry'
release:
types: [ prereleased, published ]
env:
@ -62,7 +51,7 @@ jobs:
TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix
echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV
else
echo "VERSION=3.2.1-preview.${{github.run_number}}" >> $GITHUB_ENV
echo "VERSION=3.2.1-blueberry.${{github.run_number}}" >> $GITHUB_ENV
fi
- name: Set up JDK 17
uses: actions/setup-java@v2

View file

@ -355,6 +355,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.PerformanceT
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MongoDb.UnitTests", "test\unit\Elsa.MongoDb.UnitTests\Elsa.MongoDb.UnitTests.csproj", "{56CAA9F2-1882-4EFA-BAC0-9C3D804553F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.OpenTelemetry", "src\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj", "{25880971-403E-4872-93A1-D33089E07C91}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -844,6 +846,10 @@ Global
{56CAA9F2-1882-4EFA-BAC0-9C3D804553F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56CAA9F2-1882-4EFA-BAC0-9C3D804553F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56CAA9F2-1882-4EFA-BAC0-9C3D804553F1}.Release|Any CPU.Build.0 = Release|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -991,6 +997,7 @@ Global
{CBB515F3-A0EF-43B5-A907-FD4E652DD66E} = {90031D64-CA0F-46D0-9AF4-8DC023A5FFCD}
{90CD37A9-C866-4D90-A3B1-8C87F53B845E} = {CBB515F3-A0EF-43B5-A907-FD4E652DD66E}
{56CAA9F2-1882-4EFA-BAC0-9C3D804553F1} = {18453B51-25EB-4317-A4B3-B10518252E92}
{25880971-403E-4872-93A1-D33089E07C91} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}

View file

@ -12,6 +12,7 @@
<ProjectReference Include="..\..\modules\Elsa.Caching.Distributed.MassTransit\Elsa.Caching.Distributed.MassTransit.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj" />
<ProjectReference Include="..\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\common\Elsa.DropIns\Elsa.DropIns.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj"/>

View file

@ -20,6 +20,7 @@ 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.Workflows;
using Elsa.Workflows.Management.Compression;
@ -131,6 +132,11 @@ services
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflows(workflows =>
{
workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing());
workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing());
})
.UseWorkflowManagement(management =>
{
if (useMongoDb)

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides OpenTelemetry sources for tracing workflow and activity execution.
</Description>
<PackageTags>elsa module open-telemetry</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -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()
{
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,8 @@
using System.Diagnostics;
namespace Elsa.OpenTelemetry.Helpers;
public class ElsaOpenTelemetry
{
public static readonly ActivitySource ActivitySource = new("Elsa.Workflows");
}

View file

@ -0,0 +1,42 @@
using System.Diagnostics;
using Elsa.OpenTelemetry.Helpers;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
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;
/// <inheritdoc />
[UsedImplicitly]
public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
{
/// <inheritdoc />
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());
}
}
/// <summary>
/// Contains extension methods for <see cref="OpenTelemetryTracingActivityExecutionMiddleware"/>.
/// </summary>
[UsedImplicitly]
public static class OpenTelemetryTracingActivityExecutionMiddlewareExtensions
{
/// Installs the <see cref="OpenTelemetryTracingActivityExecutionMiddleware"/> component in the workflow execution pipeline.
public static IActivityExecutionPipelineBuilder UseActivityExecutionTracing(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert<OpenTelemetryTracingActivityExecutionMiddleware>(0);
}

View file

@ -0,0 +1,49 @@
using System.Diagnostics;
using Elsa.OpenTelemetry.Helpers;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using JetBrains.Annotations;
using Activity = System.Diagnostics.Activity;
using ActivityKind = System.Diagnostics.ActivityKind;
namespace Elsa.OpenTelemetry.Middleware;
/// <summary>
/// Middleware that traces workflow execution using OpenTelemetry.
/// </summary>
[UsedImplicitly]
public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next)
{
/// <inheritdoc />
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);
if(!string.IsNullOrWhiteSpace(context.CorrelationId))
activity?.AddTag("correlationId", context.CorrelationId);
activity?.AddTag("workflowInstance.id", workflowInstanceId);
activity?.AddTag("workflowDefinition.definitionId", workflow.Identity.DefinitionId);
activity?.AddTag("workflowDefinition.version", workflow.Identity.Version);
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());
}
}
/// <summary>
/// Contains extension methods for <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/>.
/// </summary>
[UsedImplicitly]
public static class OpenTelemetryWorkflowExecutionMiddlewareExtensions
{
/// Installs the <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/> component in the workflow execution pipeline.
public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionTracing(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert<OpenTelemetryTracingWorkflowExecutionMiddleware>(0);
}

View file

@ -19,6 +19,9 @@ public interface IActivityExecutionPipelineBuilder
/// <returns>The current <see cref="IActivityExecutionPipelineBuilder"/>.</returns>
IActivityExecutionPipelineBuilder Use(Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware);
/// Inserts the middleware component at the specified index.
IActivityExecutionPipelineBuilder Insert(int index, Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware);
/// <summary>
/// Constructs the final <see cref="ActivityMiddlewareDelegate"/> delegate that invokes each installed middleware component.
/// </summary>

View file

@ -36,6 +36,9 @@ public interface IWorkflowExecutionPipelineBuilder
/// Clears the current pipeline.
/// </summary>
IWorkflowExecutionPipelineBuilder Reset();
/// Inserts the middleware component at the specified index.
IWorkflowExecutionPipelineBuilder Insert(int index, Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware);
/// <summary>
/// Replaces the middleware component at the specified index with the specified delegate.

View file

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Elsa.Workflows.Contracts;
using Microsoft.Extensions.DependencyInjection;
@ -5,16 +6,33 @@ namespace Elsa.Workflows.Pipelines.ActivityExecution;
public static class ActivityExecutionMiddlewareExtensions
{
public static IActivityExecutionPipelineBuilder UseMiddleware<TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware
public static IActivityExecutionPipelineBuilder UseMiddleware<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware
{
var delegateFactory = CreateMiddlewareDelegateFactory<TMiddleware>(pipelineBuilder, args);
return pipelineBuilder.Use(delegateFactory);
}
public static IActivityExecutionPipelineBuilder Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IActivityExecutionMiddleware
{
var delegateFactory = CreateMiddlewareDelegateFactory<TMiddleware>(pipelineBuilder, args);
return pipelineBuilder.Insert(index, delegateFactory);
}
/// Creates a middleware delegate for the specified middleware component.
public static Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> CreateMiddlewareDelegateFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware
{
var middleware = typeof(TMiddleware);
return pipelineBuilder.Use(next =>
return next =>
{
var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware);
var ctorArgs = new[] { next }.Concat(args).Select(x => x!).ToArray();
var ctorArgs = new[]
{
next
}.Concat(args).Select(x => x).ToArray();
var instance = ActivatorUtilities.CreateInstance(pipelineBuilder.ServiceProvider, middleware, ctorArgs);
return (ActivityMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(ActivityMiddlewareDelegate), instance);
});
};
}
}

View file

@ -25,6 +25,13 @@ public class ActivityExecutionPipelinePipelineBuilder : IActivityExecutionPipeli
return this;
}
/// <inheritdoc />
public IActivityExecutionPipelineBuilder Insert(int index, Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware)
{
_components.Insert(index, middleware);
return this;
}
/// <inheritdoc />
public ActivityMiddlewareDelegate Build()
{

View file

@ -4,44 +4,42 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Pipelines.WorkflowExecution;
/// <summary>
/// Provides extensions to <see cref="IWorkflowExecutionPipelineBuilder"/> that adds support for installing <see cref="IWorkflowExecutionMiddleware"/> components.
/// </summary>
public static class WorkflowExecutionMiddlewareExtensions
{
/// <summary>
/// Installs the specified middleware component into the pipeline being built.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseMiddleware<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware
{
var delegateFactory = CreateMiddlewareDelegateFactory<TMiddleware>(pipelineBuilder, args);
return pipelineBuilder.Use(delegateFactory);
}
/// Installs the specified middleware component into the pipeline being built.
public static IWorkflowExecutionPipelineBuilder Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IWorkflowExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware
{
var delegateFactory = CreateMiddlewareDelegateFactory<TMiddleware>(pipelineBuilder, args);
return pipelineBuilder.Insert(index, delegateFactory);
}
/// <summary>
/// Replaces the terminal middleware component with the specified middleware component.
/// </summary>
public static IWorkflowExecutionPipelineBuilder ReplaceTerminal<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware
{
var index = pipelineBuilder.Components.Count() - 1;
return pipelineBuilder.Replace<TMiddleware>(index, args);
}
/// <summary>
/// Replaces the middleware component at the specified index with the specified middleware component.
/// </summary>
public static IWorkflowExecutionPipelineBuilder Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IWorkflowExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware
{
var delegateFactory = CreateMiddlewareDelegateFactory<TMiddleware>(pipelineBuilder, args);
return pipelineBuilder.Replace(index, delegateFactory);
}
/// <summary>
/// Creates a middleware delegate for the specified middleware component.
/// </summary>
public static Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> CreateMiddlewareDelegateFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(
this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware
{

View file

@ -54,6 +54,14 @@ public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionPipelineBuilde
return this;
}
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Insert(int index, Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
{
_components.Insert(index, middleware);
return this;
}
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Replace(int index, Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
{
_components[index] = middleware;

View file

@ -0,0 +1,35 @@
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Features;
using Elsa.Workflows.Middleware.Activities;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
public static class PipelineWorkflowsFeatureExtensions
{
/// Installs a default workflow runtime execution pipeline.
public static WorkflowsFeature WithDefaultWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature, Action<IWorkflowExecutionPipelineBuilder>? configurePipeline = null)
{
return workflowsFeature.WithWorkflowExecutionPipeline(pipeline =>
{
pipeline.UseDefaultPipeline();
configurePipeline?.Invoke(pipeline);
});
}
/// Installs an activity invoker that can run activities asynchronously in the background.
public static WorkflowsFeature WithDefaultActivityExecutionPipeline(this WorkflowsFeature workflowsFeature, Action<IActivityExecutionPipelineBuilder>? configurePipeline = null)
{
return workflowsFeature.WithActivityExecutionPipeline(pipeline =>
{
pipeline
.UseExceptionHandling()
.UseExecutionLogging()
.UseNotifications()
.UseBackgroundActivityInvoker();
configurePipeline?.Invoke(pipeline);
});
}
}

View file

@ -6,6 +6,7 @@ using Elsa.Workflows.Middleware.Activities;
// ReSharper disable once CheckNamespace
using Elsa.Workflows.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>