< Summary

Information
Class: Elsa.Workflows.Telemetry.WorkflowInstrumentation
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Telemetry/WorkflowInstrumentation.cs
Line coverage
98%
Covered lines: 140
Uncovered lines: 2
Coverable lines: 142
Total lines: 282
Line coverage: 98.5%
Branch coverage
91%
Covered branches: 90
Total branches: 98
Branch coverage: 91.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()50%22100%
StartWorkflow(...)87.5%88100%
StartActivity(...)100%22100%
StopWorkflow(...)100%2626100%
StopActivity(...)100%1818100%
SetWorkflowTags(...)100%44100%
SetActivityTags(...)75%88100%
SetActivityOutcome(...)66.66%6688.88%
SetError(...)100%44100%
CreateWorkflowTags(...)100%66100%
CreateActivityTags(...)100%22100%
GetWorkflowStatus(...)75%4485.71%
AddIfNotNull(...)100%22100%
AddIfNotNull(...)100%22100%
ShouldAddTag(...)75%44100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Telemetry/WorkflowInstrumentation.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Diagnostics.Metrics;
 3
 4using DiagnosticsActivity = System.Diagnostics.Activity;
 5using DiagnosticsActivityKind = System.Diagnostics.ActivityKind;
 6using WorkflowActivityStatus = Elsa.Workflows.ActivityStatus;
 7using WorkflowInstanceStatus = Elsa.Workflows.WorkflowStatus;
 8using WorkflowInstanceSubStatus = Elsa.Workflows.WorkflowSubStatus;
 9
 10namespace Elsa.Workflows.Telemetry;
 11
 12/// <summary>
 13/// Provides tracing and metrics instrumentation for workflow and activity execution.
 14/// </summary>
 15public static class WorkflowInstrumentation
 16{
 17    public const string ActivitySourceName = "Elsa.Workflows";
 18    public const string MeterName = "Elsa.Workflows";
 19
 20    public const string WorkflowSystem = "workflow.system";
 21    public const string WorkflowOperationName = "workflow.operation.name";
 22    public const string WorkflowName = "workflow.name";
 23    public const string WorkflowInstanceId = "workflow.instance.id";
 24    public const string WorkflowDefinitionId = "workflow.definition.id";
 25    public const string WorkflowDefinitionVersion = "workflow.definition.version";
 26    public const string WorkflowDefinitionVersionId = "workflow.definition.version.id";
 27    public const string WorkflowStatus = "workflow.status";
 28    public const string WorkflowSubStatus = "workflow.substatus";
 29    public const string WorkflowFaulted = "workflow.faulted";
 30    public const string WorkflowParentInstanceId = "workflow.parent.instance.id";
 31    public const string WorkflowCorrelationId = "workflow.correlation.id";
 32
 33    public const string ActivityOperationName = "workflow.activity.operation.name";
 34    public const string ActivityId = "workflow.activity.id";
 35    public const string ActivityName = "workflow.activity.name";
 36    public const string ActivityType = "workflow.activity.type";
 37    public const string ActivityVersion = "workflow.activity.version";
 38    public const string ActivityExecutionId = "workflow.activity.execution.id";
 39    public const string ActivityStatus = "workflow.activity.status";
 40    public const string ActivityOutcome = "workflow.activity.outcome";
 41    public const string ActivityParentExecutionId = "workflow.activity.parent.execution.id";
 42    public const string ActivityScheduledByExecutionId = "workflow.activity.scheduled.by.execution.id";
 43    public const string ActivityFaulted = "workflow.activity.faulted";
 44
 45    public const string TenantId = "elsa.tenant.id";
 46    public const string ExceptionType = "exception.type";
 47
 48    [Obsolete("Use ExceptionType. Workflow spans follow OpenTelemetry exception semantic conventions.")]
 49    public const string ErrorType = ExceptionType;
 50
 51    private const string SystemName = "elsa";
 52    private const string WorkflowExecuteOperation = "workflow.execute";
 53    private const string ActivityExecuteOperation = "activity.execute";
 54
 455    private static readonly string? Version = typeof(WorkflowInstrumentation).Assembly.GetName().Version?.ToString();
 456    private static readonly ActivitySource Source = new(ActivitySourceName, Version);
 457    private static readonly Meter Meter = new(MeterName, Version);
 458    private static readonly Counter<long> WorkflowStartedCounter = Meter.CreateCounter<long>("elsa.workflow.started", de
 459    private static readonly Counter<long> WorkflowCompletedCounter = Meter.CreateCounter<long>("elsa.workflow.completed"
 460    private static readonly Counter<long> WorkflowFaultedCounter = Meter.CreateCounter<long>("elsa.workflow.faulted", de
 461    private static readonly Histogram<double> ActivityDuration = Meter.CreateHistogram<double>("elsa.activity.duration",
 62
 63    internal static WorkflowInstrumentationScope StartWorkflow(WorkflowExecutionContext context, bool? isStarting = null
 64    {
 50965        var shouldRecordStarted = isStarting ?? context.SubStatus == WorkflowInstanceSubStatus.Pending;
 50966        var activity = Source.StartActivity(WorkflowExecuteOperation, DiagnosticsActivityKind.Internal);
 67
 50968        if (activity != null)
 69        {
 870            SetWorkflowTags(activity, context);
 871            activity.SetTag(WorkflowOperationName, WorkflowExecuteOperation);
 72        }
 73
 50974        if (shouldRecordStarted && WorkflowStartedCounter.Enabled)
 775            WorkflowStartedCounter.Add(1, CreateWorkflowTags(context, false));
 76
 50977        return new WorkflowInstrumentationScope(activity);
 78    }
 79
 80    internal static ActivityInstrumentationScope StartActivity(ActivityExecutionContext context)
 81    {
 330682        var activity = Source.StartActivity(ActivityExecuteOperation, DiagnosticsActivityKind.Internal);
 83
 330684        if (activity != null)
 85        {
 686            SetWorkflowTags(activity, context.WorkflowExecutionContext);
 687            SetActivityTags(activity, context);
 688            activity.SetTag(ActivityOperationName, ActivityExecuteOperation);
 89        }
 90
 330691        return new ActivityInstrumentationScope(activity, Stopwatch.GetTimestamp());
 92    }
 93
 94    internal static void StopWorkflow(WorkflowInstrumentationScope scope, WorkflowExecutionContext context, Exception? e
 95    {
 50996        var activity = scope.Activity;
 50997        var workflowException = exception ?? (context.SubStatus == WorkflowInstanceSubStatus.Faulted ? context.Exception
 50998        var cancelled = exception is OperationCanceledException || (exception == null && context.SubStatus == WorkflowIn
 50999        var faulted = !cancelled && (workflowException != null || context.SubStatus == WorkflowInstanceSubStatus.Faulted
 509100        var workflowSubStatus = cancelled ? WorkflowInstanceSubStatus.Cancelled : faulted ? WorkflowInstanceSubStatus.Fa
 101
 509102        if (activity != null)
 103        {
 8104            SetWorkflowTags(activity, context, workflowSubStatus);
 8105            activity.SetTag(WorkflowFaulted, faulted);
 8106            SetError(activity, workflowException, faulted);
 8107            activity.Dispose();
 108        }
 109
 509110        if (faulted && WorkflowFaultedCounter.Enabled)
 3111            WorkflowFaultedCounter.Add(1, CreateWorkflowTags(context, workflowSubStatusOverride: workflowSubStatus));
 506112        else if (context.SubStatus == WorkflowInstanceSubStatus.Finished && WorkflowCompletedCounter.Enabled)
 1113            WorkflowCompletedCounter.Add(1, CreateWorkflowTags(context));
 506114    }
 115
 116    internal static void StopActivity(ActivityInstrumentationScope scope, ActivityExecutionContext context, Exception? e
 117    {
 3306118        var activity = scope.Activity;
 3306119        var cancelled = exception is OperationCanceledException || (exception == null && context.Status == WorkflowActiv
 3306120        var faulted = !cancelled && (exception != null || context.Status == WorkflowActivityStatus.Faulted);
 3306121        var activityStatus = cancelled ? WorkflowActivityStatus.Canceled : context.Status;
 3306122        var duration = Stopwatch.GetElapsedTime(scope.StartTimestamp).TotalSeconds;
 123
 3306124        if (ActivityDuration.Enabled)
 6125            ActivityDuration.Record(duration, CreateActivityTags(context, activityStatus, faulted));
 126
 3306127        if (activity != null)
 128        {
 6129            SetActivityTags(activity, context, activityStatus);
 6130            activity.SetTag(ActivityFaulted, faulted);
 6131            if (faulted)
 3132                activity.SetTag(ActivityStatus, WorkflowActivityStatus.Faulted.ToString());
 6133            SetActivityOutcome(activity, context);
 6134            SetError(activity, exception ?? context.Exception, faulted);
 6135            activity.Dispose();
 136        }
 3306137    }
 138
 139    private static void SetWorkflowTags(DiagnosticsActivity activity, WorkflowExecutionContext context, WorkflowInstance
 140    {
 22141        var workflow = context.Workflow;
 22142        var identity = workflow.Identity;
 22143        var workflowSubStatus = workflowSubStatusOverride ?? context.SubStatus;
 22144        var workflowStatus = GetWorkflowStatus(context, workflowSubStatusOverride);
 145
 22146        activity.SetTag(WorkflowSystem, SystemName);
 22147        activity.SetTag(WorkflowInstanceId, context.Id);
 22148        activity.SetTag(WorkflowDefinitionId, identity.DefinitionId);
 22149        activity.SetTag(WorkflowDefinitionVersion, identity.Version);
 22150        activity.SetTag(WorkflowDefinitionVersionId, identity.Id);
 22151        activity.SetTag(WorkflowStatus, workflowStatus.ToString());
 22152        activity.SetTag(WorkflowSubStatus, workflowSubStatus.ToString());
 22153        AddIfNotNull(activity, WorkflowName, workflow.WorkflowMetadata.Name ?? workflow.Name);
 22154        AddIfNotNull(activity, WorkflowParentInstanceId, context.ParentWorkflowInstanceId);
 22155        AddIfNotNull(activity, WorkflowCorrelationId, context.CorrelationId);
 22156        AddIfNotNull(activity, TenantId, identity.TenantId);
 22157    }
 158
 159    private static void SetActivityTags(DiagnosticsActivity activity, ActivityExecutionContext context, WorkflowActivity
 160    {
 12161        var currentActivity = context.Activity;
 12162        var activityStatus = activityStatusOverride ?? context.Status;
 163
 12164        activity.SetTag(ActivityId, currentActivity.Id);
 12165        activity.SetTag(ActivityType, currentActivity.Type);
 12166        activity.SetTag(ActivityVersion, currentActivity.Version);
 12167        activity.SetTag(ActivityExecutionId, context.Id);
 12168        activity.SetTag(ActivityStatus, activityStatus.ToString());
 12169        AddIfNotNull(activity, ActivityName, currentActivity.Name ?? context.ActivityDescriptor.DisplayName ?? context.A
 12170        AddIfNotNull(activity, ActivityParentExecutionId, context.ParentActivityExecutionContext?.Id);
 12171        AddIfNotNull(activity, ActivityScheduledByExecutionId, context.SchedulingActivityExecutionId);
 12172    }
 173
 174    private static void SetActivityOutcome(DiagnosticsActivity activity, ActivityExecutionContext context)
 175    {
 6176        if (!context.JournalData.TryGetValue("Outcomes", out var outcomes))
 5177            return;
 178
 1179        var outcome = outcomes switch
 1180        {
 0181            IEnumerable<string> names => string.Join(",", names),
 1182            _ => outcomes?.ToString()
 1183        };
 184
 1185        AddIfNotNull(activity, ActivityOutcome, outcome);
 1186    }
 187
 188    private static void SetError(DiagnosticsActivity activity, Exception? exception, bool faulted)
 189    {
 14190        if (!faulted)
 191        {
 8192            activity.SetStatus(ActivityStatusCode.Ok);
 8193            return;
 194        }
 195
 6196        activity.SetStatus(ActivityStatusCode.Error, "Faulted");
 197
 6198        if (exception != null)
 199        {
 5200            var exceptionType = exception.GetType().FullName;
 5201            activity.SetTag(ExceptionType, exceptionType);
 5202            activity.AddEvent(new("exception", tags: new ActivityTagsCollection
 5203            {
 5204                { ExceptionType, exceptionType }
 5205            }));
 206        }
 6207    }
 208
 209    private static TagList CreateWorkflowTags(WorkflowExecutionContext context, bool includeExecutionStatus = true, Work
 210    {
 11211        var workflow = context.Workflow;
 11212        var identity = workflow.Identity;
 11213        var workflowSubStatus = workflowSubStatusOverride ?? context.SubStatus;
 11214        var workflowStatus = GetWorkflowStatus(context, workflowSubStatusOverride);
 11215        var tags = new TagList
 11216        {
 11217            { WorkflowSystem, SystemName },
 11218            { WorkflowDefinitionId, identity.DefinitionId },
 11219            { WorkflowDefinitionVersion, identity.Version }
 11220        };
 221
 11222        if (includeExecutionStatus)
 223        {
 4224            tags.Add(WorkflowStatus, workflowStatus.ToString());
 4225            tags.Add(WorkflowSubStatus, workflowSubStatus.ToString());
 226        }
 227
 11228        AddIfNotNull(ref tags, WorkflowName, workflow.WorkflowMetadata.Name ?? workflow.Name);
 11229        AddIfNotNull(ref tags, TenantId, identity.TenantId);
 11230        return tags;
 231    }
 232
 233    private static TagList CreateActivityTags(ActivityExecutionContext context, WorkflowActivityStatus activityStatus, b
 234    {
 6235        var currentActivity = context.Activity;
 6236        var tags = new TagList
 6237        {
 6238            { WorkflowSystem, SystemName },
 6239            { WorkflowDefinitionId, context.WorkflowExecutionContext.Workflow.Identity.DefinitionId },
 6240            { ActivityType, currentActivity.Type },
 6241            { ActivityVersion, currentActivity.Version },
 6242            { ActivityStatus, faulted ? WorkflowActivityStatus.Faulted.ToString() : activityStatus.ToString() },
 6243            { ActivityFaulted, faulted }
 6244        };
 245
 6246        AddIfNotNull(ref tags, TenantId, context.WorkflowExecutionContext.Workflow.Identity.TenantId);
 6247        return tags;
 248    }
 249
 250    private static WorkflowInstanceStatus GetWorkflowStatus(WorkflowExecutionContext context, WorkflowInstanceSubStatus?
 251    {
 33252        if (workflowSubStatusOverride == null)
 23253            return context.Status;
 254
 10255        return workflowSubStatusOverride.Value switch
 10256        {
 10257            WorkflowInstanceSubStatus.Cancelled or WorkflowInstanceSubStatus.Faulted or WorkflowInstanceSubStatus.Finish
 0258            _ => WorkflowInstanceStatus.Running
 10259        };
 260    }
 261
 262    private static void AddIfNotNull(ref TagList tags, string key, object? value)
 263    {
 28264        if (ShouldAddTag(value))
 4265            tags.Add(key, value!);
 28266    }
 267
 268    private static void AddIfNotNull(DiagnosticsActivity activity, string key, object? value)
 269    {
 125270        if (ShouldAddTag(value))
 18271            activity.SetTag(key, value);
 125272    }
 273
 274    private static bool ShouldAddTag(object? value)
 275    {
 153276        return value is not null && (value is not string stringValue || !string.IsNullOrWhiteSpace(stringValue));
 277    }
 278}
 279
 280internal readonly record struct WorkflowInstrumentationScope(DiagnosticsActivity? Activity);
 281
 282internal readonly record struct ActivityInstrumentationScope(DiagnosticsActivity? Activity, long StartTimestamp);