Observability¶
Deepstaging instruments every generated effect method with OpenTelemetry tracing, metrics, and structured logging. Configuration is either code-driven (per-runtime) or config-driven (global toggle).
Config-Driven (Recommended)¶
Set one value and every runtime in your application gets full distributed tracing with OTLP export:
That's it. When running under Aspire, the OTLP endpoint is set automatically via the OTEL_EXPORTER_OTLP_ENDPOINT environment variable — traces appear in the Aspire dashboard with zero additional configuration.
Environment variables work too
Deepstaging__Telemetry__Enabled=true enables telemetry via standard .NET configuration binding, useful for Docker/Kubernetes deployments.
What Happens¶
When Deepstaging:Telemetry:Enabled is true, the generated bootstrapper for every runtime automatically:
- Registers ActivitySources — one per module (e.g.,
MyApp.OrderDispatch,MyApp.OrderStore) - Adds
Microsoft.AspNetCoresource — captures HTTP request spans - Configures the OTLP exporter — exports traces to whatever endpoint
OTEL_EXPORTER_OTLP_ENDPOINTpoints at - Enables metrics (if the runtime has instrumented modules) — effect counters and duration histograms
The OTLP exporter is configured exactly once across all runtimes (via an internal guard), so multiple AddXxxRuntime() calls don't produce duplicate exports.
Code-Driven (Per-Runtime)¶
For fine-grained control, enable observability per-runtime in the configure callback:
builder.AddCatalogRuntime(rt =>
{
rt.EnableTracing(); // OpenTelemetry tracing
rt.EnableMetrics(); // Effect operation metrics
rt.EnableLogging(); // Structured logging on each effect
});
Code-driven and config-driven approaches compose — either path activates telemetry. A runtime with EnableTracing() called in code is traced regardless of the config value, and a global Enabled: true in config enables tracing on runtimes that didn't explicitly call EnableTracing().
Configuration Reference¶
The DeepstagingTelemetryOptions class is a [ConfigSection("Deepstaging:Telemetry")]:
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables tracing and metrics across all runtimes |
OtlpEndpoint |
string? |
null |
Overrides OTEL_EXPORTER_OTLP_ENDPOINT for the OTLP exporter |
The OtlpEndpoint property is marked [Secret] and should be stored in user-secrets for production deployments.
Tracing¶
ActivitySources¶
Each generated module creates a static ActivitySource named {Namespace}.{ModuleName}:
MyApp.OrderDispatch — dispatch commands/queries
MyApp.OrderStore — data store operations
MyApp.Handlers.OrderEventHandlers — integration event handlers
When tracing is enabled, the bootstrapper registers all sources with AddOpenTelemetry().WithTracing(...). Additionally, the Microsoft.AspNetCore source is registered to capture inbound HTTP request spans.
Effect Instrumentation¶
Every generated effect method is wrapped with .WithActivity(), which creates a span containing:
| Tag | Description |
|---|---|
otel.status_code |
Ok on success, Error on failure |
peer.service |
Destination service (for APM service maps) |
correlation.id |
From CorrelationContext, if available |
user.id |
From CorrelationContext, if available |
tenant.id |
From CorrelationContext, if available |
exception.type |
Exception type name (on failure) |
exception.message |
Exception message (on failure) |
When no ActivityListener is registered (tracing disabled), WithActivity() has zero overhead — no spans are created.
OTLP Export¶
The OTLP exporter reads its endpoint from the OTEL_EXPORTER_OTLP_ENDPOINT environment variable, which .NET Aspire sets automatically. For non-Aspire deployments, set it manually or use the OtlpEndpoint config property:
{
"Deepstaging": {
"Telemetry": {
"Enabled": true,
"OtlpEndpoint": "http://otel-collector:4317"
}
}
}
Metrics¶
When metrics are enabled, each runtime registers an EffectMetrics instance with three instruments:
| Instrument | Type | Unit | Description |
|---|---|---|---|
effects.succeeded |
Counter | operations | Successful effect executions |
effects.failed |
Counter | operations | Failed effect executions |
effects.duration |
Histogram | ms | Effect execution duration |
All instruments are tagged with the operation name for per-operation breakdown.
The meter is named Deepstaging.Effects and is registered with AddOpenTelemetry().WithMetrics(...).
Logging¶
When logging is enabled via EnableLogging(), each effect method logs:
- On success — structured log at
Informationlevel with operation name and duration - On failure — structured log at
Errorlevel with exception details
Logging uses the runtime's ILoggerFactory (resolved via IHasLoggerFactory). A dedicated logger category is created per module.
Aspire Integration¶
When running under .NET Aspire, observability works automatically:
- Aspire sets
OTEL_EXPORTER_OTLP_ENDPOINTpointing at the Aspire dashboard's collector Deepstaging:Telemetry:Enabled = trueactivates tracing + metrics- Traces appear in the Aspire dashboard under the Traces tab
- Each bounded context's spans are grouped by their
ActivitySourcename
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithLifetime(ContainerLifetime.Persistent);
var serviceBus = builder.AddAzureServiceBus("messaging")
.RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent))
.ConfigureServiceBusTopology();
builder.AddProject<Projects.eShop_AppHost>("api")
.WithReference(postgres.AddDatabase("eshopdb"))
.WithReference(serviceBus)
.WaitFor(postgres);
builder.Build().Run();
No ServiceDefaults project needed — Deepstaging handles the OpenTelemetry wiring internally.
Generated Code¶
The generated bootstrapper handles all OpenTelemetry registration. Here's what the config-driven path produces:
// After configure?.Invoke(options):
if (builder.Configuration.GetValue<bool>("Deepstaging:Telemetry:Enabled"))
{
options.Tracing = true;
options.Metrics = true; // only if runtime has instrumented modules
}
// Tracing registration:
if (options.Tracing)
{
services.AddOpenTelemetry().WithTracing(tracing => tracing
.AddSource("MyApp.OrderDispatch")
.AddSource("MyApp.OrderStore")
.AddSource("Microsoft.AspNetCore"));
Deepstaging.Telemetry.OtlpExporterSetup.EnsureConfigured(services);
}
// Metrics registration:
if (options.Metrics)
{
services.AddSingleton(new EffectMetrics("MyApp.OrderRuntime"));
services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Deepstaging.Effects"));
}
OtlpExporterSetup.EnsureConfigured() is a static once-guard — UseOtlpExporter() runs on the first call and is a no-op on subsequent calls from other runtimes.
Correlation Context¶
CorrelationContext provides request-scoped metadata that flows automatically through the entire pipeline — from HTTP endpoints, through dispatch handlers, into event queues, and out via outbound HTTP calls.
The Context¶
public sealed record CorrelationContext
{
public string? CorrelationId { get; set; } // Unique per request chain
public string? UserId { get; set; } // Authenticated user
public string? TenantId { get; set; } // Multi-tenant identifier
public string? CausationId { get; set; } // Command that caused this event
public IReadOnlyDictionary<string, string>? Metadata { get; set; }
public static CorrelationContext? Current { get; set; } // AsyncLocal<T> ambient
}
Propagation Flow¶
HTTP Request
│ Middleware extracts/creates CorrelationContext
│ Sets CorrelationContext.Current (AsyncLocal)
▼
Dispatch Handler
│ WithActivity reads context → enriches Activity span with:
│ correlation.id, user.id, tenant.id, causation.id
▼
Event Queue (if events enqueued)
│ EventQueueChannel captures CorrelationContext in EventEnvelope
│ ChannelWorker restores context before handler execution
▼
Outbound HTTP (if handler calls external APIs)
CorrelationDelegatingHandler sets headers:
ds-correlation-id, ds-causation-id, ds-tenant-id
Dual Mechanism¶
The context is available through two complementary mechanisms:
- Ambient (AsyncLocal) —
CorrelationContext.Currentis available anywhere without parameter threading. Used by middleware, delegating handlers, and hand-written code. - Explicit (runtime capability) —
IHasCorrelationContexton the runtime type. Used by generated effect methods that need compile-time guarantees.
Headers Propagated¶
| Header | Source | Description |
|---|---|---|
ds-correlation-id |
CorrelationContext.CorrelationId |
Traces a request across services |
ds-causation-id |
CorrelationContext.CausationId |
The command that caused this action |
ds-tenant-id |
CorrelationContext.TenantId |
Multi-tenant identifier |
These headers are set automatically by CorrelationDelegatingHandler on every outbound HTTP call made through generated HTTP clients.