How to Create Custom Metrics in .NET Using OpenTelemetry + .NET Aspire Dashboard (Step-by-Step Guide)

How to Create Custom Metrics in .NET Using OpenTelemetry + .NET Aspire Dashboard (Step-by-Step Guide)

Sponsors

- Check out this insightful book on building CLI applications with C# and .NET. A must-read for any developer looking to master the craft!

Observability is no longer a luxury, it’s a must-have. And while logs and traces are essential, custom metrics are where real insight lives.

In this blog post, I’ll show you how to create custom metrics in .NET using OpenTelemetry and visualize them with the powerful .NET Aspire Dashboard. Whether you’re monitoring background jobs, queue lengths, or API performance, having tailored metrics helps you catch problems before your users do.

Ready to level up your observability game in a clean, scalable way? Let’s dive in.

Why You Should Create Custom Metrics

Out-of-the-box metrics are helpful, but they only tell part of the story. If you want to understand what really matters to your application, custom metrics are the way to go.

Here’s why:

  • You can track domain-specific behavior, like the number of emails sent, orders processed, or background jobs retried.

  • You get visibility over business-critical flows, not just infrastructure-level health.

  • They help you define SLOs and alerts based on your reality, not just CPU and memory.

Custom metrics bridge the gap between technical monitoring and business observability. They empower both developers and architects to make data-driven decisions.

How to Create Custom Metrics Using OpenTelemetry in .NET

Creating custom metrics in .NET with OpenTelemetry is easier than you think, and incredibly powerful when combined with the .NET Aspire Dashboard.

Let’s break it down:

1.  Configure OpenTelemetry in your Program.cs

Start by registering the services and defining the meters and counters:

C#
				builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService(DiagnosticsConfig.Name))
    .WithMetrics(metrics =>
    {
        metrics
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation();

        metrics.AddMeter(DiagnosticsConfig.Meter.Name);
        metrics.AddOtlpExporter();
    })
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddEntityFrameworkCoreInstrumentation();

        tracing.AddOtlpExporter();
    })
    .WithLogging(logging => logging.AddOtlpExporter());

			

By default, OpenTelemetry doesn’t track every Meter you define in your app. You need to explicitly register the ones you want to observe. In this case, DiagnosticsConfig.Meter.Name is “Notes”.

2. Define your custom metrics

In a separate class (e.g., DiagnosticsConfig.cs), define your custom meters:

C#
				public static class DiagnosticsConfig
{
    public const string Name = "Notes";

    public static Meter Meter = new(Name);

    public static Counter<int> NotesCounter = Meter.CreateCounter<int>("notes.count");
    public static Counter<int> NotesDescriptionTooLong = Meter.CreateCounter<int>("notes.description.too_long");
}
			

Each counter can be used to track specific business events, like the number of notes created or validation errors.

3. Use the metrics in your endpoints

Now just plug them into your API logic, for example, adding on your endpoint:

C#
				DiagnosticsConfig
    .NotesDescriptionTooLong
            .Add(1,
                new KeyValuePair<string, object?>("note.description", request.Description),
                new KeyValuePair<string, object?>("note.description.length", request.Description.Length));

//And
 DiagnosticsConfig.NotesCounter.Add(1, new KeyValuePair<string, object?>("note.id", note.Id));
			

You’re free to pass as many KeyValuePair<string, object?> as needed. These are extremely useful for filtering and grouping metrics later in the Dashboard.

C#
				app.MapPost("notes", async (
    [FromBody] CreateNoteRequest request, 
    AppDbContext context,
    ILogger<Program> logger,
    CancellationToken ct) =>
{
    if (request.Description.Length > CNotesLength.description)
    {
        logger.LogWarning("Invalid description length -> {Description} - {length}", request.Description, request.Description.Length);

        DiagnosticsConfig
            .NotesDescriptionTooLong
                .Add(
                    1,
                    new KeyValuePair<string, object?>("note.description", request.Description),
                    new KeyValuePair<string, object?>("note.description.length", request.Description.Length));

        return Results.BadRequest();
    }

    var note = new Note()
    {
        Description = request.Description,
    };

    await context.AddAsync(note);
    await context.SaveChangesAsync(ct);

    DiagnosticsConfig.NotesCounter.Add(1, new KeyValuePair<string, object?>("note.id", note.Id));

    logger.LogInformation("Note created with ID -> {Id}", note.Id);

    return Results.Ok(note);
});
			

With these counters, you’ll be able to visualize and filter metrics in the Aspire Dashboard or any backend supporting OTLP (like Prometheus, Grafana, or Azure Monitor).

Visualizing Custom Metrics in the .NET Aspire Dashboard

Once your custom metrics are up and running, the next step is seeing them come to life. The .NET Aspire Dashboard provides a beautiful UI to explore your data in real time.

1. Run the Aspire Dashboard with Docker (in seconds)

Docker
				services:
  notes.api:
    image: ${DOCKER_REGISTRY-}notesapi
    container_name: notes.api
    build:
      context: .
      dockerfile: Notes.API\Dockerfile
    ports:
      - "5000:5000"
      - "5001:5001"
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://notes.dashboard:18889
    networks:
      - otel

  notes.dashboard:
    image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
    container_name: notes.dashboard
    ports:
      - 18888:18888
    networks:
      - otel

  notes.database:
    image: postgres:latest
    container_name: notes.database
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://notes.dashboard:18889
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=NotesDb
    volumes:
      - notes-data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    networks:
      - otel

networks:
  otel:

volumes:
  notes-data:
			

After a few seconds, access http://localhost:18888 and your app’s metrics will start showing up automatically if it’s running and sending OTLP data.

2. See Your Custom Metrics in Action

Once connected, you can open the “Metrics” tab and look for the counters you defined.

notes side menu

Example 1 – Too Long Description
Below, we can see how many times users tried to create a note with an invalid description length:

too long

Example 2 – Notes Created
And here’s a chart showing how many notes have been successfully created over time:

count

You can filter metrics by name, tags (like note.id or note.description.length), and even build dashboards around them. Metrics are updated in real time, You’re not just tracking performance, you’re tracking business value, notes.count tells you how much your feature is being used. It’s a direct signal of user interaction.

Conclusion

In this guide, we’ve explored how to create custom metrics in a .NET application using OpenTelemetry and visualize them with the .NET Aspire Dashboard. By defining meaningful counters and enriching them with key-value tags, you can gain real-time insights into how your application behaves and how users interact with it.

With just a few lines of code and a simple Docker command, you can bring observability into your local development flow—making it easier to debug issues, monitor features, and improve the quality of your system.

You can check out the full code and implementation in my GitHub repository:

Observability .NET with OpenTelemetry

Thank you for reading.

See you next time!

Sponsors

- Check out this insightful book on building CLI applications with C# and .NET. A must-read for any developer looking to master the craft!

Share the Post:
plugins premium WordPress