Prometheus & Grafana — Visual Dashboards for Your .NET API
Add prometheus-net to your ASP.NET Core API, expose metrics, build Grafana dashboards, and alert on p99 latency — complete with docker-compose.
Why Prometheus + Grafana?
Logs tell you what happened. Metrics tell you how your system is behaving right now — request rate, error rate, latency percentiles. Prometheus scrapes those numbers on a schedule; Grafana draws the graphs. Together they give you production visibility that health checks alone can't.
Install the Package
dotnet add package prometheus-net.AspNetCoreWire It Up
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpMetrics(); // records request duration, status codes, HTTP method
app.MapControllers();
app.MapMetrics(); // exposes /metrics endpoint for Prometheus to scrape
app.Run();UseHttpMetrics() must come before MapControllers() so it wraps every request.
Built-in ASP.NET Core Metrics
After wiring up, hit /metrics — you'll see:
http_requests_received_total{code="200",method="GET",controller="WeatherForecast"} 42
http_request_duration_seconds_bucket{le="0.1",...} 38
process_cpu_seconds_total 0.34
dotnet_total_memory_bytes 6291456Key metrics out of the box:
http_requests_received_total— request count by status + routehttp_request_duration_seconds— histogram (gives you p50/p95/p99)process_cpu_seconds_total— CPU usagedotnet_gc_collections_total— GC pressure
Custom Counters and Histograms
using Prometheus;
public class OrderService
{
private static readonly Counter OrdersCreated = Metrics
.CreateCounter("orders_created_total", "Number of orders created",
new CounterConfiguration { LabelNames = ["status"] });
private static readonly Histogram OrderProcessingDuration = Metrics
.CreateHistogram("order_processing_duration_seconds",
"Time to process an order",
new HistogramConfiguration
{
Buckets = Histogram.LinearBuckets(start: 0.01, width: 0.05, count: 10)
});
public async Task<Order> CreateOrderAsync(CreateOrderRequest req)
{
using var timer = OrderProcessingDuration.NewTimer();
try
{
var order = await _repo.SaveAsync(req);
OrdersCreated.WithLabels("success").Inc();
return order;
}
catch
{
OrdersCreated.WithLabels("failure").Inc();
throw;
}
}
}Use a Gauge for values that go up and down (queue depth, active connections):
private static readonly Gauge QueueDepth = Metrics
.CreateGauge("order_queue_depth", "Current items in processing queue");
// In your worker:
QueueDepth.Set(queue.Count);docker-compose Setup
# docker-compose.yml
services:
api:
build: .
ports:
- "5000:8080"
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheusprometheus.yml Scrape Config
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "dotnet-api"
static_configs:
- targets: ["api:8080"]
metrics_path: /metricsscrape_interval: 15s is a good default — aggressive enough for alerting, light enough on storage.
Grafana Dashboard
- Open
http://localhost:3000, loginadmin/admin - Add Prometheus data source: URL =
http://prometheus:9090 - Import dashboard ID 10427 (ASP.NET Core) from grafana.com — covers request rate, latency, GC, thread pool
Useful PromQL queries to build panels:
# Request rate (req/s) over 5 min window
rate(http_requests_received_total[5m])
# p99 latency
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# Error rate %
sum(rate(http_requests_received_total{code=~"5.."}[5m]))
/ sum(rate(http_requests_received_total[5m])) * 100Alerting on p99 Latency
In Grafana (Alerting → Alert Rules) or in a rules.yml for Prometheus:
# prometheus-rules.yml
groups:
- name: dotnet_api
rules:
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])) > 0.5
for: 2m
labels:
severity: warning
annotations:
summary: "p99 latency above 500ms for 2 minutes"
- alert: HighErrorRate
expr: |
sum(rate(http_requests_received_total{code=~"5.."}[5m]))
/ sum(rate(http_requests_received_total[5m])) > 0.05
for: 1m
labels:
severity: critical
annotations:
summary: "Error rate above 5%"Mount this file and reference it in prometheus.yml:
rule_files:
- "prometheus-rules.yml"Summary
prometheus-net.AspNetCore+UseHttpMetrics()+MapMetrics()— five minutes to working metrics- Custom counters/histograms give you business-level signals, not just HTTP stats
- The docker-compose stack runs locally; swap
static_configsfor service discovery (Kubernetes, Consul) in production - Set alerts on p99 latency and error rate — those two catch 90% of production incidents
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.