Ever wondered why a perfectly designed Minimal API can still feel like a black box during a traffic surge?
Understanding the observability gap in .NET 8 Minimal APIs
.NET 8 introduced a leaner way to build HTTP endpoints, but the reduction in boilerplate also means fewer built‑in hooks for runtime insight. Without explicit instrumentation, you miss out on request latency, error rates, and custom business counters—metrics that are essential for SREs and developers alike. According to the 2023 Cloud Native Computing Survey, 68% of teams rely on Prometheus as their primary metrics store, yet only 34% have fully automated metric collection for their APIs.
Bridging this gap starts with OpenTelemetry, the vendor‑neutral standard that can emit metrics, traces, and logs from a single SDK. In .NET 8 you can wire the SDK directly into the Minimal API pipeline, keeping the codebase under 20 lines while delivering production‑grade data to Prometheus and visualizing it in Grafana.
Adding OpenTelemetry Metrics to a Minimal API
The first step is to reference the OpenTelemetry packages that target .NET 8. The OpenTelemetry.Exporter.Prometheus.AspNetCore package adds a Prometheus endpoint automatically, while OpenTelemetry.Metrics provides the meter API for custom counters and histograms.
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package OpenTelemetry.Metrics Next, modify Program.cs to configure the meter and the Prometheus exporter. The example below creates a Meter named MyApp.Metrics, registers a request duration histogram, and maps the /metrics endpoint that Prometheus will scrape.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
var builder = WebApplication.CreateBuilder(args);
// Register OpenTelemetry services
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MinimalApiDemo"))
.WithMetrics(metricsBuilder =>
metricsBuilder
.AddAspNetCoreInstrumentation()
.AddMeter("MyApp.Metrics")
.AddPrometheusExporter());
var app = builder.Build();
// Example custom metric
var meter = new System.Diagnostics.Metrics.Meter("MyApp.Metrics");
var requestCounter = meter.CreateCounter("requests_total", description: "Total number of HTTP requests");
var requestDuration = meter.CreateHistogram("request_duration_seconds", description: "Request processing time");
app.MapGet("/weather", (HttpContext ctx) =>
{
var start = System.Diagnostics.Stopwatch.GetTimestamp();
requestCounter.Add(1, new KeyValuePair("endpoint", "/weather"));
// Simulate work
Thread.Sleep(Random.Shared.Next(50, 200));
var elapsed = (System.Diagnostics.Stopwatch.GetTimestamp() - start) / (double)System.Diagnostics.Stopwatch.Frequency;
requestDuration.Record(elapsed, new KeyValuePair("endpoint", "/weather"));
return Results.Ok(new[] { "Sunny", "Rainy", "Cloudy" });
});
// Expose Prometheus endpoint
app.MapPrometheusScrapingEndpoint();
app.Run();
Notice the use of AddAspNetCoreInstrumentation(), which automatically captures request count, duration, and response size without extra code. The custom counters illustrate how you can enrich the data set with business‑specific values, such as feature usage or queue length.
Exporting metrics to Prometheus
After the application is running, Prometheus needs a scrape target. Add the following job definition to prometheus.yml:
scrape_configs:
- job_name: "minimal_api"
static_configs:
- targets: ["localhost:5000"]
Prometheus will call /metrics every 15 seconds by default, parse the OpenTelemetry‑generated exposition format, and store the time‑series. Verify the setup by visiting http://localhost:5000/metrics—you should see lines like # HELP request_duration_seconds Request processing time followed by numeric samples.
Visualizing data in Grafana
Grafana connects to Prometheus as a data source. Create a new dashboard and add a “Graph” panel with the query rate(request_duration_seconds_sum[1m]) / rate(request_duration_seconds_count[1m]) to display average latency. For request volume, use sum by (endpoint) (rate(requests_total[1m])). These panels update in real time, giving you immediate feedback on performance regressions.
Because the metrics are labeled with the endpoint tag, you can slice and dice traffic per route, spot outliers, and set up alert rules. For example, an alert that fires when latency exceeds 0.5 seconds for more than five consecutive minutes can be defined directly in Grafana’s alerting UI.
Production‑ready tips
1. **Limit cardinality** – Avoid high‑cardinality labels such as user IDs; they can explode the series count in Prometheus. Stick to static dimensions like endpoint, http_method, and status_code.
2. **Batch metric export** – The OpenTelemetry SDK batches observations by default, but you can fine‑tune the interval with builder.Services.Configure<PrometheusExporterOptions>(o => o.ScrapeResponseCacheDuration = TimeSpan.FromSeconds(10)); to reduce CPU load.
3. **Secure the endpoint** – Expose /metrics only on internal networks or protect it with basic auth. In Kubernetes, use a sidecar or an Ingress rule to restrict access.
4. **Version your metrics** – Prefix metric names with the application version (e.g., v1_requests_total) to avoid clashes when you deploy breaking changes.
By following these practices, you keep the observability pipeline lightweight while preserving the rich diagnostic data needed for modern DevOps workflows.
Sources
- Microsoft Docs: OpenTelemetry .NET
- Prometheus Documentation: Exporters and Scrape Configuration
- Grafana Labs: Building Dashboards for .NET Metrics
Author: Mahmut Sarıkaya — sarikayadev.com