Tools · Prometheus

Prometheus

Prometheus is the cloud native metrics system: a single binary that walks your cluster on a timer, reads a plain-text scoreboard off every service’s /metrics endpoint, stores those numbers as labelled time series, lets you interrogate them in a query language called PromQL — and fires alerts when the answers get ugly. It solves the platform problem of “something is slow and nobody can prove what, when, or how much” by turning a running system into numbers every team can chart and alert on, with nothing to buy and nothing to configure per app.

☺ Explain it like I’m 10

Imagine every ride in a theme park has a chalkboard by the gate: how many people rode today, how long the queue is, how many times it broke. Nobody shouts those numbers to the office — a tireless attendant walks the whole park every fifteen seconds, reads each board, and writes it all in a giant notebook with the time beside it. Later you can ask the notebook “how fast was the queue growing at 3pm?” And if a queue keeps growing for ten minutes straight, a bell rings in the office. Prometheus is the attendant, the notebook, and the bell.

🐘Your host for this topic: Ellie the Elephant — she never forgets a number. Ellie walks the park, reads every chalkboard, remembers the shape of every curve, and knows the difference between a bell worth ringing and one that just makes people stop listening.

What Prometheus is and the problem it solves

☺ Like you’re 10: It’s a robot that reads everybody’s numbers on a timer and remembers them, so you can draw pictures of the past and get warned about the present.

Prometheus was built at SoundCloud in 2012, donated to the CNCF in 2016, and became the second project ever to graduate — right after Kubernetes itself. That lineage matters: it was designed for the world Kubernetes creates, where targets are ephemeral and nobody can maintain a hand-written list of what to monitor. Today it is the default metrics backend of essentially every Kubernetes platform, and its query language and exposition format are de-facto standards other systems deliberately imitate.

The problem before Prometheus

Classical monitoring assumed stable hosts with names, checked against a static config, producing one number per check — “host web-03: CPU 78%, OK.” Kubernetes breaks every assumption in that sentence. Pods vanish in seconds, IP addresses are meaningless, and the interesting question is never “is web-03 up?” but “what is p99 checkout latency for version 1.4.3 in the EU region?” You need dimensions, not hostnames; discovery, not a hand-edited file; a query language, not a threshold.

The pull model and the exposition format

Prometheus’s defining architectural choice is that it pulls. Your application exposes an HTTP endpoint — conventionally /metrics — serving a simple line-based text format, and Prometheus fetches it on a schedule you control. Pull sounds like a detail; it is the whole design. Because Prometheus initiates, it controls the load and cannot be overwhelmed by a misbehaving client. A failed scrape is itself a signal — the synthetic up metric goes to 0, so target-down detection needs no heartbeat. No application needs monitoring credentials, and you can curl the endpoint yourself to see exactly what Prometheus sees. The format is deliberately boring:

# HELP http_requests_total Total HTTP requests handled.
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/checkout",code="200"} 41822
http_requests_total{method="GET",route="/checkout",code="500"} 17

# HELP http_request_duration_seconds Request latency.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{route="/checkout",le="0.1"} 39120
http_request_duration_seconds_bucket{route="/checkout",le="0.5"} 41500
http_request_duration_seconds_bucket{route="/checkout",le="+Inf"} 41839
http_request_duration_seconds_sum{route="/checkout"} 5210.4
http_request_duration_seconds_count{route="/checkout"} 41839

That is the entire contract, which is why the ecosystem is enormous. Anything that cannot speak it natively gets an exporter that translates: node_exporter for machine CPU, memory, disk and network; kube-state-metrics for the state of Kubernetes objects (replicas wanted versus available, whether a Job failed); blackbox_exporter for probing a URL from outside; plus Postgres, Redis, Kafka and hundreds more.

What Prometheus deliberately is not

Prometheus is not a logging system, a tracing system, or a billing-grade record. It samples; it does not capture every event. Its storage is deliberately short-lived and single-node — no clustering, sharding or replication. It will tell you error rate rose to 4% at 14:32; it will never tell you which customer’s order failed. That is a feature: cheap, dimensional metrics are what make it affordable to watch every service. For per-request detail you go to logs and traces, as the observability lesson lays out.

