<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kim Ipsen</title>
    <description>The latest articles on DEV Community by Kim Ipsen (@kimipsen).</description>
    <link>https://dev.to/kimipsen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3299143%2Fb622dcd9-ce42-4c8a-9599-027aa961687c.png</url>
      <title>DEV Community: Kim Ipsen</title>
      <link>https://dev.to/kimipsen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kimipsen"/>
    <language>en</language>
    <item>
      <title>Metrics and Console applications</title>
      <dc:creator>Kim Ipsen</dc:creator>
      <pubDate>Fri, 22 Aug 2025 20:02:51 +0000</pubDate>
      <link>https://dev.to/kimipsen/metrics-and-console-applications-212l</link>
      <guid>https://dev.to/kimipsen/metrics-and-console-applications-212l</guid>
      <description>&lt;h1&gt;
  
  
  Collecting Metrics in a .NET Console Application
&lt;/h1&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/kimipsen/getting-started-using-metrics-1hfo"&gt;previous post&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;yes&lt;/strong&gt;, 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up the Aspire Dashboard
&lt;/h2&gt;

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

&lt;p&gt;You can run the Aspire dashboard locally with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;aspire-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/dotnet/aspire-dashboard:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;18888:18888"&lt;/span&gt; &lt;span class="c1"&gt;# Dashboard UI&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;18889:18889"&lt;/span&gt; &lt;span class="c1"&gt;# OTLP endpoint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard UI will be available at &lt;a href="http://localhost:18888" rel="noopener noreferrer"&gt;http://localhost:18888&lt;/a&gt;, and the OTLP endpoint for metrics will be available on &lt;code&gt;http://localhost:18889&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Console apps: with or without a Host
&lt;/h2&gt;

&lt;p&gt;There are actually two ways you can wire up metrics in a console application:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Without a Host
&lt;/h3&gt;

&lt;p&gt;This is the most minimal approach. You create your &lt;code&gt;Meter&lt;/code&gt;, &lt;code&gt;Counter&lt;/code&gt;, and &lt;code&gt;MeterProvider&lt;/code&gt; directly in &lt;code&gt;Program.cs&lt;/code&gt;. This works well for small tools and keeps things lightweight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Diagnostics.Metrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;OpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;OpenTelemetry.Metrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Meter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlainConsoleApp"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateCounter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"records_processed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;meterProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateMeterProviderBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlainConsoleApp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:18889"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Processed record &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h3&gt;
  
  
  2. With a Host
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;OpenTelemetry.Metrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IHost&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefaultBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;builder&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HostConsoleApp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:18889"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the &lt;code&gt;Worker&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;👉 You can find both full examples in my GitHub repo: &lt;a href="https://github.com/kimipsen/cli-app-metrics" rel="noopener noreferrer"&gt;kimipsen/cli-app-metrics&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why is the setup different from Web APIs?
&lt;/h2&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;But when you switch to a console (CLI) app, things look a little different — and here’s why.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hosting model
&lt;/h3&gt;

&lt;p&gt;Web APIs run on the &lt;strong&gt;Generic Host&lt;/strong&gt;, which comes with dependency injection, logging, metrics, and tracing all wired up for you.  &lt;/p&gt;

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

&lt;h3&gt;
  
  
  2. Application lifetime
&lt;/h3&gt;

&lt;p&gt;A Web API is a &lt;strong&gt;long-running process&lt;/strong&gt;, constantly handling requests. That makes continuous instrumentation natural.  &lt;/p&gt;

&lt;p&gt;A CLI tool, on the other hand, is often &lt;strong&gt;short-lived&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Instrumentation sources
&lt;/h3&gt;

&lt;p&gt;ASP.NET Core ships with &lt;strong&gt;automatic instrumentation&lt;/strong&gt; for things like &lt;code&gt;HttpClient&lt;/code&gt;, Entity Framework, and Kestrel (requests, connections).  &lt;/p&gt;

&lt;p&gt;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: &lt;em&gt;files processed&lt;/em&gt;, &lt;em&gt;records imported&lt;/em&gt;, or &lt;em&gt;errors encountered&lt;/em&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Different focus
&lt;/h3&gt;

&lt;p&gt;In a web service, metrics are usually about &lt;strong&gt;observability&lt;/strong&gt; — monitoring traffic, latency, and errors over time.  &lt;/p&gt;

&lt;p&gt;In a console app, metrics are usually about &lt;strong&gt;outcomes&lt;/strong&gt; — 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.&lt;/p&gt;




&lt;p&gt;Here’s a quick side-by-side comparison:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;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.  &lt;/p&gt;

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

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




&lt;p&gt;👉 You can find the code for this example here: &lt;a href="https://github.com/kimipsen/cli-app-metrics" rel="noopener noreferrer"&gt;github.com/kimipsen/cli-app-metrics&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devcontainer</category>
      <category>telemetry</category>
      <category>devops</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Getting started using metrics</title>
      <dc:creator>Kim Ipsen</dc:creator>
      <pubDate>Tue, 01 Jul 2025 19:59:37 +0000</pubDate>
      <link>https://dev.to/kimipsen/getting-started-using-metrics-1hfo</link>
      <guid>https://dev.to/kimipsen/getting-started-using-metrics-1hfo</guid>
      <description>&lt;h1&gt;
  
  
  Getting Started Using the Aspire Dashboard for Metrics, Traces, and Logs
&lt;/h1&gt;

