07-production-observability

Grafana Dashboards: From Metrics to an Actual Dashboard

Turn the Micrometer metrics from the last guide into a real Grafana dashboard — token throughput, latency percentiles, cost estimates, active streams, and the alerts that page someone before a customer complains.

August 13, 2026
spring-aigrafanaprometheusdashboardalertspromql

Metrics Nobody Looks At

/actuator/prometheus has been scrapeable since the last guide. Nobody at Acme Fintech has looked at it once — raw Prometheus output is not something a support-team lead or an on-call engineer opens voluntarily. This guide is the last step: turning those metrics into a dashboard someone actually glances at, and alerts that page someone before a customer complaint is the first signal anything's wrong.


1. Local Setup

yaml
# docker-compose.yml — Prometheus + Grafana, pointed at your app
services:
  prometheus:
    image: prom/prometheus:latest
    ports: ["9090:9090"]
    volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"]
  grafana:
    image: grafana/grafana:latest
    ports: ["3000:3000"]
    depends_on: [prometheus]
yaml
# prometheus.yml
scrape_configs:
  - job_name: acme-support-agent
    metrics_path: /actuator/prometheus
    scrape_interval: 15s
    static_configs:
      - targets: ["host.docker.internal:8080"]

Add Prometheus as a Grafana data source pointing at http://prometheus:9090, and every panel below is a PromQL query away.


2. Token Throughput

promql
sum(rate(gen_ai_client_token_usage_total{gen_ai_token_type="output"}[5m])) by (gen_ai_request_model)

rate(...[5m]) turns a cumulative counter into "tokens per second over the last 5 minutes," broken out per model — the panel that shows, at a glance, whether the support agent is still mostly running on Ollama in staging or has shifted to a hosted provider in production.


3. Latency Percentiles

Micrometer's Timer doesn't expose real percentiles to Prometheus by default — gen_ai_client_operation_seconds_max gives you the worst case, not p99. Turn on histogram buckets for this specific metric to get real quantiles:

yaml
# application.yml
management:
  metrics:
    distribution:
      percentiles-histogram:
        gen_ai.client.operation.duration: true
promql
histogram_quantile(0.99,
  sum(rate(gen_ai_client_operation_seconds_bucket[5m])) by (le, gen_ai_request_model)
)
⚠️

percentiles-histogram adds real cardinality — every configured bucket becomes its own time series, multiplied by every label combination (model, operation type). Enable it deliberately for the specific meters you'll actually chart, not blanket-enabled for every metric in the application, or your Prometheus storage grows a lot faster than the dashboard's value justifies.


4. Error Rate

Spring AI's gen_ai metrics track successful call duration and token usage; they don't ship a ready-made error-rate dimension. Track it the same way this phase's Micrometer guide added support.escalations — a custom counter around the call:

java
try {
    return chatClient.prompt().user(message).call().content();
} catch (Exception e) {
    aiCallErrors.increment(); // Counter, registered once in the constructor
    throw e;
}
promql
sum(rate(ai_call_errors_total[5m])) / sum(rate(gen_ai_client_operation_seconds_count[5m]))

Errors over total calls, as a ratio — the panel that answers "is the provider having a bad day" before enough customers complain to make it obvious.


5. Active Streams and Cost Estimate

Straight from the custom gauge this phase's Micrometer guide already registered:

promql
support_streams_active

Cost is always an estimate — Grafana doesn't know your contract's actual per-token pricing, so encode it as a constant in the query:

promql
(sum(rate(gen_ai_client_token_usage_total{gen_ai_token_type="input"}[1h])) * 0.00015
 + sum(rate(gen_ai_client_token_usage_total{gen_ai_token_type="output"}[1h])) * 0.0006) * 3600

That's (input tokens/sec × price-per-token + output tokens/sec × price-per-token) × 3600, giving an estimated dollars-per-hour panel — useful for spotting a spike, not a substitute for your provider's actual billing dashboard, which remains the source of truth.


6. Alerts

Three alerts cover most of what actually pages someone, expressed as Grafana alert rule conditions:

AlertConditionWhy
Latency p99 too highhistogram_quantile(0.99, ...) > 5 (seconds) for 5mCustomers are staring at spinners even with streaming's improved perceived latency
Error rate too highError ratio query (section 4) > 0.01 for 5mA provider outage or a broken integration, not isolated bad luck
Token budget exceededsum(increase(gen_ai_client_token_usage_total[1d])) > <daily budget>Runaway cost — a bug causing retry storms, or usage genuinely outgrowing the current budget
yaml
# Grafana alert rule (simplified)
- alert: SupportAgentLatencyP99High
  expr: histogram_quantile(0.99, sum(rate(gen_ai_client_operation_seconds_bucket[5m])) by (le)) > 5
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Support agent p99 latency above 5s"

7. Correlating With Logs

When an alert fires, the dashboard tells you that something's wrong; a log panel filtered by trace ID tells you what. Add a Loki (or equivalent) log panel to the same dashboard, filtered by the trace IDs the previous guide's tracing setup already attaches to every request — click a slow point on the latency panel, jump straight to the exact request's full log trail, including which advisor or tool call in the chain was actually slow, without grepping.


What's Next

Dashboards and alerts are watching. The next guide turns to the other side of production readiness: actually controlling what those dashboards are measuring — prompt caching, guardrails, and a model-router that keeps cost from growing unbounded in the first place.

Frequently asked questions

Should every metric get a percentiles-histogram, just in case?

No — this guide's cardinality warning applies broadly. Enable it for the handful of metrics you'll genuinely chart percentiles for (latency is the obvious one), and rely on sum/count/max or your own targeted counters for everything else.

Is the cost-estimate panel accurate enough to reconcile against an actual invoice?

No, and it shouldn't be treated that way — it's a rough, real-time signal for catching spikes, built from constants you maintain by hand and that go stale the moment pricing changes. Reconcile actual spend against your provider's billing dashboard or API, and treat this panel as an early-warning system, not an accounting source of truth.

Do I need Grafana specifically, or does this work with other dashboard tools?

The underlying metrics are standard Prometheus format regardless of what visualizes them — Datadog, New Relic, or any Prometheus-compatible backend can consume the exact same /actuator/prometheus endpoint and PromQL-equivalent queries this guide describes. Grafana is simply a common, self-hostable default.

How long should I retain these metrics?

Long enough to compare week-over-week trends, which for a support agent usually means at least 30 days, ideally longer for seasonal patterns — Prometheus itself isn't meant for long-term storage though, and most production setups pair it with a long-term store (Thanos, Mimir, or a hosted equivalent) once retention needs outgrow local disk on the Prometheus instance itself.