Skip to content

Commit 5963b5f

Browse files
thomhurstclaude
andcommitted
refactor: Add Activity-based tracing for module execution (Phase 1)
Add System.Diagnostics.Activity-based tracing alongside existing AsyncLocal pattern for backward compatibility. This enables integration with OpenTelemetry and other APM tools. Changes: - Add ModuleActivityTracing.cs with ActivitySource for module execution - Update ModuleRunner to wrap execution in Activity scope - Record success/skip/failure status on Activity with appropriate tags This is Phase 1 (foundation, non-breaking) of the AsyncLocal refactor. Fixes #1461 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f01ebb4 commit 5963b5f

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

src/ModularPipelines/Engine/Execution/ModuleRunner.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using System.Reflection;
23
using Mediator;
34
using Microsoft.Extensions.DependencyInjection;
@@ -10,6 +11,7 @@
1011
using ModularPipelines.Models;
1112
using ModularPipelines.Modules;
1213
using ModularPipelines.Options;
14+
using ModularPipelines.Tracing;
1315

1416
namespace ModularPipelines.Engine.Execution;
1517

@@ -147,6 +149,9 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr
147149
var logger = GetOrCreateLogger(moduleType, scopedServiceProvider);
148150
var moduleContext = new ModuleContext(pipelineContext, module, executionContext, logger);
149151

152+
// Start Activity for distributed tracing (Phase 1: alongside AsyncLocal for compatibility)
153+
using var activity = ModuleActivityTracing.StartModuleActivity(moduleType);
154+
150155
// Set up logging and module type context - use try/finally to ensure cleanup of AsyncLocal context
151156
// Assignments MUST be inside try block to guarantee cleanup even if an exception
152157
// occurs immediately after assignment
@@ -155,6 +160,22 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr
155160
ModuleLogger.Values.Value = logger;
156161
ModuleLogger.CurrentModuleType.Value = moduleType;
157162
await ExecuteModuleLifecycle(moduleState, scopedServiceProvider, pipelineContext, executionContext, moduleContext, cancellationToken).ConfigureAwait(false);
163+
164+
// Record success or skip status on the Activity
165+
if (executionContext.Status == Enums.Status.Skipped)
166+
{
167+
ModuleActivityTracing.RecordSkipped(activity);
168+
}
169+
else
170+
{
171+
ModuleActivityTracing.RecordSuccess(activity);
172+
}
173+
}
174+
catch (Exception ex)
175+
{
176+
// Record failure on the Activity before re-throwing
177+
ModuleActivityTracing.RecordFailure(activity, ex);
178+
throw;
158179
}
159180
finally
160181
{
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Diagnostics;
2+
3+
namespace ModularPipelines.Tracing;
4+
5+
/// <summary>
6+
/// Provides Activity-based tracing for module execution.
7+
/// </summary>
8+
/// <remarks>
9+
/// This class provides distributed tracing support using System.Diagnostics.Activity,
10+
/// enabling integration with OpenTelemetry and other APM tools.
11+
///
12+
/// Phase 1 (current): Foundation - provides ActivitySource for module execution tracing
13+
/// alongside existing AsyncLocal pattern for backward compatibility.
14+
///
15+
/// Future phases will gradually transition ambient context from AsyncLocal to Activity.
16+
/// </remarks>
17+
public static class ModuleActivityTracing
18+
{
19+
/// <summary>
20+
/// Tag key for the module type name.
21+
/// </summary>
22+
public const string ModuleTypeTag = "modular_pipelines.module.type";
23+
24+
/// <summary>
25+
/// Tag key for the module type's full name (including namespace).
26+
/// </summary>
27+
public const string ModuleTypeFullNameTag = "modular_pipelines.module.type_full";
28+
29+
/// <summary>
30+
/// Tag key for the module execution status.
31+
/// </summary>
32+
public const string ModuleStatusTag = "modular_pipelines.module.status";
33+
34+
/// <summary>
35+
/// Tag key for exception type when a module fails.
36+
/// </summary>
37+
public const string ExceptionTypeTag = "exception.type";
38+
39+
/// <summary>
40+
/// Tag key for exception message when a module fails.
41+
/// </summary>
42+
public const string ExceptionMessageTag = "exception.message";
43+
44+
/// <summary>
45+
/// The ActivitySource for ModularPipelines module execution.
46+
/// </summary>
47+
/// <remarks>
48+
/// Listeners can subscribe to this source to receive module execution spans.
49+
/// The source name follows the recommended namespace-based naming convention.
50+
/// </remarks>
51+
public static readonly ActivitySource Source = new(
52+
name: "ModularPipelines.Modules",
53+
version: "1.0.0");
54+
55+
/// <summary>
56+
/// Starts a new Activity for module execution.
57+
/// </summary>
58+
/// <param name="moduleType">The type of the module being executed.</param>
59+
/// <returns>The started Activity, or null if no listeners are registered.</returns>
60+
public static Activity? StartModuleActivity(Type moduleType)
61+
{
62+
var activity = Source.StartActivity(
63+
name: $"Module.{moduleType.Name}",
64+
kind: ActivityKind.Internal);
65+
66+
if (activity is not null)
67+
{
68+
activity.SetTag(ModuleTypeTag, moduleType.Name);
69+
activity.SetTag(ModuleTypeFullNameTag, moduleType.FullName);
70+
}
71+
72+
return activity;
73+
}
74+
75+
/// <summary>
76+
/// Records a successful module completion on the activity.
77+
/// </summary>
78+
/// <param name="activity">The activity to update.</param>
79+
public static void RecordSuccess(Activity? activity)
80+
{
81+
activity?.SetTag(ModuleStatusTag, "Successful");
82+
activity?.SetStatus(ActivityStatusCode.Ok);
83+
}
84+
85+
/// <summary>
86+
/// Records a skipped module on the activity.
87+
/// </summary>
88+
/// <param name="activity">The activity to update.</param>
89+
public static void RecordSkipped(Activity? activity)
90+
{
91+
activity?.SetTag(ModuleStatusTag, "Skipped");
92+
activity?.SetStatus(ActivityStatusCode.Ok, "Module was skipped");
93+
}
94+
95+
/// <summary>
96+
/// Records a failed module execution on the activity.
97+
/// </summary>
98+
/// <param name="activity">The activity to update.</param>
99+
/// <param name="exception">The exception that caused the failure.</param>
100+
public static void RecordFailure(Activity? activity, Exception exception)
101+
{
102+
if (activity is null)
103+
{
104+
return;
105+
}
106+
107+
activity.SetTag(ModuleStatusTag, "Failed");
108+
activity.SetTag(ExceptionTypeTag, exception.GetType().FullName);
109+
activity.SetTag(ExceptionMessageTag, exception.Message);
110+
activity.SetStatus(ActivityStatusCode.Error, exception.Message);
111+
}
112+
}

0 commit comments

Comments
 (0)