10-production-engineering-observability

Monitoring and Alerting: Prometheus, Grafana, Golden Signals

A staff-engineer guide to Prometheus scraping, PromQL, Grafana dashboards, golden signals, and alert design that avoids fatigue.

August 14, 2026
backend-engineerprometheusgrafanaalertinggolden-signalspromql

Monitoring and Alerting Basics

Collecting metrics is the easy part — Micrometer and Actuator do most of that work for you. The hard part, and the part that actually determines whether your on-call rotation is sustainable, is deciding what to look at, what to alert on, and how to word an alert so the person paged at 3am knows what to do in the first ten seconds of reading it. This guide covers the Prometheus/Grafana stack end to end and, more importantly, the discipline of designing dashboards and alerts that surface real problems without training your team to ignore pages.


1. How Prometheus Scraping Works

Prometheus operates on a pull model: it periodically scrapes an HTTP endpoint on each target and stores whatever numeric samples it finds, each tagged with labels, as a time series. This is the opposite of most logging pipelines, which push data outward — the target doesn't send anything anywhere, it just exposes current values whenever asked.

Making a Spring Boot service scrapable

xml
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
properties
management.endpoints.web.exposure.include=health,prometheus
management.metrics.tags.application=${spring.application.name}
yaml
# prometheus.yml
scrape_configs:
  - job_name: 'spring-boot-services'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 15s
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: app
💡

In Kubernetes, Prometheus almost never scrapes static hostnames — it uses service discovery (kubernetes_sd_configs) to automatically find pods as they're created and destroyed, filtered by annotations like prometheus.io/scrape: "true". This is why you annotate pod specs rather than hand-maintain a scrape target list.

Scrape interval trade-offs

IntervalResolutionStorage costWhen
5-10sHigh — catches brief spikesHighCritical low-latency services, small fleets
15-30sStandardModerateMost production services (Prometheus default is 15s scrape / evaluation)
60s+Coarse — can miss short-lived spikesLowLow-priority background services, cost-sensitive at scale

2. PromQL Basics

PromQL is Prometheus's query language for slicing and aggregating time series. A working knowledge of a handful of functions covers the vast majority of production dashboards and alerts.

promql
# Raw counter value (rarely useful directly — counters only go up)
http_server_requests_seconds_count{uri="/orders", method="POST"}
 
# Per-second rate over a 5-minute window — the actual useful signal
rate(http_server_requests_seconds_count{uri="/orders"}[5m])
 
# Error rate as a percentage of total requests
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
* 100
 
# p99 latency from a histogram
histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket{uri="/orders"}[5m])) by (le)
)
 
# Connection pool saturation
hikaricp_connections_pending{pool="HikariPool-1"} > 0
FunctionPurpose
rate()Per-second average rate of increase for a counter over a time window — always use this, never graph a raw counter
sum() by (label)Aggregate across instances/pods while preserving one dimension (e.g., per-endpoint)
histogram_quantile()Compute a percentile (p50/p95/p99) from histogram bucket data
increase()Total increase over a window (e.g., total errors in the last hour)
irate()Instantaneous rate from the last two data points — spikier, use for fast-moving graphs, not alerts
⚠️

Never graph or alert on a raw counter value — it only ever goes up (or resets to 0 on restart), so the number itself is meaningless without a rate. Always wrap counters in rate() or increase(). This is the single most common PromQL mistake for engineers new to Prometheus.


3. Grafana Dashboards

Grafana visualizes Prometheus (and other) data sources. The failure mode to avoid is the "wall of graphs" dashboard — 40 panels nobody reads during an incident because nobody can tell which ones matter in the first five seconds.

Dashboard design principles

PrincipleWhy
Top row = golden signals for this serviceThe four panels that answer "is this service healthy right now" at a glance
One dashboard per service, not one giant dashboardFaster to load, faster to reason about, ownership is clear
Consistent time ranges and label filters across panelsComparing panels side by side only works if they're looking at the same window
Link dashboards to traces/logsA latency panel should let you click through to the relevant traces, not just show a number
Use template variables for environment/region/instanceOne dashboard reused across prod/staging, not copy-pasted per environment
json
{
  "title": "Order Service — Overview",
  "templating": {
    "list": [
      { "name": "environment", "type": "query", "query": "label_values(environment)" }
    ]
  },
  "panels": [
    { "title": "Request Rate", "targets": [{ "expr": "sum(rate(http_server_requests_seconds_count{application=\"order-service\", environment=\"$environment\"}[5m]))" }] },
    { "title": "Error Rate %", "targets": [{ "expr": "sum(rate(http_server_requests_seconds_count{application=\"order-service\", status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"order-service\"}[5m])) * 100" }] },
    { "title": "p99 Latency", "targets": [{ "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application=\"order-service\"}[5m])) by (le))" }] },
    { "title": "CPU / Memory Saturation", "targets": [{ "expr": "process_cpu_usage{application=\"order-service\"}" }] }
  ]
}