◆ Key idea

A Prometheus time series is uniquely identified by its metric name plus the full set of its label key/value pairs. Change one label value and it is a different series with its own memory footprint and its own chunk of disk. Every design decision, cost surprise and outage in this page follows from that one sentence — so learn it before anything else.

Where Prometheus fits in a platform

☺ Like you’re 10: Prometheus sits in the platform’s watchtower. It doesn’t build or deploy anything — it watches everything else and tells other tools when to act.

In the reference architecture, Prometheus lives in the observability plane: a shared capability the platform team operates and every tenant consumes without installing anything. Its job is to be the metrics substrate — where numbers land — while other tools read from it. Because it is configured through Kubernetes objects, it is deployed and managed through GitOps like any other workload.

Its neighbours, and who does what

Prometheus is almost never used alone, and confusing it with its neighbours is a common exam stumble. Grafana stores nothing — it queries Prometheus and draws the result. OpenTelemetry is the vendor-neutral instrumentation standard whose Collector exports metrics into Prometheus, so the two are complements, not rivals. Alertmanager is a separate binary that receives fired alerts and decides who gets woken. KEDA autoscales on PromQL queries; OpenCost emits cost as Prometheus metrics; Argo Rollouts and Flagger query it mid-canary to promote or roll back. Prometheus is the shared numeric truth they all lean on.

🐘 Ellie’s-eye view

“Here’s the trick nobody tells you: metrics-server and Prometheus are not the same thing and never were. metrics-server feeds kubectl top and basic HPA — it keeps a few seconds of CPU and memory in RAM and forgets. I keep everything, with labels, for weeks, and I can answer questions you didn’t think to ask this morning. If someone tells you they have monitoring because kubectl top works, they have a thermometer, not a watchtower.”

CNPE domain relevance

Prometheus sits squarely inside the exam’s Observability domain — 20% of the blueprint — and is named on the official CNPE tool list. It leaks into other domains too, and the exam likes those seams: SLOs and error budgets are computed in PromQL, canary analysis queries Prometheus, FinOps dashboards are Prometheus queries, and custom-metric autoscaling reads from it. Treat it as one of the few tools you must be able to write, not merely recognise.

How it works — architecture, components, CRDs

☺ Like you’re 10: One program does four jobs in a loop: find the targets, read their numbers, store them, and run little rules over them.

The Prometheus server is a single Go binary with four internal jobs running continuously, plus a handful of friends around it. Understanding the loop makes almost every failure mode obvious.

App pods /metrics node-exporter machine stats kube-state-metrics object state ServiceMonitor PodMonitor selects targets by label 🐘 Prometheus service discovery scrape + relabel TSDB (local, ~15d) rule evaluation scrape Grafana PromQL dashboards Alertmanager group · silence · route Thanos / Mimir long-term storage Slack · PagerDuty · email query fire alerts remote_write Prometheus pulls · nothing pushes to it · a failed scrape sets up == 0

Service discovery, relabelling, and the TSDB

Service discovery is how Prometheus learns what exists. On Kubernetes it queries the API server through kubernetes_sd_configs with a rolepod, service, endpoints, endpointslice, node or ingress — and gets a live list of candidates decorated with metadata labels like __meta_kubernetes_pod_label_app. Relabelling then filters and rewrites that list: relabel_configs run before the scrape and decide which targets survive and what labels they carry; metric_relabel_configs run after it and drop noisy series before storage. It is the most powerful and most confusing part of the config, and where cardinality problems get fixed.

Scraped samples land in the TSDB, Prometheus’s local time series database. Recent data sits in an in-memory head block protected by a write-ahead log; every two hours the head is compacted into an immutable on-disk block, and old blocks are merged then deleted. Retention defaults to 15 days (--storage.tsdb.retention.time, or a size cap with --storage.tsdb.retention.size). There is no clustering: HA means two identical replicas that both scrape everything, and long retention means shipping data elsewhere.

The four metric types

The exposition format labels every metric with a type, and choosing the wrong one is the most common instrumentation bug.

TypeWhat it isQuery it withClassic example
CounterOnly ever goes up (or resets to 0 on restart)rate(), increase() — never the raw valuehttp_requests_total
GaugeGoes up and down; a snapshot of “right now”Read directly; avg_over_time(), delta()node_memory_MemAvailable_bytes
HistogramCounts observations into cumulative buckets, plus _sum and _counthistogram_quantile() over rate(..._bucket[5m])http_request_duration_seconds
SummaryQuantiles computed in the client, exposed as quantile="0.99"Read directly — but cannot be aggregated across instancesLegacy latency metrics

Histogram versus summary is a favourite exam question. A histogram ships raw bucket counts, so Prometheus can add ten pods’ buckets together and compute a fleet-wide p99 correctly. A summary ships an already-computed p99 per pod, and there is no valid mathematics for averaging percentiles — so summaries are effectively unusable in a distributed system. Prefer histograms, with bucket boundaries straddling your SLO.

The Prometheus Operator and its CRDs

On Kubernetes you almost never hand-edit prometheus.yml. The Prometheus Operator — packaged with everything else as the kube-prometheus-stack Helm chart — turns configuration into custom resources, so scrape config becomes a declarative, GitOps-managed, self-service thing a tenant can create in their own namespace. The Operator watches those CRs, generates the real config into a Secret, and reloads Prometheus.

Custom resourceWhat it declares
PrometheusA Prometheus server: replicas, retention, resources, storage, and the selectors that decide which monitors and rules it adopts
ServiceMonitor“Scrape the endpoints behind Services matching these labels, on this named port and path”
PodMonitorThe same, but selecting Pods directly — for workloads with no Service
PrometheusRuleA group of recording and/or alerting rules
Alertmanager / AlertmanagerConfigAn Alertmanager cluster, and namespaced routing/receiver fragments tenants can own
ProbeBlackbox-style probing of static targets or Ingresses
ScrapeConfigAn escape hatch for discovery mechanisms outside the cluster (EC2, Consul, static targets)

All of them live in the monitoring.coreos.com API group, but not all at the same version: Prometheus, ServiceMonitor, PodMonitor, PrometheusRule, Alertmanager and Probe are served at v1, while AlertmanagerConfig and ScrapeConfig have historically trailed on an alpha/beta version. Before writing one from memory, confirm what your cluster actually serves with kubectl api-resources --api-group=monitoring.coreos.com.

The queries and resources you will actually write

☺ Like you’re 10: Two things you type all the time: little questions in PromQL, and little YAML files that say “watch this” and “ring the bell when that.”

PromQL essentials

PromQL has two result shapes you must tell apart. An instant vector is one value per series at a single moment — http_requests_total. A range vector adds a bracketed duration and returns a set of samples over time per series — http_requests_total[5m]. Range vectors cannot be graphed directly; they exist to be fed to a function that collapses them back to an instant vector, which is why rate() always takes a bracketed range.

# Per-second rate of a COUNTER, averaged over a 5-minute window.
# rate() handles counter resets, and needs at least two samples in the window —
# so keep the range >= 4x your scrape interval (30s scrape -> [2m] minimum).
rate(http_requests_total[5m])

# irate() uses only the LAST TWO samples: spiky, good for zooming into a
# fast-moving graph, bad for alerts (it misses everything in between).
irate(http_requests_total[1m])

# increase() = total growth over the window (it is rate() * window seconds).
increase(http_requests_total{code=~"5.."}[1h])

# Aggregate ACROSS series. "by" keeps the listed labels; "without" drops them.
sum by (route, code) (rate(http_requests_total[5m]))
sum without (instance, pod) (rate(http_requests_total[5m]))

# Error ratio: aggregate numerator and denominator the SAME way, then divide.
  sum by (route) (rate(http_requests_total{code=~"5.."}[5m]))
/ sum by (route) (rate(http_requests_total[5m]))

# p99 latency from a histogram. The "le" label MUST survive the aggregation.
histogram_quantile(0.99,
  sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

# offset = look back in time. Compare now against the same moment last week.
  sum(rate(http_requests_total[5m]))
/ sum(rate(http_requests_total[5m] offset 7d))

# Subquery: run an inner range query over an outer window.
# "the highest 5m error rate seen at any point in the last hour, sampled each minute"
max_over_time( sum(rate(http_requests_total{code=~"5.."}[5m]))[1h:1m] )

# Selectors: = exact, != not, =~ regex match, !~ regex not-match.
up{job="checkout", namespace!="kube-system"} == 0

Two rules carry most of PromQL. First, rate() only makes sense on a counter — on a gauge it produces confident nonsense. Second, rate first, aggregate second: sum(rate(x[5m])) is correct; summing counters before rating them mishandles pod restarts. Binary operators match series by label set, which is why the error-ratio example aggregates both sides identically — mismatched labels silently return an empty result rather than an error.

A ServiceMonitor (and its PodMonitor twin)

This is the resource an application team writes to get scraped. Note the three things that must line up: the selector matches labels on the Service, the port is the Service port’s name (not a number), and the resource’s own labels must match what the Prometheus CR is selecting.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: checkout
  namespace: checkout
  labels:
    release: kube-prometheus-stack   # <-- the label the Prometheus CR selects on
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: checkout   # must match the SERVICE's labels
  namespaceSelector:
    matchNames: [checkout]
  endpoints:
    - port: http-metrics       # the NAME of a port on the Service, not 9090
      path: /metrics           # default is /metrics
      interval: 30s
      scrapeTimeout: 10s
      metricRelabelings:       # drop a known-noisy series before storing it
        - sourceLabels: [__name__]
          regex: 'go_gc_duration_seconds.*'
          action: drop
---
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor          # same idea, but selects Pods directly (no Service needed)
metadata:
  name: batch-workers
  namespace: checkout
  labels:
    release: kube-prometheus-stack
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: batch-worker   # matches POD labels
  podMetricsEndpoints:
    - port: metrics        # the NAME of a containerPort on the Pod
      interval: 30s

A PrometheusRule with recording and alerting rules

Recording rules pre-compute an expensive expression on a schedule and store the result as a new series — dashboards and alerts then query the cheap pre-computed series. The convention for their names is level:metric:operations. Alerting rules evaluate an expression and fire when it returns any series; for requires the condition to hold continuously before firing, which is what kills flapping.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: checkout-slo
  namespace: checkout
  labels:
    release: kube-prometheus-stack   # same adoption label as the ServiceMonitor
spec:
  groups:
    - name: checkout.rules
      interval: 30s
      rules:
        # RECORDING RULE — compute once, query many times.
        - record: job:checkout_request_error_ratio:rate5m
          expr: |
            sum by (job) (rate(http_requests_total{job="checkout",code=~"5.."}[5m]))
              / sum by (job) (rate(http_requests_total{job="checkout"}[5m]))

        # ALERTING RULE — a SYMPTOM alert: users are actually being hurt.
        - alert: CheckoutHighErrorRate
          expr: job:checkout_request_error_ratio:rate5m > 0.02
          for: 10m                     # must hold 10 minutes before it fires
          labels:
            severity: critical         # Alertmanager routes on these
            team: payments
          annotations:                 # for humans; templating is allowed
            summary: "Checkout error ratio is {{ $value | humanizePercentage }}"
            runbook_url: "https://runbooks.acme.dev/checkout-errors"

        # A latency alert built from the histogram.
        - alert: CheckoutLatencySLOBreach
          expr: |
            histogram_quantile(0.99,
              sum by (le) (rate(http_request_duration_seconds_bucket{job="checkout"}[5m]))
            ) > 0.75
          for: 15m
          labels: { severity: warning, team: payments }
          annotations:
            summary: "p99 checkout latency above the 750ms SLO"

Alertmanager routing

Prometheus decides what is wrong; Alertmanager decides who hears about it, and how often. Its config is a routing tree: an alert enters at the root and walks down until it matches a node, inheriting whatever it does not override. group_by collapses related alerts into one notification, group_wait briefly holds the first send so siblings can join, and inhibit_rules suppress a lesser alert while a severe one fires. Silences are created at runtime via the UI or amtool, not in this file.

global:
  # Slack receivers need a webhook URL from here or from the receiver itself.
  slack_api_url_file: /etc/alertmanager/secrets/slack/url

route:
  receiver: platform-default
  group_by: [alertname, namespace, cluster]   # one notification per group
  group_wait: 30s            # wait for siblings before the first send
  group_interval: 5m         # wait before sending updates to an existing group
  repeat_interval: 4h        # re-nag interval for a still-firing group
  routes:
    - matchers: [ 'severity="critical"' ]
      receiver: pagerduty-oncall
      continue: false        # the default; stop here, do not try later siblings
    - matchers: [ 'team="payments"' ]
      receiver: slack-payments

inhibit_rules:               # if the whole cluster is down, don't also page
  - source_matchers: [ 'alertname="ClusterDown"' ]  # for every service in it
    target_matchers: [ 'severity=~"warning|critical"' ]
    equal: [cluster]         # only inhibit alerts sharing this label's value

receivers:
  - name: platform-default
    slack_configs:
      - channel: '#platform-alerts'
        send_resolved: true
  - name: pagerduty-oncall
    pagerduty_configs:
      - routing_key_file: /etc/alertmanager/secrets/pd/key   # Events API v2
  - name: slack-payments
    slack_configs:
      - channel: '#payments-alerts'
⚠ Alert on symptoms, not causes

The fastest way to make an on-call rotation miserable is to page on internal causes — “CPU above 80%”, “a pod restarted”, “disk 70% full”. High CPU that harms nobody is not an emergency, and an alert nobody acts on trains people to ignore the ones that matter. Page on what the user actually feels: error ratio, latency, the SLO burning. Everything else is a dashboard, a ticket, or a warning — see Reliability & Incidents.

Day-to-day commands

☺ Like you’re 10: Mostly you check your rules are spelled right, open the web page, and look at the list of things it’s watching.

promtool — validate before you commit

promtool ships in the Prometheus image and is your CI gate. Plain CRD schema validation checks structure, not PromQL, so unless the Prometheus Operator’s validating admission webhook is deployed and reachable (the kube-prometheus-stack chart installs one for PrometheusRule by default), a rule with a broken expression is accepted by the API server and then simply never loads. Validating in CI is the belt-and-braces fix — but note that promtool check rules expects a plain Prometheus rule file, so lift .spec out of the custom resource first.

promtool check config /etc/prometheus/prometheus.yml   # whole server config
promtool check rules rules/*.yaml                      # syntax of PLAIN rule files
promtool test rules tests/checkout_test.yaml           # unit-test alerts against fake data
promtool check metrics < scraped.txt                   # lint exposition output

# A PrometheusRule is not a rule file — lift .spec out before linting it.
kubectl -n checkout get prometheusrule checkout-slo -o jsonpath='{.spec}' > /tmp/rules.yaml
promtool check rules /tmp/rules.yaml

promtool query instant  http://localhost:9090 'up == 0'
promtool query range    http://localhost:9090 --start=$(date -u -d '-1 hour' +%Y-%m-%dT%H:%M:%SZ) \
  'sum(rate(http_requests_total[5m]))'

# The Operator stores generated config in a Secret — extract it to lint it.
kubectl -n monitoring get secret prometheus-kube-prometheus-stack-prometheus \
  -o jsonpath='{.data.prometheus\.yaml\.gz}' | base64 -d | gunzip | head -40

Reaching the UI and reading its status pages

# The Operator always creates a headless Service called prometheus-operated.
kubectl -n monitoring port-forward svc/prometheus-operated 9090
#   http://localhost:9090/targets   <- is my target UP? what was the scrape error?
#   http://localhost:9090/config    <- did my ServiceMonitor become scrape config?
#   http://localhost:9090/alerts    <- inactive / pending / firing
#   http://localhost:9090/tsdb-status <- top label/metric cardinality offenders

kubectl -n monitoring port-forward svc/alertmanager-operated 9093

# Query the HTTP API directly (handy inside a debug pod or a script).
curl -sG http://localhost:9090/api/v1/query --data-urlencode 'query=up{job="checkout"}'
curl -s  'http://localhost:9090/api/v1/targets?state=active' | jq '.data.activeTargets[].health'
curl -s  http://localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:10]'
curl -s  http://localhost:9090/-/healthy ; curl -s http://localhost:9090/-/ready

Inspecting the Operator’s objects, and Alertmanager

kubectl get servicemonitor,podmonitor,prometheusrule -A
kubectl -n monitoring get prometheus -o yaml | grep -A6 'serviceMonitorSelector'  # THE gotcha
kubectl -n monitoring logs sts/prometheus-kube-prometheus-stack-prometheus -c prometheus
kubectl -n monitoring logs deploy/kube-prometheus-stack-operator   # config generation errors

# Prove an app is actually exposing metrics, from inside the cluster.
kubectl -n checkout exec deploy/checkout -- wget -qO- localhost:8080/metrics | head

amtool --alertmanager.url=http://localhost:9093 alert query
amtool --alertmanager.url=http://localhost:9093 silence add alertname=CheckoutHighErrorRate \
  --duration=2h --comment="planned migration"
amtool config routes test --config.file=alertmanager.yml severity=critical team=payments

Gotchas and failure modes

☺ Like you’re 10: Here are the ways Prometheus quietly does nothing and lets you think it’s working.

“No metrics” — the label mismatch that causes 80% of tickets

This is the most common Prometheus problem on Kubernetes, and it is worth memorising as a chain. A Prometheus CR only adopts monitors matching its serviceMonitorSelector and living in a namespace matching its serviceMonitorNamespaceSelector. The kube-prometheus-stack chart defaults that selector to release: <your-helm-release-name>, so a perfectly valid ServiceMonitor without that label is simply ignored — no error, no event, nothing in /targets. Work the chain in order: does it carry the release label? Does its selector match the Service’s labels (not the Deployment’s or Pod’s)? Is the namespaceSelector right? Is endpoints[].port the port name on the Service? And does that Service actually have ready backends (kubectl get endpointslices -l kubernetes.io/service-name=checkout)? Workload triage walks the same ladder for pods that never go ready.

⚠ The silence is the bug

Every step in that chain fails quietly. A ServiceMonitor that selects nothing is indistinguishable, from the API server’s point of view, from one that works. Build the habit: after creating a monitor, always confirm the target appears in /targets with health UP. And add a meta-alert — absent(up{job="checkout"}) — so the platform notices when a service stops being scraped at all.

Cardinality explosions

Each unique label-value combination is a separate series held in memory. Put an unbounded value in a label — a user ID, an email, a request ID, a full URL with query string — and series count jumps from thousands to millions. Queries slow, then time out, then Prometheus is OOM-killed and restarts into the same problem. The cure is bounded labels only (status code, method, route template, region, version) plus metricRelabelings to drop known offenders at ingestion. Diagnose with /tsdb-status, topk(10, count by (__name__)({__name__=~".+"})), and prometheus_tsdb_head_series. High-cardinality identifiers belong on traces and logs, never on metric labels.

rate() on the wrong thing, or the wrong window

rate() requires a counter and at least two samples inside the window. Ask for rate(x[15s]) with a 30-second scrape interval and you get an empty result — not an error, an empty graph, which people read as “no traffic.” The rule of thumb is a range of at least four times the scrape interval. Applying rate() to a gauge is silently meaningless. And a low-traffic endpoint that gets one request an hour will show rate values that look like zero, so use increase() over a long window for rare events.

Storage, HA, and the things Prometheus will not do for you

Prometheus keeps roughly 15 days locally and has no replication: if the volume dies, the history is gone. Two replicas give availability, not durability, and they disagree slightly because they scrape at different instants — which is why HA pairs are deduplicated downstream. For long retention, durable object storage and a global query view you add Thanos or Grafana Mimir, fed by the Thanos sidecar or by remote_write. Two further traps: remote_write queues fill and drop samples when the receiver is slow (watch prometheus_remote_storage_samples_dropped_total), and alerting still depends on the local Prometheus — if it is down, nothing fires at all, so alert on Prometheus itself from somewhere else. The troubleshooting playbook has the wider decision tree.

🐘 Ellie’s workshop · 25 min

On a throwaway kind or minikube cluster, install kube-prometheus-stack into a monitoring namespace. Port-forward 9090 and open /targets. Now deploy any app exposing /metrics behind a Service, and write a ServiceMonitor deliberately missing the release label. Refresh /targets: nothing — and nothing in the logs either. Add the label, wait a minute, watch it appear UP; that thirty seconds of confusion is the lesson. Break it again by putting a port number where the port name goes, and read the scrape error. Finally add a PrometheusRule with for: 1m, watch it go inactive → pending → firing, and silence it with amtool.

Alternatives and when to choose it

☺ Like you’re 10: Other notebooks exist. Most of them copy Prometheus’s handwriting so you can switch without relearning.

The remarkable thing about the alternatives is how many speak PromQL and accept Prometheus remote_write. That is a real platform-engineering advantage: dashboards, rules and instrumentation stay portable, so the backend becomes a swappable detail rather than a decade of lock-in.

The comparison that decides it

OptionModelBest whenCosts you
PrometheusSingle-binary pull-based scraper with local TSDBAlmost always the starting point: per-cluster metrics, alerting, canary analysis, autoscaling signals — nothing else to buyShort local retention, no HA or clustering, vertical scaling only; you eventually bolt on long-term storage
ThanosSidecar + object storage + global querier on top of PrometheusYou already run Prometheus per cluster and now need years of retention and one query across all of themSeveral more components to operate; query latency over object storage; more moving parts to debug
Grafana MimirHorizontally scalable, multi-tenant PromQL backend fed by remote_writeVery large scale with hard multi-tenancy and central retention; you want one place, not many PrometheusesA real distributed system to run; Prometheus (or an agent) still does the scraping
VictoriaMetricsPrometheus-compatible store, MetricsQL, lower resource useCost or memory pressure at scale, with a mostly drop-in migrationQuery dialect differs at the edges; smaller community than the CNCF core
OpenTelemetry CollectorVendor-neutral pipeline: receive, process, exportAlways, as a complement — normalise and enrich telemetry, then export to PrometheusNot a store or a query engine; it moves data, it does not keep or answer questions about it
Datadog / Cloud-native SaaSHosted agent-push monitoringYou want no operational burden and will pay for itCost that scales with cardinality and surprises you; vendor lock-in; not on the CNPE tool list

A practical rule

Start with Prometheus per cluster, run it through the Operator so tenants self-serve their own ServiceMonitors, and add Thanos or Mimir only when a real requirement arrives — “13 months for a compliance report,” or one dashboard across six clusters, which is also a multi-cluster conversation. Do not start with the distributed system. See The Tool Landscape for how Prometheus sits among the other named CNPE projects.

🎬 At the Platform Guild
🦊

Foxy: I wrote the ServiceMonitor, applied it, no errors. There are still no metrics. Is Prometheus broken?

🐘

Ellie: Prometheus never even heard about it. The Prometheus CR only adopts monitors matching serviceMonitorSelector — in this chart, release: kube-prometheus-stack. Your monitor has no such label, so it’s invisible.

🦊

Foxy: Shouldn’t it tell me?

🐘

Ellie: It’s a label selector. Selecting nothing is perfectly valid. Check /targets every time — that page is the truth.

👺

Gizmo: Easy fix — put the user_id on every metric label. Then you can find any customer instantly! 🤑

🐘

Ellie: That is four million time series by Thursday and an OOM-killed Prometheus by Friday. Bounded labels only. Customer IDs go on traces.

🐢

Timmy: And run promtool check rules in CI. A rule file with one typo loads as nothing, and you find out during the outage it was meant to catch.

🦆

Dot: Honestly I just want to add four lines of YAML and see my p99 on a dashboard. Which — once the label is right — is exactly what happens.

Exam relevance and going further

☺ Like you’re 10: On exam day you cannot open Prometheus’s website — so the YAML and the queries have to already live in your head.

Prometheus is on the official CNPE tool list and is the most likely vehicle for an Observability task. Expect to be asked to make a workload scraped, to write or repair a PrometheusRule, to read a PromQL expression and say what it means, to explain pull versus push, or to diagnose a target that is not appearing.

The documentation allowlist — read this twice

⚠ Prometheus’s own docs are not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. prometheus.io/docs and prometheus-operator.dev are not on that list, and neither is the Grafana or Alertmanager documentation. Unless a task’s Quick Reference hands you a link, you must write the ServiceMonitor and PrometheusRule from memory. Drill them from Know Cold — that page exists precisely for the manifests you cannot look up.

⚖ CNPA vs CNPE — That allowlist is a CNPE-only mechanic: CNPE is hands-on, so it permits those narrow live lookups mid-task. CNPA is stricter, not looser — a fully closed-book, multiple-choice exam with zero external resources and zero lookups of any kind, so neither prometheus.io nor kubernetes.io would be reachable there either. Even so, the concept-level knowledge above — the pull model, the four metric types, and reading a PromQL expression like histogram_quantile over a rate — is exactly the kind of thing CNPA's closed-book recall draws on.

What to be able to do without notes

Write a ServiceMonitor from a blank file: apiVersion: monitoring.coreos.com/v1, the adoption label, selector.matchLabels matching the Service, a namespaceSelector, and endpoints with a port name, path and interval. Write a PrometheusRule with one recording rule (record/expr) and one alerting rule (alert/expr/for/labels/annotations). Name the four metric types and say why a summary cannot be aggregated but a histogram can. Write sum by (le) (rate(..._bucket[5m])) inside histogram_quantile without hesitating. Explain up == 0, the pull model, and what Alertmanager adds. Know the diagnostic path: /targets, then selector labels, then Service endpoints. The CLI spine sits in the command reference.

Official resources for after the exam

Outside the exam the canonical sources are prometheus.io/docs (the Querying and Best practices sections repay slow reading), the Operator API reference at prometheus-operator.dev, github.com/prometheus-operator/kube-prometheus, the exposition standard at openmetrics.io, and cncf.io/projects/prometheus. Pair this page with Observability for the pillars and SLOs, Grafana for the dashboards on top, OpenTelemetry for the instrumentation underneath, and the glossary when a term stops making sense.

🐢 Timmy’s checkpoint

1. Why does Prometheus pull instead of accepting pushes, and what free signal does that give you? 2. You applied a valid ServiceMonitor and no target appears. Name the first three things you check, in order. 3. Why can you compute a fleet-wide p99 from a histogram but not from a summary? 4. What is wrong with rate(node_memory_MemAvailable_bytes[5m]), and what is wrong with rate(http_requests_total[15s]) on a 30-second scrape interval? 5. Name three jobs Alertmanager does that Prometheus does not. 6. During the exam, where can you look up the PrometheusRule schema?

Check your answers
  1. Pulling means Prometheus controls its own load, needs no credentials distributed to applications, and can be reproduced by hand with curl. The free signal is up: a failed scrape sets up == 0, so target-down detection needs no heartbeat.
  2. (a) Does it carry the label the Prometheus CR’s serviceMonitorSelector requires — in kube-prometheus-stack, release: <release-name>? (b) Does selector.matchLabels match the Service’s labels, and is namespaceSelector right? (c) Is endpoints[].port the port name, and does the Service have ready endpoints? Then confirm in /targets.
  3. A histogram exposes raw cumulative bucket counts (_bucket with the le label), so buckets from many instances can be summed and the quantile computed centrally with histogram_quantile. A summary exposes quantiles already computed per client, and percentiles cannot validly be averaged.
  4. The first applies rate() to a gauge, which is meaningless — rate() is for counters. The second asks for a 15-second window when samples arrive every 30 seconds: fewer than two samples, so it returns empty, not an error. Keep the range around four times the scrape interval.
  5. Any three of: grouping related alerts into one notification, deduplication across HA Prometheus replicas, inhibition (suppress a lesser alert while a bigger one fires), silencing for planned work, and routing to the right receiver by label.
  6. You can’t — prometheus.io and prometheus-operator.dev are not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). Write it from memory; drill it on Know Cold.