2024-09-23 09:35:43 +00:00
using System.Diagnostics ;
2024-10-11 18:41:49 +00:00
using Elsa.Common ;
2025-05-07 08:59:44 +00:00
using Elsa.OpenTelemetry.Contracts ;
2024-09-23 09:35:43 +00:00
using Elsa.OpenTelemetry.Helpers ;
2025-05-07 08:59:44 +00:00
using Elsa.OpenTelemetry.Models ;
2025-04-22 09:46:16 +00:00
using Elsa.OpenTelemetry.Options ;
2024-09-23 09:35:43 +00:00
using Elsa.Workflows ;
2025-03-14 22:17:54 +00:00
using Elsa.Workflows.Models ;
2024-09-23 09:35:43 +00:00
using Elsa.Workflows.Pipelines.WorkflowExecution ;
using JetBrains.Annotations ;
2025-04-22 09:46:16 +00:00
using Microsoft.Extensions.Options ;
2024-09-23 09:35:43 +00:00
using Activity = System . Diagnostics . Activity ;
using ActivityKind = System . Diagnostics . ActivityKind ;
namespace Elsa.OpenTelemetry.Middleware ;
/// <summary>
/// Middleware that traces workflow execution using OpenTelemetry.
/// </summary>
[UsedImplicitly]
2025-04-22 09:46:16 +00:00
public class OpenTelemetryTracingWorkflowExecutionMiddleware ( WorkflowMiddlewareDelegate next , ISystemClock systemClock , IOptions < OpenTelemetryOptions > options ) : WorkflowExecutionMiddleware ( next )
2024-09-23 09:35:43 +00:00
{
/// <inheritdoc />
public override async ValueTask InvokeAsync ( WorkflowExecutionContext context )
{
2025-04-22 09:46:16 +00:00
var workflowName = context . Workflow . WorkflowMetadata . Name ;
2024-09-23 09:35:43 +00:00
var workflowInstanceId = context . Id ;
var workflow = context . Workflow ;
2024-09-23 21:35:30 +00:00
2025-04-22 09:46:16 +00:00
using var span = CreateTraceActivity ( context , workflowName ) ;
if ( span = = null )
2024-09-23 21:35:30 +00:00
{
await Next ( context ) ;
return ;
}
2025-03-14 22:17:54 +00:00
span . SetTag ( "operation.name" , "elsa.workflow.execution" ) ;
2025-03-14 22:55:02 +00:00
span . SetTag ( "span.type" , "workflow" ) ;
2025-03-14 22:17:54 +00:00
span . SetTag ( "workflow.definition.id" , workflow . Identity . DefinitionId ) ;
span . SetTag ( "workflow.definition.version" , workflow . Identity . Version ) ;
span . SetTag ( "workflow.definition.name" , workflow . WorkflowMetadata . Name ) ;
span . SetTag ( "workflow.definition.tenant.id" , workflow . Identity . TenantId ) ;
span . SetTag ( "workflow.instance.id" , workflowInstanceId ) ;
if ( context . TriggerActivityId ! = null )
2024-10-07 08:15:35 +00:00
{
2025-03-14 22:17:54 +00:00
var activity = context . FindActivityById ( context . TriggerActivityId ) ? ? throw new ( $"Trigger activity with ID {context.TriggerActivityId} not found. This should not happen." ) ;
span . SetTag ( "workflow.trigger.activity.id" , activity . Id ) ;
span . SetTag ( "workflow.trigger.activity.name" , activity . Name ) ;
span . SetTag ( "workflow.trigger.activity.type" , activity . Type ) ;
span . SetTag ( "workflow.trigger.activity.version" , activity . Version ) ;
2024-10-07 08:15:35 +00:00
}
2025-03-14 22:17:54 +00:00
span . AddEvent ( new ( "executing" ) ) ;
2024-09-23 21:35:30 +00:00
await Next ( context ) ;
if ( context . SubStatus = = WorkflowSubStatus . Faulted )
{
2025-03-14 22:17:54 +00:00
span . AddEvent ( new ( "faulted" ) ) ;
2025-05-07 08:59:44 +00:00
var lastIncident = context . Incidents . FirstOrDefault ( ) ;
if ( lastIncident = = null )
span . SetStatus ( ActivityStatusCode . Error , "The workflow entered the Faulted state. See incidents for details." ) ;
else
{
span . SetStatus ( ActivityStatusCode . Error , lastIncident . Message ) ;
var activityExecutionContext = context . ActivityExecutionContexts . FirstOrDefault ( x = > x . Activity . NodeId = = lastIncident . ActivityNodeId & & x . Status = = ActivityStatus . Faulted ) ;
var exception = activityExecutionContext ? . Exception ;
if ( exception ! = null )
{
var errorSpanHandlerContext = new WorkflowErrorSpanContext ( span , lastIncident , exception ) ;
var errorSpanHandler = context . GetServices < IWorkflowErrorSpanHandler > ( )
. OrderBy ( x = > x . Order )
. FirstOrDefault ( x = > x . CanHandle ( errorSpanHandlerContext ) ) ;
errorSpanHandler ? . Handle ( errorSpanHandlerContext ) ;
}
}
2024-09-23 21:35:30 +00:00
}
2025-03-14 22:17:54 +00:00
else if ( context . SubStatus = = WorkflowSubStatus . Finished )
2024-09-23 21:35:30 +00:00
{
2025-03-14 22:17:54 +00:00
span . AddEvent ( new ( "finished" ) ) ;
2024-09-23 21:35:30 +00:00
}
2025-03-14 22:17:54 +00:00
else if ( context . SubStatus = = WorkflowSubStatus . Cancelled )
2024-10-05 09:19:02 +00:00
{
2025-03-14 22:17:54 +00:00
span . AddEvent ( new ( "canceled" ) ) ;
}
else if ( context . SubStatus = = WorkflowSubStatus . Suspended )
{
span . AddEvent ( new ( "suspended" ) ) ;
}
if ( context . Incidents . Any ( ) )
{
span . SetTag ( "workflow.incidents.count" , context . Incidents . Count ) ;
2024-10-05 09:19:02 +00:00
2025-03-14 22:17:54 +00:00
foreach ( var incident in context . Incidents )
span . AddEvent ( new ( "incident" , incident . Timestamp , CreateIncidentTags ( incident ) ) ) ;
2024-10-05 09:19:02 +00:00
}
2025-03-14 22:17:54 +00:00
2024-09-23 21:35:30 +00:00
if ( ! string . IsNullOrWhiteSpace ( context . CorrelationId ) )
2025-03-14 22:17:54 +00:00
span . SetTag ( "workflow.correlation_id" , context . CorrelationId ) ;
2024-09-23 09:35:43 +00:00
}
2025-03-14 22:17:54 +00:00
2025-04-22 09:46:16 +00:00
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 ;
2025-05-07 08:59:44 +00:00
2025-04-22 09:46:16 +00:00
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 ) ;
2025-05-07 08:59:44 +00:00
2025-04-22 09:46:16 +00:00
if ( span ! = null & & linkedTraceContext ! = null )
2025-05-07 08:59:44 +00:00
span . AddLink ( new ( linkedTraceContext . Value ) ) ;
2025-04-22 09:46:16 +00:00
return span ;
}
2025-03-14 22:17:54 +00:00
private ActivityTagsCollection CreateIncidentTags ( ActivityIncident incident )
2024-10-05 11:39:58 +00:00
{
2025-03-14 22:17:54 +00:00
var tags = new ActivityTagsCollection ( new Dictionary < string , object? >
2024-10-05 11:39:58 +00:00
{
2025-03-14 22:17:54 +00:00
["incident.message"] = incident . Message ,
2024-10-05 11:39:58 +00:00
} ) ;
2025-03-14 22:17:54 +00:00
if ( incident . Exception ! = null )
{
tags [ "incident.exception.message" ] = incident . Exception . Message ;
tags [ "incident.exception.stackTrace" ] = incident . Exception . StackTrace ;
2025-03-14 22:27:16 +00:00
tags [ "incident.exception.type" ] = incident . Exception . Type . FullName ;
2025-03-14 22:17:54 +00:00
}
return tags ;
2024-10-05 11:39:58 +00:00
}
2024-09-23 09:35:43 +00:00
}
/// <summary>
/// Contains extension methods for <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/>.
/// </summary>
[UsedImplicitly]
public static class OpenTelemetryWorkflowExecutionMiddlewareExtensions
{
2024-11-18 12:41:27 +00:00
/// <summary>
2024-09-23 09:35:43 +00:00
/// Installs the <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/> component in the workflow execution pipeline.
2024-11-18 12:41:27 +00:00
/// </summary>
2024-09-23 09:35:43 +00:00
public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionTracing ( this IWorkflowExecutionPipelineBuilder pipelineBuilder ) = > pipelineBuilder . Insert < OpenTelemetryTracingWorkflowExecutionMiddleware > ( 0 ) ;
2024-09-23 21:35:30 +00:00
}