Build one fleet-level overview dashboard and one per-service dashboard, and make sure every alert links directly to the specific dashboard panel relevant to it. An alert that says "error rate high" with no link to a dashboard sends the on-call engineer hunting for the right graph while the incident continues.


4. The Four Golden Signals

Google's SRE book distills what to monitor for any service down to four signals. If you only have bandwidth to build four dashboard panels and four alert rules, build these.

SignalWhat to actually measureCommon pitfall
Latencyp50/p95/p99, separately for success and error responsesAveraging in fast error responses (e.g., instant 400s) makes overall latency look artificially good
TrafficRequests/sec, messages consumed/sec, active connectionsIgnoring traffic shape (bursty vs steady) when setting thresholds
Errors5xx rate, but also business-logic failures that return 200 (e.g., "payment declined" wrapped in a 200 response)Only counting HTTP status codes and missing semantic failures
SaturationConnection pool pending, thread pool queue depth, CPU throttling, heap usageAlerting on CPU % alone without checking if it's actually causing latency impact
🚨

Always separate error-response latency from success-response latency. A service returning fast 500s during an outage can make your "average latency" dashboard look better during the incident, because failed requests return almost instantly while successful ones were slow. Measure latency conditioned on outcome, not blended across all responses.


5. Designing Alerts That Don't Cause Fatigue

The purpose of an alert is to get a human to take an action they wouldn't have taken otherwise. Every alert that pages someone with nothing actionable to do erodes trust in the entire alerting system — eventually people start silencing channels instead of reading pages, which is how real incidents get missed.

Symptom-based vs cause-based alerting

⚠️

Page on symptoms, not causes. "CPU is at 85%" is not, by itself, a customer-facing problem — plenty of healthy services run hot on CPU with no latency impact. "p99 latency on the checkout endpoint exceeded 2 seconds for 5 minutes" is a symptom that directly reflects user pain. Use cause-level metrics (CPU, memory, pool saturation) for dashboards and root-cause investigation, and reserve pages for symptom-level, user-impacting thresholds.

A well-formed alert rule

yaml
# alerting-rules.yml
groups:
  - name: order-service-slo
    rules:
      - alert: OrderServiceHighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{application="order-service", status=~"5.."}[5m]))
          /
          sum(rate(http_server_requests_seconds_count{application="order-service"}[5m]))
          > 0.02
        for: 5m
        labels:
          severity: page
          team: orders
        annotations:
          summary: "order-service error rate above 2% for 5 minutes"
          description: "Error rate is {{ $value | humanizePercentage }}. Check the order-service dashboard: https://grafana.internal/d/order-service"
          runbook_url: "https://runbooks.internal/order-service-high-error-rate"
ElementWhy it matters
for: 5mRequires the condition to hold for a sustained window, not a single noisy sample — prevents flapping on transient blips
severity: page vs severity: ticketNot every alert needs a human woken up at 3am; route by actual urgency
annotations.description with the live valueThe person paged shouldn't have to open a dashboard just to know how bad it is
runbook_urlTurns "what do I do" into a link, not a memory test at 3am
Threshold tied to an SLO, not a guess> 0.02 should trace back to an agreed error budget, not an arbitrary round number

The alert severity ladder

SeverityResponseExample
Page (wake someone up)Immediate, human paged nowUser-facing error rate or latency SLO breach
Ticket / Slack notifyLook at it during business hoursDisk usage trending toward a threshold in 3 days
Dashboard onlyNo notification, just visibleCause-level metrics like raw CPU%, GC pause count

Run a regular alert audit: for every page fired in the last month, ask "did this require immediate human action, and was the response documented anywhere?" Alerts that consistently get acknowledged and ignored, or that always resolve themselves before anyone acts, should be demoted to a ticket or removed entirely. An alerting system that pages for things nobody acts on trains people to ignore pages — which is worse than having no alert at all.

Routing and deduplication with Alertmanager

Prometheus fires alerts; Alertmanager decides what to do with them — grouping related firings into a single notification, deduplicating repeats, silencing known issues, and routing by team/severity. Without it, a single outage that trips ten alert rules across twenty pods pages your on-call twenty times in the same minute instead of once.

