diff --git a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs index 00c920f17e..a33b51471b 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection; using Mediator; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.Options; +using ModularPipelines.Tracing; namespace ModularPipelines.Engine.Execution; @@ -147,6 +149,9 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr var logger = GetOrCreateLogger(moduleType, scopedServiceProvider); var moduleContext = new ModuleContext(pipelineContext, module, executionContext, logger); + // Start Activity for distributed tracing (Phase 1: alongside AsyncLocal for compatibility) + using var activity = ModuleActivityTracing.StartModuleActivity(moduleType); + // Set up logging and module type context - use try/finally to ensure cleanup of AsyncLocal context // Assignments MUST be inside try block to guarantee cleanup even if an exception // occurs immediately after assignment @@ -155,6 +160,22 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr ModuleLogger.Values.Value = logger; ModuleLogger.CurrentModuleType.Value = moduleType; await ExecuteModuleLifecycle(moduleState, scopedServiceProvider, pipelineContext, executionContext, moduleContext, cancellationToken).ConfigureAwait(false); + + // Record success or skip status on the Activity + if (executionContext.Status == Enums.Status.Skipped) + { + ModuleActivityTracing.RecordSkipped(activity); + } + else + { + ModuleActivityTracing.RecordSuccess(activity); + } + } + catch (Exception ex) + { + // Record failure on the Activity before re-throwing + ModuleActivityTracing.RecordFailure(activity, ex); + throw; } finally { diff --git a/src/ModularPipelines/Tracing/ModuleActivityTracing.cs b/src/ModularPipelines/Tracing/ModuleActivityTracing.cs new file mode 100644 index 0000000000..c53ff6f250 --- /dev/null +++ b/src/ModularPipelines/Tracing/ModuleActivityTracing.cs @@ -0,0 +1,112 @@ +using System.Diagnostics; + +namespace ModularPipelines.Tracing; + +/// +/// Provides Activity-based tracing for module execution. +/// +/// +/// This class provides distributed tracing support using System.Diagnostics.Activity, +/// enabling integration with OpenTelemetry and other APM tools. +/// +/// Phase 1 (current): Foundation - provides ActivitySource for module execution tracing +/// alongside existing AsyncLocal pattern for backward compatibility. +/// +/// Future phases will gradually transition ambient context from AsyncLocal to Activity. +/// +public static class ModuleActivityTracing +{ + /// + /// Tag key for the module type name. + /// + public const string ModuleTypeTag = "modular_pipelines.module.type"; + + /// + /// Tag key for the module type's full name (including namespace). + /// + public const string ModuleTypeFullNameTag = "modular_pipelines.module.type_full"; + + /// + /// Tag key for the module execution status. + /// + public const string ModuleStatusTag = "modular_pipelines.module.status"; + + /// + /// Tag key for exception type when a module fails. + /// + public const string ExceptionTypeTag = "exception.type"; + + /// + /// Tag key for exception message when a module fails. + /// + public const string ExceptionMessageTag = "exception.message"; + + /// + /// The ActivitySource for ModularPipelines module execution. + /// + /// + /// Listeners can subscribe to this source to receive module execution spans. + /// The source name follows the recommended namespace-based naming convention. + /// + public static readonly ActivitySource Source = new( + name: "ModularPipelines.Modules", + version: "1.0.0"); + + /// + /// Starts a new Activity for module execution. + /// + /// The type of the module being executed. + /// The started Activity, or null if no listeners are registered. + public static Activity? StartModuleActivity(Type moduleType) + { + var activity = Source.StartActivity( + name: $"Module.{moduleType.Name}", + kind: ActivityKind.Internal); + + if (activity is not null) + { + activity.SetTag(ModuleTypeTag, moduleType.Name); + activity.SetTag(ModuleTypeFullNameTag, moduleType.FullName); + } + + return activity; + } + + /// + /// Records a successful module completion on the activity. + /// + /// The activity to update. + public static void RecordSuccess(Activity? activity) + { + activity?.SetTag(ModuleStatusTag, "Successful"); + activity?.SetStatus(ActivityStatusCode.Ok); + } + + /// + /// Records a skipped module on the activity. + /// + /// The activity to update. + public static void RecordSkipped(Activity? activity) + { + activity?.SetTag(ModuleStatusTag, "Skipped"); + activity?.SetStatus(ActivityStatusCode.Ok, "Module was skipped"); + } + + /// + /// Records a failed module execution on the activity. + /// + /// The activity to update. + /// The exception that caused the failure. + public static void RecordFailure(Activity? activity, Exception exception) + { + if (activity is null) + { + return; + } + + activity.SetTag(ModuleStatusTag, "Failed"); + activity.SetTag(ExceptionTypeTag, exception.GetType().FullName); + activity.SetTag(ExceptionMessageTag, exception.Message); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + } +}