Start a new workflow trace when triggered from UI

This commit is contained in:
Raymond den Haan 2025-04-22 11:46:16 +02:00
parent c8ed08cc85
commit 28a18d1a86
3 changed files with 64 additions and 25 deletions

View file

@ -2,16 +2,34 @@ using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Handlers;
using Elsa.OpenTelemetry.Options;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.OpenTelemetry.Features;
public class OpenTelemetryFeature(IModule module) : FeatureBase(module)
{
/// <summary>
/// When active this will create a new root Activity if the current Activity has a remote parent.
/// </summary>
public bool UseNewRootActivityForRemoteParent { get; set; } = true;
/// <summary>
/// Determines if instead of a empty parent, a dummy parent Activity should be used to create a new Root Activity.
/// </summary>
/// <remarks>This is needed when middleware is active that uses the previous Parent Activity despite creating an Activity based on an empty Parent.</remarks>
public bool UseDummyParentActivityAsRootSpan { get; set; } = false;
public override void Configure()
{
Services
.AddScoped<IErrorSpanHandler, DefaultErrorSpanHandler>()
.AddScoped<IErrorSpanHandler, FaultExceptionErrorSpanHandler>();
Services.Configure<OpenTelemetryOptions>(options =>
{
options.UseNewRootActivityForRemoteParent = UseNewRootActivityForRemoteParent;
options.UseDummyParentActivityAsRootSpan = UseDummyParentActivityAsRootSpan;
});
}
}

View file

@ -1,16 +1,12 @@
using System.Diagnostics;
using System.Runtime.InteropServices.Marshalling;
using System.Text.Json;
using Elsa.Common;
using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.OpenTelemetry.Helpers;
using Elsa.OpenTelemetry.Options;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Models;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
using Activity = System.Diagnostics.Activity;
using ActivityKind = System.Diagnostics.ActivityKind;
@ -20,37 +16,23 @@ namespace Elsa.OpenTelemetry.Middleware;
/// Middleware that traces workflow execution using OpenTelemetry.
/// </summary>
[UsedImplicitly]
public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock) : WorkflowExecutionMiddleware(next)
public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, IOptions<OpenTelemetryOptions> options) : WorkflowExecutionMiddleware(next)
{
/// <inheritdoc />
public override async ValueTask InvokeAsync(WorkflowExecutionContext context)
{
var workflowName = context.Workflow.WorkflowMetadata.Name;
var workflowInstanceId = context.Id;
var workflow = context.Workflow;
var startNewTrace = context.Properties.TryGetValue("StartNewTrace", out var startNewTraceValue) && (bool)startNewTraceValue;
var parentTraceContext = startNewTrace ? default : Activity.Current?.Context ?? default;
var linkedTraceContext = startNewTrace ? Activity.Current : null;
if(startNewTrace)
{
Activity.Current?.Stop();
Activity.Current = null;
}
using var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"execute workflow {workflow.WorkflowMetadata.Name}", ActivityKind.Server, parentTraceContext);
if (span == null) // No listener is registered.
using var span = CreateTraceActivity(context, workflowName);
if (span == null)
{
await Next(context);
return;
}
if(startNewTrace)
{
if (linkedTraceContext != null)
span.AddLink(new(linkedTraceContext.Context));
}
span.SetTag("operation.name", "elsa.workflow.execution");
span.SetTag("span.type", "workflow");
span.SetTag("workflow.definition.id", workflow.Identity.DefinitionId);
@ -101,6 +83,38 @@ public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareD
span.SetTag("workflow.correlation_id", context.CorrelationId);
}
private Activity? CreateTraceActivity(WorkflowExecutionContext context, string? workflowName)
{
var startNewTraceOptionValue = context.Properties.TryGetValue("StartNewTrace", out var startNewTraceValue) && (bool)startNewTraceValue;
var startNewTraceForRemoteParent = options.Value.UseNewRootActivityForRemoteParent && Activity.Current?.HasRemoteParent == true;
var startNewTrace = startNewTraceOptionValue || startNewTraceForRemoteParent;
ActivityContext contextToUse;
ActivityContext? linkedTraceContext = null;
if (startNewTrace)
{
linkedTraceContext = Activity.Current?.Context;
Activity.Current?.Stop();
Activity.Current = null;
contextToUse = options.Value.UseDummyParentActivityAsRootSpan
? new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)
: default;
}
else
{
contextToUse = Activity.Current?.Context ?? default;
}
var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"execute workflow {workflowName}", ActivityKind.Server, contextToUse);
if (span != null && linkedTraceContext != null)
span.AddLink(new (linkedTraceContext.Value));
return span;
}
private ActivityTagsCollection CreateIncidentTags(ActivityIncident incident)
{
var tags = new ActivityTagsCollection(new Dictionary<string, object?>

View file

@ -0,0 +1,7 @@
namespace Elsa.OpenTelemetry.Options;
public class OpenTelemetryOptions
{
public bool UseNewRootActivityForRemoteParent { get; set; }
public bool UseDummyParentActivityAsRootSpan { get; set; }
}