Refactor OpenTelemetry error handling implementation (#6621)

* Refactor OpenTelemetry error handling implementation

Introduce WorkflowErrorSpanHandler interface and context for improved error span handling in workflows. Separate activity and workflow error handling using dedicated abstractions and context models. Update error handling logic in tracing middleware and adjust DI configuration accordingly.

* Rename handler class and improve error tagging logic

Renamed `FaultExceptionActivityErrorSpanHandler` to `FaultExceptionErrorSpanHandler` for consistency and clarity. Updated error attribute tagging to align with Datadog's well-known attributes. Adjusted incident selection logic to use the first incident instead of the last.

* Align .gitignore file with consistent formatting

Standardized comments in the .gitignore file by adding missing spaces and capitalizing as needed. Updated the `/docker/data/` entry to `/docker/azurite-data/` for clarity.

* Fix incorrect selection of activity execution context

Replaced `LastOrDefault` with `FirstOrDefault` to ensure the correct activity execution context is retrieved when handling errors. This change resolves potential inaccuracies in identifying the faulted activity node.

* Exclude docker-compose-datadog.yml from solution file.
This commit is contained in:
Sipke Schoorstra 2025-05-07 10:59:44 +02:00 committed by GitHub
parent dacbef13e6
commit f0639cbd42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 134 additions and 56 deletions

18
.gitignore vendored
View file

@ -1,7 +1,7 @@
#Ignore thumbnails created by Windows
# Ignore thumbnails created by Windows
Thumbs.db
#Ignore files built by Visual Studio
# Ignore files built by Visual Studio
*.obj
*.exe
*.pdb
@ -32,26 +32,26 @@ _ReSharper*/
# VS Code/Omnisharp crash dumps
mono_crash.mem*.blob
#Nuget packages folder
# Nuget packages folder
packages/
#Ignore git-related files
# Ignore git-related files
*.orig
#Rider
# Rider
.idea
.run
#wwwroot
# wwwroot
wwwroot/
#npm
# npm
node_modules/
.babelrc
.stylelintrc.yml
.eslintrc.yml
#volatile
# volatile
App_Data/
*.db
*.db-journal
@ -78,5 +78,5 @@ unlist.sh
/artifacts
/docker/data/
/docker/azurite-data/
docker/docker-compose-datadog.yml

View file

@ -105,7 +105,7 @@ const bool useTenantsFromConfiguration = true;
const bool useSecrets = false;
const bool disableVariableWrappers = false;
const bool disableVariableCopying = false;
const bool useManualOtelInstrumentation = false;
const bool useManualOtelInstrumentation = true;
ObjectConverter.StrictMode = false;
@ -137,7 +137,7 @@ TypeAliasRegistry.RegisterAlias("OrderReceivedConsumerFactory", typeof(GenericCo
if (useManualOtelInstrumentation)
{
services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("elsa-workflows", serviceVersion: "3.4.0").AddTelemetrySdk())
.ConfigureResource(resource => resource.AddService("elsa-workflows", serviceVersion: "3.5.0").AddTelemetrySdk())
.WithTracing(tracing =>
{
tracing
@ -536,7 +536,7 @@ services
alterations.UseMassTransitDispatcher();
}
})
.UseOpenTelemetry()
.UseOpenTelemetry(otel => otel.UseNewRootActivityForRemoteParent = true)
.UseWorkflowContexts();
if (useQuartz)

View file

@ -0,0 +1,11 @@
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Abstractions;
public abstract class ActivityErrorSpanHandlerBase : IActivityErrorSpanHandler
{
public virtual float Order => 0;
public abstract bool CanHandle(ActivityErrorSpanContext context);
public abstract void Handle(ActivityErrorSpanContext context);
}

View file

@ -1,11 +0,0 @@
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Abstractions;
public abstract class ErrorSpanHandlerBase : IErrorSpanHandler
{
public virtual float Order => 0;
public abstract bool CanHandle(ErrorSpanContext context);
public abstract void Handle(ErrorSpanContext context);
}

View file

@ -0,0 +1,11 @@
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Abstractions;
public abstract class WorkflowErrorSpanHandlerBase : IWorkflowErrorSpanHandler
{
public virtual float Order => 0;
public abstract bool CanHandle(WorkflowErrorSpanContext context);
public abstract void Handle(WorkflowErrorSpanContext context);
}

View file

@ -0,0 +1,10 @@
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Contracts;
public interface IActivityErrorSpanHandler
{
float Order { get; }
bool CanHandle(ActivityErrorSpanContext context);
void Handle(ActivityErrorSpanContext context);
}

View file

@ -1,11 +0,0 @@
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Contracts;
public interface IErrorSpanHandler
{
float Order { get; }
bool CanHandle(ErrorSpanContext context);
void Handle(ErrorSpanContext context);
}

View file

@ -0,0 +1,10 @@
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Contracts;
public interface IWorkflowErrorSpanHandler
{
float Order { get; }
bool CanHandle(WorkflowErrorSpanContext context);
void Handle(WorkflowErrorSpanContext context);
}

View file

@ -23,8 +23,10 @@ public class OpenTelemetryFeature(IModule module) : FeatureBase(module)
public override void Configure()
{
Services
.AddScoped<IErrorSpanHandler, DefaultErrorSpanHandler>()
.AddScoped<IErrorSpanHandler, FaultExceptionErrorSpanHandler>();
.AddScoped<IActivityErrorSpanHandler, DefaultErrorSpanHandler>()
.AddScoped<IActivityErrorSpanHandler, FaultExceptionErrorSpanHandler>()
.AddScoped<IWorkflowErrorSpanHandler, DefaultErrorSpanHandler>()
.AddScoped<IWorkflowErrorSpanHandler, FaultExceptionErrorSpanHandler>();
Services.Configure<OpenTelemetryOptions>(options =>
{

View file

@ -1,15 +1,21 @@
using Elsa.OpenTelemetry.Abstractions;
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Models;
namespace Elsa.OpenTelemetry.Handlers;
public class DefaultErrorSpanHandler : ErrorSpanHandlerBase
public class DefaultErrorSpanHandler : IActivityErrorSpanHandler, IWorkflowErrorSpanHandler
{
public override float Order => 100000;
public override bool CanHandle(ErrorSpanContext context) => context.Exception != null;
public float Order => 100000;
public override void Handle(ErrorSpanContext context)
public bool CanHandle(WorkflowErrorSpanContext context) => false;
public void Handle(WorkflowErrorSpanContext context)
{
}
public bool CanHandle(ActivityErrorSpanContext context) => context.Exception != null;
public void Handle(ActivityErrorSpanContext context)
{
context.Span.AddException(context.Exception!);
}

View file

@ -1,23 +1,39 @@
using Elsa.OpenTelemetry.Abstractions;
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Models;
using Elsa.Workflows.Exceptions;
namespace Elsa.OpenTelemetry.Handlers;
public class FaultExceptionErrorSpanHandler : ErrorSpanHandlerBase
public class FaultExceptionErrorSpanHandler : IActivityErrorSpanHandler, IWorkflowErrorSpanHandler
{
public override bool CanHandle(ErrorSpanContext context) => context.Exception is FaultException;
public float Order => 0;
public override void Handle(ErrorSpanContext context)
public bool CanHandle(ActivityErrorSpanContext context) => context.Exception is FaultException;
public bool CanHandle(WorkflowErrorSpanContext context) => context.Exception is FaultException;
public void Handle(ActivityErrorSpanContext context)
{
var faultException = (FaultException)context.Exception!;
var span = context.Span;
var tags = new Dictionary<string, object?>
var tags = new Dictionary<string, object?>()
{
["exception.code"] = faultException.Code,
["exception.category"] = faultException.Category,
["exception.type"] = faultException.Type
};
span.AddException(context.Exception, new(tags.ToArray()));
span.AddException(faultException, new(tags.ToArray()));
}
public void Handle(WorkflowErrorSpanContext context)
{
var faultException = (FaultException)context.Exception!;
var span = context.Span;
// The following two attributes are well-known by datadog.
span.SetTag("error.code", faultException.Code);
span.SetTag("error.category", faultException.Category);
// Datadog will ignore unknown attributes, so we'll set them on a different object.
span.SetTag("error_details.type", faultException.Type);
}
}

View file

@ -50,8 +50,8 @@ public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareD
span.AddEvent(new("faulted"));
span.SetStatus(ActivityStatusCode.Error);
var errorSpanHandlerContext = new ErrorSpanContext(span, context.Exception);
var errorSpanHandler = context.GetServices<IErrorSpanHandler>()
var errorSpanHandlerContext = new ActivityErrorSpanContext(span, context.Exception);
var errorSpanHandler = context.GetServices<IActivityErrorSpanHandler>()
.OrderBy(x => x.Order)
.FirstOrDefault(x => x.CanHandle(errorSpanHandlerContext));

View file

@ -1,6 +1,8 @@
using System.Diagnostics;
using Elsa.Common;
using Elsa.OpenTelemetry.Contracts;
using Elsa.OpenTelemetry.Helpers;
using Elsa.OpenTelemetry.Models;
using Elsa.OpenTelemetry.Options;
using Elsa.Workflows;
using Elsa.Workflows.Models;
@ -56,7 +58,28 @@ public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareD
if (context.SubStatus == WorkflowSubStatus.Faulted)
{
span.AddEvent(new("faulted"));
span.SetStatus(ActivityStatusCode.Error, "The workflow entered the Faulted state. See incidents for details.");
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);
}
}
}
else if (context.SubStatus == WorkflowSubStatus.Finished)
{
@ -88,7 +111,7 @@ public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareD
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;
@ -108,10 +131,10 @@ public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareD
}
var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"execute workflow {workflowName}", ActivityKind.Server, contextToUse);
if (span != null && linkedTraceContext != null)
span.AddLink(new (linkedTraceContext.Value));
span.AddLink(new(linkedTraceContext.Value));
return span;
}

View file

@ -2,7 +2,7 @@ using System.Diagnostics;
namespace Elsa.OpenTelemetry.Models;
public class ErrorSpanContext(Activity span, Exception? exception)
public class ActivityErrorSpanContext(Activity span, Exception? exception)
{
public Activity Span => span;
public Exception? Exception => exception;

View file

@ -0,0 +1,11 @@
using System.Diagnostics;
using Elsa.Workflows.Models;
namespace Elsa.OpenTelemetry.Models;
public class WorkflowErrorSpanContext(Activity span, ActivityIncident incident, Exception? exception)
{
public Activity Span => span;
public ActivityIncident Incident => incident;
public Exception? Exception { get; } = exception;
}