.NET & C# Development · Lesson 85 of 92

Prometheus + Grafana — Build a Live Dashboard for Your API

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

Bash
dotnet add package prometheus-net.AspNetCore

Wire It Up

C#
// 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 6291456

Key metrics out of the box:

  • http_requests_received_total — request count by status + route
  • http_request_duration_seconds — histogram (gives you p50/p95/p99)
  • process_cpu_seconds_total — CPU usage
  • dotnet_gc_collections_total — GC pressure

Custom Counters and Histograms

C#
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):

C#
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

YAML
# 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:
      - prometheus

prometheus.yml Scrape Config

YAML
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "dotnet-api"
    static_configs:
      - targets: ["api:8080"]
    metrics_path: /metrics

scrape_interval: 15s is a good default — aggressive enough for alerting, light enough on storage.

Grafana Dashboard

  1. Open http://localhost:3000, login admin/admin
  2. Add Prometheus data source: URL = http://prometheus:9090
  3. Import dashboard ID 10427 (ASP.NET Core) from grafana.com — covers request rate, latency, GC, thread pool

Useful PromQL queries to build panels:

PROMQL
# 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])) * 100

Alerting on p99 Latency

In Grafana (Alerting → Alert Rules) or in a rules.yml for Prometheus:

YAML
# 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:

YAML
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_configs for service discovery (Kubernetes, Consul) in production
  • Set alerts on p99 latency and error rate — those two catch 90% of production incidents