yaml
# alertmanager.yml
route:
  receiver: default-slack
  group_by: ["alertname", "application"]
  group_wait: 30s        # wait briefly to batch related alerts into one notification
  group_interval: 5m     # minimum time between updates to an existing group
  repeat_interval: 4h    # don't re-page for an already-acknowledged, still-firing alert
  routes:
    - matchers: ["severity=page"]
      receiver: pagerduty-oncall
      continue: true
    - matchers: ["severity=ticket"]
      receiver: jira-ticket
    - matchers: ["team=orders"]
      receiver: orders-slack
 
receivers:
  - name: pagerduty-oncall
    pagerduty_configs:
      - routing_key: "${PAGERDUTY_ROUTING_KEY}"
  - name: orders-slack
    slack_configs:
      - channel: "#orders-alerts"
ConceptWhat it doesWhy it matters
group_byBundles alerts sharing the same labels into one notification20 pods failing the same check page once, not 20 times
group_waitDelays the first notification briefly to catch related alerts starting togetherAvoids a burst of near-duplicate pages during the opening seconds of an incident
repeat_intervalControls how often a still-firing alert re-notifiesLong enough to avoid re-paging every few minutes for a known, being-worked issue
SilencesManually suppress a specific alert for a bounded timePlanned maintenance, known issue already being fixed, avoids needless pages
⚠️

A silence with no expiry is an alert that quietly stops working forever. Always set a bounded silence duration, and treat any silence still active after its original incident closed as a bug to fix, not a permanent alerting decision.

Multi-window, multi-burn-rate alerting (SLO-based)

Simple threshold alerts either fire too late (average over a long window smooths out real spikes) or too often (a short window catches noise). A common refinement pairs a fast, sensitive window with a slow, high-confidence window and requires both to agree:

yaml
- alert: OrderServiceErrorBudgetBurnFast
  expr: |
    (
      sum(rate(http_server_requests_seconds_count{application="order-service", status=~"5.."}[5m]))
      / sum(rate(http_server_requests_seconds_count{application="order-service"}[5m]))
    ) > (14.4 * 0.001)
    and
    (
      sum(rate(http_server_requests_seconds_count{application="order-service", status=~"5.."}[1h]))
      / sum(rate(http_server_requests_seconds_count{application="order-service"}[1h]))
    ) > (14.4 * 0.001)
  for: 2m
  labels:
    severity: page
💡

This pattern — borrowed from Google's SRE workbook — pages fast on a genuinely severe burn rate (would exhaust a 30-day error budget in a few hours) while requiring corroboration from a longer window, which filters out single-minute noise without delaying real incidents by hours. It's more setup than a flat threshold, but it materially reduces both false pages and missed incidents for teams that track formal SLOs.


Key takeaways

  • Prometheus pulls metrics on a schedule from a known endpoint (/actuator/prometheus); Kubernetes deployments almost always use service discovery rather than static scrape targets.
  • Never graph or alert on a raw counter — always wrap it in rate() or increase(), or the number is meaningless.
  • Build the four golden signals — latency, traffic, errors, saturation — as the top row of every service dashboard before anything else.
  • Measure latency separately for success and error responses; blended latency can look artificially good during an outage of fast failures.
  • Page on symptoms (user-facing latency/error SLO breaches), not causes (CPU%, memory%) — causes belong on dashboards for root-cause work, not in pages.
  • Every alert needs a for: window to avoid flapping, a runbook link, and a live value in the description — a page with no next action is a page that trains people to ignore pages.
  • Regularly audit alerts for ones that fire without action or resolve themselves; demote or delete them, because alert fatigue is what causes real incidents to get missed.

Interview Questions

  • How does Prometheus's pull-based scraping model differ from a push-based metrics pipeline, and what are the trade-offs?
  • Why should you never alert on a raw Prometheus counter value directly?
  • What are the four golden signals, and what does each one actually measure?
  • Why is it important to measure latency separately for successful and failed requests?
  • What's the difference between a symptom-based alert and a cause-based alert? Give an example of each.
  • Why might paging on "CPU > 80%" alone be a bad alerting strategy?
  • What does the for: clause in a Prometheus alert rule do, and why does it matter?
  • Design an alert rule for an error-rate SLO breach. What fields would you include beyond the raw expression?
  • What is a multi-window, multi-burn-rate alert, and what problem does it solve compared to a single threshold?
  • How would you decide whether an alert should page someone immediately versus just create a ticket?
  • What's wrong with a dashboard that has 40 panels and no clear "check this first" section?
  • Explain histogram_quantile() — what data does it need, and why can't you compute a percentile from a plain counter?
  • How does Kubernetes service discovery in Prometheus avoid the need to hand-maintain a list of scrape targets?
  • What would you look for in a monthly alert audit, and what actions might come out of it?