DEV Community

Kim Ipsen
Kim Ipsen

Posted on

Metrics and Console applications

Collecting Metrics in a .NET Console Application

In a previous post, I showed how to get started with metrics collection in a .NET application using the Aspire dashboard. That example was based on an ASP.NET Core Web API, where metrics and tracing feel almost effortless to enable thanks to the hosting model and built-in instrumentation.

But what if you’re working on a console application — a CLI tool, a batch processor, or a background job? Can you still use the same approach? The answer is yes, but with a slightly different setup. In this post, I’ll walk through how to add metrics to a console app, show two possible approaches (with and without a host), and explain why the setup differs from what you get in a web app.


Setting up the Aspire Dashboard

First, let’s set up the Aspire dashboard to receive metrics. If you followed along in my previous post, this part will look familiar.

You can run the Aspire dashboard locally with Docker:

# docker-compose.yml
services:
  aspire-dashboard:
    image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
    ports:
      - "18888:18888" # Dashboard UI
      - "18889:18889" # OTLP endpoint
Enter fullscreen mode Exit fullscreen mode

Start it with:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

The dashboard UI will be available at http://localhost:18888, and the OTLP endpoint for metrics will be available on http://localhost:18889.


Console apps: with or without a Host

There are actually two ways you can wire up metrics in a console application:

1. Without a Host

This is the most minimal approach. You create your Meter, Counter, and MeterProvider directly in Program.cs. This works well for small tools and keeps things lightweight.

using System.Diagnostics.Metrics;
using OpenTelemetry;
using OpenTelemetry.Metrics;

var meter = new Meter("PlainConsoleApp");
var counter = meter.CreateCounter<long>("records_processed");

using var meterProvider = Sdk.CreateMeterProviderBuilder()
    .AddMeter("PlainConsoleApp")
    .AddOtlpExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:18889");
    })
    .Build();

for (int i = 0; i < 5; i++)
{
    counter.Add(1);
    Console.WriteLine($"Processed record {i + 1}");
    await Task.Delay(500);
}
Enter fullscreen mode Exit fullscreen mode

This approach is straightforward, but you don’t get the conveniences of a hosting environment (like DI, config, or logging automatically wired up).


2. With a Host

If your console app grows more complex, you can use the Generic Host. This gives you a structure that’s closer to an ASP.NET Core app, making it easier to share setup patterns.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Metrics;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddOpenTelemetry()
            .WithMetrics(builder =>
            {
                builder
                    .AddMeter("HostConsoleApp")
                    .AddOtlpExporter(options =>
                    {
                        options.Endpoint = new Uri("http://localhost:18889");
                    });
            });

        services.AddHostedService<Worker>();
    })
    .Build();

await host.RunAsync();
Enter fullscreen mode Exit fullscreen mode

In this example, the Worker service can inject metrics and emit them as part of its work loop. This is more boilerplate, but it scales better if your app has multiple services, configuration needs, or background tasks.

👉 You can find both full examples in my GitHub repo: kimipsen/cli-app-metrics.


Why is the setup different from Web APIs?

If you’ve worked with ASP.NET Core before, you might have noticed that collecting metrics there feels almost effortless. You enable instrumentation, and suddenly you’ve got request counts, response times, and dependency calls showing up in your dashboard.

But when you switch to a console (CLI) app, things look a little different — and here’s why.

1. Hosting model

Web APIs run on the Generic Host, which comes with dependency injection, logging, metrics, and tracing all wired up for you.

Console apps don’t use a host unless you add one. They just run whatever you put in Main, which means there’s no prebuilt telemetry pipeline. That’s why you have to explicitly create and configure the MeterProvider (and optionally TracerProvider) yourself.

2. Application lifetime

A Web API is a long-running process, constantly handling requests. That makes continuous instrumentation natural.

A CLI tool, on the other hand, is often short-lived: it runs a job, then exits. Because of this, you need to make sure telemetry is flushed before the app shuts down (disposing the provider is key). Metrics in a console app are tied to the lifecycle of a single run.

3. Instrumentation sources

ASP.NET Core ships with automatic instrumentation for things like HttpClient, Entity Framework, and Kestrel (requests, connections).

Console apps don’t have a request pipeline — so you won’t get anything “for free.” Instead, you define your own metrics with counters, gauges, or histograms that match the logic of your CLI tool (for example: files processed, records imported, or errors encountered).

4. Different focus

In a web service, metrics are usually about observability — monitoring traffic, latency, and errors over time.

In a console app, metrics are usually about outcomes — how many items were processed, how long a run took, and whether it succeeded or failed. You’re measuring the job itself, not ongoing service health.


Here’s a quick side-by-side comparison:

Aspect ASP.NET Core (Web API) Console App (without Host) Console App (with Host)
Hosting Generic Host with built-in DI, logging, metrics No host by default, manual setup Generic Host, similar to ASP.NET Core
App Lifetime Long-lived, continuous Short-lived, often one-off Short-lived or long-running
Instrumentation Automatic (HTTP, EF Core, etc.) Manual (custom counters, histograms) Manual, but easier to extend via DI
Metric Focus Requests, latency, availability Task progress, job duration, outcomes Same, but can scale to complex apps

Wrapping up

Adding metrics to a console app is just as possible as in a web app — it just requires a bit more manual setup. The key differences come from the hosting model and application lifetime, which shape how you collect and flush telemetry.

The good news is that once you’ve wired up MeterProvider and added your custom counters, you can use the same Aspire dashboard and OTLP pipeline across both web and console apps.

That means you can monitor batch jobs, CLI utilities, and background workers with the same tools you’re already using for APIs and services.


👉 You can find the code for this example here: github.com/kimipsen/cli-app-metrics

Top comments (0)