diff --git a/Directory.Packages.props b/Directory.Packages.props index 8d70a9957..1ea00d408 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,197 +1,199 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 7f0887c07..526cd58b0 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -68,6 +68,8 @@ using Medallion.Threading.Postgres; using Medallion.Threading.Redis; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Options; +using OpenTelemetry; +using OpenTelemetry.Trace; using Proto.Cluster.Kubernetes; using Proto.Persistence.Sqlite; using Proto.Persistence.SqlServer; @@ -120,6 +122,14 @@ var sqlDatabaseProvider = Enum.Parse(configuration["Databas TypeAliasRegistry.RegisterAlias("OrderReceivedProducerFactory", typeof(GenericProducerFactory)); TypeAliasRegistry.RegisterAlias("OrderReceivedConsumerFactory", typeof(GenericConsumerFactory)); +// Configure OpenTelemetry Tracing +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("Elsa.Workflows") // Match your ActivitySource name here + .SetSampler(new AlwaysOnSampler()) // Always record traces for testing + .AddConsoleExporter() // Export spans to the console (optional) + .Build(); + + // Add Elsa services. services .AddElsa(elsa => @@ -454,6 +464,7 @@ services alterations.UseMassTransitDispatcher(); } }) + .UseOpenTelemetry() .UseWorkflowContexts(); if (useQuartz) diff --git a/src/modules/Elsa.OpenTelemetry/Abstractions/ErrorSpanHandlerBase.cs b/src/modules/Elsa.OpenTelemetry/Abstractions/ErrorSpanHandlerBase.cs new file mode 100644 index 000000000..420936119 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Abstractions/ErrorSpanHandlerBase.cs @@ -0,0 +1,9 @@ +using Elsa.OpenTelemetry.Contracts; +using Elsa.OpenTelemetry.Models; + +namespace Elsa.OpenTelemetry.Abstractions; + +public abstract class ErrorSpanHandlerBase : IErrorSpanHandler +{ + public abstract void Handle(ErrorSpanContext context); +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Contracts/IErrorSpanHandler.cs b/src/modules/Elsa.OpenTelemetry/Contracts/IErrorSpanHandler.cs new file mode 100644 index 000000000..d15c3dee4 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Contracts/IErrorSpanHandler.cs @@ -0,0 +1,10 @@ +using Elsa.OpenTelemetry.Abstractions; +using Elsa.OpenTelemetry.Models; + +namespace Elsa.OpenTelemetry.Contracts; + +public interface IErrorSpanHandler +{ + void Handle(ErrorSpanContext context); +} + diff --git a/src/modules/Elsa.OpenTelemetry/Extensions/ModuleExtensions.cs b/src/modules/Elsa.OpenTelemetry/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..25edd2794 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using Elsa.OpenTelemetry.Features; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseOpenTelemetry(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs index 67201c44d..d80f308b7 100644 --- a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs +++ b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs @@ -1,5 +1,8 @@ using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.OpenTelemetry.Contracts; +using Elsa.OpenTelemetry.Handlers; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.OpenTelemetry.Features; @@ -7,5 +10,8 @@ public class OpenTelemetryFeature(IModule module) : FeatureBase(module) { public override void Configure() { + Services + .AddScoped() + .AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Handlers/DefaultErrorSpanHandler.cs b/src/modules/Elsa.OpenTelemetry/Handlers/DefaultErrorSpanHandler.cs new file mode 100644 index 000000000..04bebef62 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Handlers/DefaultErrorSpanHandler.cs @@ -0,0 +1,24 @@ +using Elsa.OpenTelemetry.Abstractions; +using Elsa.OpenTelemetry.Models; + +namespace Elsa.OpenTelemetry.Handlers; + +public class DefaultErrorSpanHandler : ErrorSpanHandlerBase +{ + public override void Handle(ErrorSpanContext context) + { + var span = context.Span; + var exception = context.Exception; + var errorMessage = string.IsNullOrWhiteSpace(exception?.Message) ? "Unknown error" : exception.Message; + span.SetTag("error", true); + span.SetTag("error.message", errorMessage); + + if (exception != null) + { + span.SetTag("error.exceptionType", exception.GetType().FullName); + + if (!string.IsNullOrEmpty(exception.StackTrace)) + span.SetTag("error.stackTrace", exception.StackTrace); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Handlers/FaultExceptionErrorSpanHandler.cs b/src/modules/Elsa.OpenTelemetry/Handlers/FaultExceptionErrorSpanHandler.cs new file mode 100644 index 000000000..14aa2860a --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Handlers/FaultExceptionErrorSpanHandler.cs @@ -0,0 +1,19 @@ +using Elsa.OpenTelemetry.Abstractions; +using Elsa.OpenTelemetry.Models; +using Elsa.Workflows.Exceptions; + +namespace Elsa.OpenTelemetry.Handlers; + +public class FaultExceptionErrorSpanHandler : ErrorSpanHandlerBase +{ + public override void Handle(ErrorSpanContext context) + { + if(context.Exception is not FaultException faultException) + return; + + var span = context.Span; + span.SetTag("error.code", faultException.Code); + span.SetTag("error.category", faultException.Category); + span.SetTag("error.faultType", faultException.Type); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs index 0045fde75..5410d70bf 100644 --- a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs +++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using Elsa.Common; +using Elsa.OpenTelemetry.Contracts; using Elsa.OpenTelemetry.Helpers; +using Elsa.OpenTelemetry.Models; using Elsa.Workflows; using Elsa.Workflows.Pipelines.ActivityExecution; using JetBrains.Annotations; @@ -40,14 +42,13 @@ public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareD { span.AddEvent(new("Faulted", tags: CreateStatusTags(context))); span.SetStatus(ActivityStatusCode.Error); - span.SetTag("error", true); span.SetTag("activityInstance.hasIncidents", true); - var errorMessage = string.IsNullOrWhiteSpace(context.Exception?.Message) ? "Unknown error" : context.Exception.Message; - span.SetTag("error.message", errorMessage); + var errorSpanHandlers = context.GetServices(); + var errorSpanHandlerContext = new ErrorSpanContext(span, context.Exception); - if (!string.IsNullOrEmpty(context.Exception?.StackTrace)) - span.SetTag("error.stackTrace", context.Exception.StackTrace); + foreach (var handler in errorSpanHandlers) + handler.Handle(errorSpanHandlerContext); } else { diff --git a/src/modules/Elsa.OpenTelemetry/Models/ErrorSpanContext.cs b/src/modules/Elsa.OpenTelemetry/Models/ErrorSpanContext.cs new file mode 100644 index 000000000..a364b1b68 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Models/ErrorSpanContext.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace Elsa.OpenTelemetry.Models; + +public class ErrorSpanContext(Activity span, Exception? exception) +{ + public Activity Span => span; + public Exception? Exception => exception; +} \ No newline at end of file