SigNoz Cloud - This page is relevant for SigNoz Cloud editions.
Self-Host - This page is relevant for self-hosted SigNoz editions.

Manual Instrumentation for .NET

Manual instrumentation gives you programmatic control over OpenTelemetry in your .NET application. Use this approach when you need to create custom spans, add business-specific attributes, or when zero-code automatic instrumentation doesn't cover your use case.

Prerequisites

Step 1. Install dependencies

Add the required packages to your project:

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore

For manual instrumentation, the core dependency is System.Diagnostics.DiagnosticSource (included with .NET 5+).

Step 2. Create an ActivitySource

Create a dedicated class to hold your ActivitySource. This is your entry point for creating spans:

Instrumentation.cs
using System.Diagnostics;

public class Instrumentation : IDisposable
{
    public const string ActivitySourceName = "MyApp.OrderService";
    public const string ActivitySourceVersion = "1.0.0";

    public ActivitySource ActivitySource { get; }

    public Instrumentation()
    {
        ActivitySource = new ActivitySource(ActivitySourceName, ActivitySourceVersion);
    }

    public void Dispose()
    {
        ActivitySource.Dispose();
    }
}

Register it in your DI container:

Program.cs
builder.Services.AddSingleton<Instrumentation>();

Step 3. Configure OpenTelemetry

Set up the TracerProvider and register your ActivitySource:

Program.cs
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<Instrumentation>();

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("my-dotnet-service"))
    .WithTracing(tracing => tracing
        .AddSource(Instrumentation.ActivitySourceName) // Register your ActivitySource
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("https://ingest.<region>.signoz.cloud:443");
            options.Headers = "signoz-ingestion-key=<your-ingestion-key>";
        }));

Replace <region> and <your-ingestion-key> with your SigNoz Cloud values.

Step 4. Create custom spans

Use StartActivity() to create spans. The using pattern ensures spans are properly ended:

Services/OrderService.cs
using System.Diagnostics;

public class OrderService
{
    private readonly Instrumentation _instrumentation;

    public OrderService(Instrumentation instrumentation)
    {
        _instrumentation = instrumentation;
    }

    public async Task<Order> ProcessOrderAsync(string orderId)
    {
        // Start a new span
        using var activity = _instrumentation.ActivitySource.StartActivity("ProcessOrder");

        // Add attributes
        activity?.SetTag("order.id", orderId);
        activity?.SetTag("order.status", "processing");

        // Your business logic here
        var order = await FetchOrder(orderId);

        activity?.SetTag("order.total", order.Total);

        return order;
    }
}
Info

Use activity? (null-conditional) because StartActivity() returns null when no listeners are registered or the activity is sampled out.

Step 5. Create nested spans

Child spans automatically link to their parent when you start them within a parent's scope:

Services/DiceService.cs
public int RollDice()
{
    using var parentActivity = _instrumentation.ActivitySource.StartActivity("RollDice");

    int total = 0;
    for (int i = 0; i < 3; i++)
    {
        total += RollOnce();
    }

    parentActivity?.SetTag("dice.total", total);
    return total;
}

private int RollOnce()
{
    // This creates a child span under the parent
    using var childActivity = _instrumentation.ActivitySource.StartActivity("RollOnce");

    int result = Random.Shared.Next(1, 7);
    childActivity?.SetTag("dice.result", result);

    return result;
}

Step 6. Add events

Events mark specific moments within a span:

using var activity = _instrumentation.ActivitySource.StartActivity("ProcessPayment");

activity?.AddEvent(new ActivityEvent("PaymentInitiated"));

// Process payment...

activity?.AddEvent(new ActivityEvent("PaymentCompleted", DateTimeOffset.UtcNow,
    new ActivityTagsCollection
    {
        { "transaction.id", "txn_123456" },
        { "payment.method", "credit_card" }
    }));

Step 7. Record errors

Mark spans as failed and attach exception details:

using OpenTelemetry.Trace;

public async Task<Result> RiskyOperationAsync()
{
    using var activity = _instrumentation.ActivitySource.StartActivity("RiskyOperation");

    try
    {
        var result = await DoSomethingRisky();
        activity?.SetStatus(ActivityStatusCode.Ok);
        return result;
    }
    catch (Exception ex)
    {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        activity?.RecordException(ex);
        throw;
    }
}

The RecordException method is an extension method from OpenTelemetry.Trace that adds the exception as an event with stack trace details.

Step 8. Access the current span

Get the active span from anywhere in your code:

var currentActivity = Activity.Current;
currentActivity?.SetTag("additional.context", "some-value");
currentActivity?.AddEvent(new ActivityEvent("checkpoint-reached"));

This is useful for adding context from code that doesn't have direct access to the activity.

Validate

  1. Generate traffic by making requests to your application.
  2. Open SigNoz and go to Services.
  3. Look for your service name in the list.
  4. Click on your service to view traces, latency, and error rates.

Troubleshooting

Why don't my custom spans appear?

  • Verify your ActivitySource name matches what's registered with AddSource() in Program.cs
  • Ensure the TracerProvider is configured before your application starts handling requests
  • Check that traffic actually reaches the instrumented code paths

Why does StartActivity() return null?

  • No listener is registered for your ActivitySource (check AddSource() configuration)
  • The activity was sampled out by your sampler configuration
  • The TracerProvider wasn't set up correctly

How do I debug spans locally?

Add the console exporter:

dotnet add package OpenTelemetry.Exporter.Console
.WithTracing(tracing => tracing
    .AddSource(Instrumentation.ActivitySourceName)
    .AddConsoleExporter() // Prints spans to console
    .AddOtlpExporter(/* ... */));

Why are child spans not linked to parents?

  • Ensure you create child activities within the using block of the parent
  • Don't use Task.Run() or thread pool operations without propagating context
  • For async operations, the context flows automatically if you use async/await

Next steps

  • Setup alerts for your traces to get notified on errors and latency

Last updated: January 3, 2026

Edit on GitHub

Was this page helpful?