OpenTelemetry integration for NPipeline observability - provides seamless integration with OpenTelemetry SDKs for distributed tracing
$ dotnet add package NPipeline.Extensions.Observability.OpenTelemetryOpenTelemetry integration for NPipeline observability - provides seamless integration with OpenTelemetry SDKs for distributed tracing.
This package enables NPipeline pipelines to export traces to OpenTelemetry-compatible backends such as Jaeger, Zipkin, Azure Monitor, AWS X-Ray, and others. It
implements the IPipelineTracer interface using .NET's System.Diagnostics.ActivitySource and Activity APIs, following OpenTelemetry best practices for .NET
applications.
ActivitySource pattern for compatibility with all OpenTelemetry exportersActivity.Current context for hierarchical tracingdotnet add package NPipeline.Extensions.Observability.OpenTelemetry
using Microsoft.Extensions.DependencyInjection;
using NPipeline.Extensions.Observability.OpenTelemetry;
using NPipeline.Observability.DependencyInjection;
// Register observability services
var services = new ServiceCollection();
services.AddNPipelineObservability();
services.AddOpenTelemetryPipelineTracer("MyPipeline");
// Configure OpenTelemetry export to Jaeger
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddJaegerExporter()
.Build();
var serviceProvider = services.BuildServiceProvider();
// Get the tracer and context factory
var tracer = serviceProvider.GetRequiredService<IPipelineTracer>();
var contextFactory = serviceProvider.GetRequiredService<IObservablePipelineContextFactory>();
// Create a context with tracing enabled
await using var context = contextFactory.Create(
PipelineContextConfiguration.WithObservability(tracer: tracer)
);
// Run your pipeline - traces are automatically exported
var runner = serviceProvider.GetRequiredService<IPipelineRunner>();
await runner.RunAsync<MyPipeline>(context);
The main tracer implementation that creates Activity instances from an ActivitySource.
Why this design: Using ActivitySource is the recommended OpenTelemetry pattern for .NET. It ensures activities are captured by providers configured with
AddSource(serviceName), providing automatic integration with all OpenTelemetry exporters.
var tracer = new OpenTelemetryPipelineTracer("MyPipeline");
// The tracer creates activities with the service name as the source
var activity = tracer.StartActivity("ProcessOrder");
// Activity is automatically exported to configured OpenTelemetry backends
Key behaviors:
ActivitySource named after the serviceActivity.CurrentActivitySource when the tracer is disposedFluent extension methods for configuring OpenTelemetry to capture NPipeline traces.
Configures the tracer provider to listen for activities from a specific NPipeline service.
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline") // Must match tracer service name
.AddJaegerExporter()
.Build();
Why this is needed: The service name used when creating OpenTelemetryPipelineTracer must match the source name added to the tracer provider. This
extension ensures consistency and prevents configuration errors.
Configures multiple NPipeline services at once.
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSources(new[] { "PipelineA", "PipelineB", "PipelineC" })
.AddZipkinExporter()
.Build();
Use case: Ideal for microservices architectures where multiple pipeline services share a single OpenTelemetry configuration.
Extracts NPipeline-specific metadata from activities for custom exporters or processors.
public class CustomExporter : BaseExporter<Activity>
{
public override ExportResult Export(in Batch<Activity> batch)
{
foreach (var activity in batch)
{
var info = activity.GetNPipelineInfo();
if (info != null)
{
Console.WriteLine($"Service: {info.ServiceName}, Activity: {info.ActivityName}");
}
}
return ExportResult.Success;
}
}
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddJaegerExporter(o =>
{
o.AgentHost = "localhost";
o.AgentPort = 6831;
})
.Build();
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddZipkinExporter(o =>
{
o.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
})
.Build();
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://localhost:4317");
})
.Build();
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddAzureMonitorTraceExporter(o =>
{
o.ConnectionString = "InstrumentationKey=your-key";
})
.Build();
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddAWSXRayTraceExporter(o =>
{
o.SetResourceResourceDetector(new AWSEBSDetector());
})
.Build();
Pipeline Execution
↓
OpenTelemetryPipelineTracer.StartActivity()
↓
ActivitySource.StartActivity(name)
↓
Activity created (or null if sampled out)
↓
PipelineActivity wrapper (or NullPipelineActivity)
↓
Exported to configured OpenTelemetry backend
The service name serves as the bridge between the tracer and the exporter:
new OpenTelemetryPipelineTracer("MyPipeline")builder.AddNPipelineSource("MyPipeline")Source.Name = "MyPipeline"If these don't match, traces won't be captured. The extension methods enforce this contract.
When OpenTelemetry sampling drops an activity (returns null from StartActivity), the tracer automatically falls back to NullPipelineTracer.Instance. This
design choice:
PipelineActivity wrapper is created for dropped activities// Good: Service name is a constant
const string ServiceName = "OrderProcessingPipeline";
services.AddOpenTelemetryPipelineTracer(ServiceName);
builder.AddNPipelineSource(ServiceName);
// Register the tracer as a singleton
services.AddSingleton<IPipelineTracer>(sp =>
new OpenTelemetryPipelineTracer("MyPipeline"));
// Ensure proper disposal of the tracer provider
await using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.AddJaegerExporter()
.Build();
try
{
// Run pipelines
}
finally
{
// Tracer provider is disposed automatically
}
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.SetSampler(new TraceIdRatioBasedSampler(0.1)) // 10% sampling
.AddJaegerExporter()
.Build();
using var tracerProvider = new TracerProviderBuilder()
.AddNPipelineSource("MyPipeline")
.ConfigureResource(r => r
.AddService("MyPipeline", "1.0.0")
.AddAttributes(new Dictionary<string, object>
{
["environment"] = "production",
["deployment.region"] = "us-west-2"
}))
.AddJaegerExporter()
.Build();
PipelineActivity wrappersProblem: Traces are not visible in Jaeger, Zipkin, or other backends.
Solutions:
Problem: StartActivity returns null activities.
Solutions:
ActivitySource is registered with the tracer providerOpenTelemetryPipelineTracer is properly initializedProblem: Tracing causes increased memory consumption.
Solutions:
MIT License - see LICENSE file for details.