Skip to content

.NET custom OpenTelemetry instrumentation

The automatic and framework instrumentation covers inbound requests and outbound HTTP calls. Custom instrumentation lets you record your own spans and metrics for the operations that matter to your service. .NET exposes these through the built-in System.Diagnostics APIs — ActivitySource for traces and Meter for metrics — and OpenTelemetry collects from them once you register the source.

This page assumes the SDK is already configured as in the ASP.NET Core guide. No extra NuGet package is required; ActivitySource and Meter ship with the runtime.

An ActivitySource is a named factory for spans (Activity is the .NET name for a span). Declare one per component, then start an activity around the work you want to trace.

  1. Declare a shared ActivitySource. The name identifies the source and is what you register with the SDK.

    using System.Diagnostics;
    public static class Telemetry
    {
    public const string ActivitySourceName = "MyCompany.MyApp";
    public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
    }
  2. Start an activity around the operation. Set tags for the attributes you want on the span, and set the status if the work fails.

    using System.Diagnostics;
    public class OrderService
    {
    public async Task ProcessOrderAsync(string orderId)
    {
    using var activity = Telemetry.ActivitySource.StartActivity("process-order");
    activity?.SetTag("order.id", orderId);
    try
    {
    await DoWorkAsync(orderId);
    }
    catch (Exception ex)
    {
    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
    throw;
    }
    }
    }
  3. Register the source with the tracer provider so its activities are exported.

    builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
    .AddSource(Telemetry.ActivitySourceName)
    .AddAspNetCoreInstrumentation()
    .AddOtlpExporter());

A Meter is a named factory for instruments — counters, histograms, and gauges. Declare one per component and create the instruments you need.

  1. Declare a Meter and its instruments.

    using System.Diagnostics.Metrics;
    public class OrderMetrics
    {
    public const string MeterName = "MyCompany.MyApp";
    private readonly Counter<long> _ordersProcessed;
    private readonly Histogram<double> _orderValue;
    public OrderMetrics(IMeterFactory meterFactory)
    {
    var meter = meterFactory.Create(MeterName);
    _ordersProcessed = meter.CreateCounter<long>("orders.processed");
    _orderValue = meter.CreateHistogram<double>("orders.value");
    }
    public void RecordOrder(double value)
    {
    _ordersProcessed.Add(1);
    _orderValue.Record(value);
    }
    }
  2. Register OrderMetrics for dependency injection, and register the meter with the SDK so its instruments are exported.

    builder.Services.AddSingleton<OrderMetrics>();
    builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
    .AddMeter(OrderMetrics.MeterName)
    .AddAspNetCoreInstrumentation()
    .AddOtlpExporter());
  3. Inject OrderMetrics where the work happens and record values.

    public class OrderService(OrderMetrics metrics)
    {
    public void CompleteOrder(double value)
    {
    metrics.RecordOrder(value);
    }
    }

AddSource and AddMeter accept the exact source and meter names. Call them once per name, or pass a wildcard suffix to match a prefix.

builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("MyCompany.MyApp")
.AddSource("MyCompany.Workers"))
.WithMetrics(metrics => metrics
.AddMeter("MyCompany.*"));

Once registered, custom spans and metrics flow to Osuite through the same OTLP exporter as the automatic instrumentation. See ASP.NET Core instrumentation for the exporter and environment configuration.