diff --git a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs index d80f308b7..32589960d 100644 --- a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs +++ b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs @@ -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) { + /// + /// When active this will create a new root Activity if the current Activity has a remote parent. + /// + public bool UseNewRootActivityForRemoteParent { get; set; } = true; + + /// + /// Determines if instead of a empty parent, a dummy parent Activity should be used to create a new Root Activity. + /// + /// This is needed when middleware is active that uses the previous Parent Activity despite creating an Activity based on an empty Parent. + public bool UseDummyParentActivityAsRootSpan { get; set; } = false; + public override void Configure() { Services .AddScoped() .AddScoped(); + + Services.Configure(options => + { + options.UseNewRootActivityForRemoteParent = UseNewRootActivityForRemoteParent; + options.UseDummyParentActivityAsRootSpan = UseDummyParentActivityAsRootSpan; + }); } } \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs index 837226655..cbb1c583f 100644 --- a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs +++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs @@ -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. /// [UsedImplicitly] -public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock) : WorkflowExecutionMiddleware(next) +public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, IOptions options) : WorkflowExecutionMiddleware(next) { /// 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 diff --git a/src/modules/Elsa.OpenTelemetry/Options/OpenTelemetryOptions.cs b/src/modules/Elsa.OpenTelemetry/Options/OpenTelemetryOptions.cs new file mode 100644 index 000000000..4a52ea383 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Options/OpenTelemetryOptions.cs @@ -0,0 +1,7 @@ +namespace Elsa.OpenTelemetry.Options; + +public class OpenTelemetryOptions +{ + public bool UseNewRootActivityForRemoteParent { get; set; } + public bool UseDummyParentActivityAsRootSpan { get; set; } +} \ No newline at end of file