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);
+ }
+}