&lt;p&gt;I was recently asked to create and publish a simple repository showcasing how to use the &lt;strong&gt;standalone Aspire dashboard&lt;/strong&gt; for collecting &lt;strong&gt;metrics&lt;/strong&gt;, &lt;strong&gt;traces&lt;/strong&gt;, and &lt;strong&gt;structured logs&lt;/strong&gt;—all using OpenTelemetry standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🔗 Find the full repo here: &lt;a href="https://github.com/kimipsen/aspire-metrics" rel="noopener noreferrer"&gt;github.com/kimipsen/aspire-metrics&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Aspire?
&lt;/h2&gt;

&lt;p&gt;This project was born out of frustration while wrestling with the Elastic stack to create a local observability environment. One of the biggest pain points? 🔐 Managing logins across multiple services just to get basic logs flowing.&lt;/p&gt;

&lt;p&gt;Cue &lt;a href="https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire/" rel="noopener noreferrer"&gt;&lt;strong&gt;Aspire&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Aspire makes it incredibly easy to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spin up dev containers for your services&lt;/li&gt;
&lt;li&gt;Wire them together&lt;/li&gt;
&lt;li&gt;Collect observability data (logs, traces, metrics)&lt;/li&gt;
&lt;li&gt;View it all in a single, clean dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All using &lt;strong&gt;OpenTelemetry&lt;/strong&gt; standards 🔭🎉&lt;/p&gt;


&lt;h2&gt;
  
  
  Use Case: Standalone Dashboard Only
&lt;/h2&gt;

&lt;p&gt;Usually, Aspire is used as a fully configured project with multiple services defined in code. But in my case, I &lt;strong&gt;only needed the dashboard&lt;/strong&gt;. I didn’t want Aspire to control or define my services—I wanted my existing &lt;code&gt;devcontainer&lt;/code&gt; setup to remain untouched.&lt;/p&gt;

&lt;p&gt;Luckily, Aspire supports running the dashboard as a &lt;strong&gt;standalone container&lt;/strong&gt;, which still supports everything I need: metrics, traces, and structured logs via OTLP.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configuring the Dashboard (Docker Compose)
&lt;/h2&gt;

&lt;p&gt;Setup is refreshingly simple—no login-sharing between services, no complex collectors.&lt;/p&gt;

&lt;p&gt;Here’s the full &lt;code&gt;docker-compose.yaml&lt;/code&gt; snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;aspire&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/dotnet/aspire-dashboard:latest&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;18888:18888&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;18889:18889&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🤔 Wait—didn’t you say no logins?&lt;br&gt;
Why set ALLOW_ANONYMOUS if I want authentication?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good catch! This just controls access to the dashboard UI, not communication between services. If you prefer password protection, remove the ALLOW_ANONYMOUS line. The login token will be shown in the Aspire container logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔌 About the Ports
&lt;/h2&gt;

&lt;p&gt;The Aspire dashboard container exposes two ports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18888&lt;/strong&gt; — Serves the dashboard UI, where you can explore traces, metrics, and logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18889&lt;/strong&gt; — The OTLP (OpenTelemetry Protocol) ingestion endpoint used by your services to send telemetry data (spans, metrics, and logs).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running multiple services, just point their OTLP exporters to &lt;code&gt;http://localhost:18889&lt;/code&gt; (or the appropriate container network name), and Aspire will start receiving data automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Instrumenting a .NET 8 Web API
&lt;/h2&gt;

&lt;p&gt;To demonstrate end-to-end observability, I built a small .NET 8 Web API with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EF Core&lt;/li&gt;
&lt;li&gt;Npgsql (PostgreSQL)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenTelemetry is added in just ~25 lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetResourceBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ResourceBuilder&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAspNetCoreInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClientInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEntityFrameworkCoreInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNpgsql&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAspNetCoreInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClientInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNpgsqlInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOtlpExporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You migth be wondering how I'm specifying where my collector is located, how OpenTelemetry knows how to use the aspire dashboard for collecting metrics and traces. &lt;/p&gt;

&lt;p&gt;I've configured this part in the docker-compose.yml file. By specifying the 2 environment variables &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; and &lt;code&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/code&gt;, OpenTelemetry will pick up on this configuration when debugging the .net application.&lt;/p&gt;

&lt;p&gt;If I wanted to customize the configuration to send my metrics somewhere else, eg. a central server such as Jaeger, I could then customize my .net application using either code (in the Program.cs file) or appsettings.json. This would then override the environment variables from the container.&lt;/p&gt;

&lt;p&gt;✅ This config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sends traces and metrics via the OTLP exporter&lt;/li&gt;
&lt;li&gt;Adds common instrumentation: ASP.NET Core, HTTP clients, EF Core, Npgsql&lt;/li&gt;
&lt;li&gt;Can be easily extracted into a reusable extension method&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;If you want to go deeper—create custom metrics or spans, enrich telemetry with custom properties—I highly recommend checking out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📚 &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel" rel="noopener noreferrer"&gt;.NET OpenTelemetry docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If you're tired of setting up complex stacks just to monitor a few services, the Aspire dashboard offers a great alternative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Lightweight&lt;/li&gt;
&lt;li&gt;✅ Standards-based&lt;/li&gt;
&lt;li&gt;✅ Works out-of-the-box with OpenTelemetry&lt;/li&gt;
&lt;li&gt;✅ Easy to run standalone&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🚀 I’d love to hear what you build with Aspire or how you're using OpenTelemetry in your .NET projects—feel free to share your thoughts in the comments!&lt;/p&gt;

</description>
      <category>devcontainer</category>
      <category>telemetry</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
