What Is Observability? Prometheus and Grafana on kind, and Who Notices First When a Pod Dies

A pod died on kind and no one knew until a user said so. Prometheus, Alertmanager, Grafana: the three waits behind every alert and what faster costs.

By Mirac Can Yılmaz 9 min read
What Is Observability? Prometheus and Grafana on kind, and Who Notices First When a Pod Dies - DevOps yazısının kapak görseli

A pod of mine stopped serving and I found out when someone else told me. Nothing in the cluster said a word, because nothing was watching it. So I put Prometheus and Grafana on a kind cluster, wired a single alert, and killed the pod on purpose to see what actually happens between a pod dying and a person hearing about it. The alert did come, but late enough that a user could still have been first. Then I changed three settings, the same failure was caught almost at once, and I started paying for it in a different way. This post is about that gap, the three settings that make it, and the trade-off you take on when you close it.

What is observability?

Observability is being able to answer "what is happening inside this system, and since when" from the outside, without adding new print statements and redeploying. It usually rests on three kinds of signal:

  • Metrics: values sampled over time (request rate, error count, memory, up or down). Cheap, and what alerts are built on.
  • Logs: discrete events with detail, for reading after something fires.
  • Traces: one request's path across services.

This post is about the metrics half, because that is where alerting lives: Prometheus collects the values, Alertmanager decides when one of them has been bad for long enough to wake someone, and Grafana draws them. On kind, a local cluster, the whole thing runs on a laptop.

The problem, and why it matters

Before this, a pod could stop serving and the first signal was a user. There was no record of when it went down, because nothing was recording "down" at all. You cannot alert on a signal you never collect, and you cannot explain an outage you have no timeline for. The question an on-call person actually needs, at 3am, is "is it down, since when, and did it page me." Without metrics you are guessing at all three.

The second cost is quieter. Even once you collect metrics, the delay between a failure and the alert is not zero, and it is not obvious. It is the sum of three waits most people never look at: how often you scrape, how often you evaluate the rule, and how long you make the rule wait before it is sure. This post walks through each of them, and through what you give up when you shorten them.

What Prometheus and Grafana give you

In practice What changed in the lab
Automatic target discovery A ServiceMonitor tells Prometheus what to scrape; new pods are picked up without editing config Prometheus found the cluster's own components and my app on its own; I never listed a target by hand
An alert that fires on its own A PrometheusRule watches a metric and routes to Alertmanager the dead pod reached Alertmanager without anyone looking at a dashboard; before, a person was the alert
Dashboards without wiring kube-prometheus-stack ships Grafana with cluster dashboards already loaded Grafana showed live node and pod views the moment the stack came up; I built no panel
One install, not five The operator runs Prometheus, Alertmanager, Grafana, node-exporter and kube-state-metrics together one helm install, one values file, and the five pieces already knew about each other

How it works: where the delay comes from

Diagram: the path from a failing pod to a firing alert, and the three waits along it. Prometheus scrapes the target on a scrape interval; when the pod's endpoint disappears the up series goes absent at the next scrape. The rule is evaluated on an evaluation interval, which flips the alert to pending. The alert then has to stay true for the for duration before it becomes firing and is sent to Alertmanager. Each of the three waits is a setting you choose, and each has a cost when you shorten it.

Prometheus does not get pushed events. It pulls: it scrapes each target's /metrics on a fixed interval and stores what it reads. An alert is a query that runs on its own interval, and it only fires after it has been true for a set duration. So a failure has to clear three waits before anyone hears about it, and each wait is a setting you choose.

The lab

The app is a one-pod HTTP service that exposes Prometheus metrics. A ServiceMonitor tells Prometheus to scrape it, and a PrometheusRule says "alert if this target is not up." Then I scale the deployment to zero and watch what the stack does, first with the chart's defaults, then with the three waits shortened.

Terminal: kube-prometheus-stack pods on the kind cluster, all Running. Prometheus, Alertmanager, Grafana, kube-state-metrics, node-exporter and the operator, installed by one helm command, plus the one-pod sample app in the demo namespace.

  1. The gap is real, and it is three waits. With the defaults, killing the pod played out in slow motion. First Prometheus noticed the target was gone, at its next scrape. Then, at the next rule evaluation, the alert went pending. Then it sat there, pending, while the for timer ran, and only after that did it go firing and reach Alertmanager. Nothing was broken. That is simply what three timers in a row feel like from the outside: long enough to refresh a page, frown, and refresh again. A user who was on the app at that moment would have known before the pager did.
  2. Shortening the three waits changed who finds out first. I scraped more often, evaluated the rule more often, and shortened for. Same kill, same app: the alert reached Alertmanager before I had finished reading the terminal. The gap did not disappear, it cannot, because the pull model always waits for the next scrape. But it moved from "a user could be first" to "the pager is first." The only change was three values in two YAML files, and the bill for it is in the trade-off section below.
  3. Grafana was live with no panels built. kube-prometheus-stack installs Grafana with cluster dashboards already loaded, so the node and pod views showed real data the moment the stack came up. Before the install there was no dashboard at all; after one command there was a live one, and the dead pod showed up on it as a gap in the graph.

Grafana: a cluster dashboard that shipped with kube-prometheus-stack, showing live CPU, memory and network for the kind node, with no panel built by hand.

Where it broke

1. up == 0 never fired, because the target was gone, not down

My first alert was up == 0. It never fired. When I scaled the deployment to zero, the pod's endpoint disappeared, so Prometheus dropped the target entirely and the up series for that job stopped existing. up == 0 matches a target that is present and failing, not one that is absent. A query over a series that has no samples returns nothing, and nothing is not zero, so the rule stayed quiet. The fix was up == 0 or absent(up{job="sample-app"}): absent() returns a value exactly when the series is missing, which is the case a vanished pod actually produces.

2. Faster is not free

The tuned config is not strictly better than the default one. A shorter for means the alert trusts a shorter blip, so a single missed scrape during a rollout or a one-off network hiccup can page someone for a service that is fine. Shorter scrape and evaluation intervals also mean more queries and more writes, on every target, all day. The right values depend on how bad a false page is for that service, not on making everything as small as it goes.

3. The rule change took a reload, not effect, immediately

Editing the PrometheusRule did not change behaviour the instant I applied it. The operator has to regenerate Prometheus's rule files and Prometheus has to reload them, and that took long enough that my first run after the change was still using the old rule. If you test right after an apply, you are testing the previous config.

Tuning: the three settings, and what each costs

Setting What it controls Shorter means The cost
Scrape interval (ServiceMonitor) how often a target is read failures seen sooner more scrapes, more stored samples
Evaluation interval (rule) how often the alert query runs pending sooner more query load
for duration (rule) how long it must stay true firing sooner more false pages on brief blips

Rule of thumb: pick for from how bad a false page is, not from how fast you want to look. A user-facing API can take a short for; a batch job that flaps is better left with a longer one.

The trade-off, side by side

Alert fast Alert slow
Plus the pager is first, not a user; short outages leave a timeline you can explain a quiet pager; rollouts and blips do not wake anyone; less load on Prometheus
Minus false pages on blips and rollouts; more scrape and query load; on-call learns to ignore the pager a user can be first; short outages leave no trace; "since when" has a coarse answer
Fits user-facing APIs, anything with a person waiting on it batch jobs, internal tools, anything that flaps by design

Neither column is the right one. I run the user-facing app on the left column and the batch job on the right, in the same cluster, and I expect to move things between them as the pager teaches me.

Prometheus vs CloudWatch, briefly

If you came from AWS, the mental model is different. CloudWatch is push and per-metric billing: you pay for custom metrics and for higher resolution, and the standard resolution is fixed and coarse. Prometheus is pull and self-hosted: the resolution is the scrape interval you set, and the cost is the CPU and disk it runs on, not a line item per metric. On kind it is free and local, which is the point of learning it this way before paying for it in a cloud.

When to use which

  • Reach for kube-prometheus-stack when you want Prometheus, Alertmanager and Grafana wired together in one install rather than three.
  • Define scrape targets with a ServiceMonitor, not hand-edited config, so new services are picked up on their own.
  • Write the "is it down" alert as up == 0 or absent(up{...}), so a vanished pod fires too.
  • Set for from the cost of a false page, and scrape and evaluation intervals from how fast you truly need to know.

Your call

I picked a short for for the user-facing app and left the batch job slow. That is the right split for my cluster this week; it is not a rule for yours. Two things I would like to read in the comments, because they are the part of this I cannot learn on a laptop:

  • Who noticed your last outage first, the pager or a person?
  • If you ever tuned alerting faster, what did it cost you: false pages, load, or nothing at all?

I am not after who is right. I want the pluses and minuses of both columns above to be visible to everyone who reads this, in words from real clusters, and the comments are where that happens. If you would rather vote than write, the same question is a poll on my LinkedIn post for this article: https://www.linkedin.com/in/mirac-can-yilmaz

Summary

  • Observability is answering "what is happening, and since when" from the outside. Metrics are the half that alerts run on.
  • The delay from failure to alert is three waits: scrape interval, evaluation interval, and for. With defaults a user can be first; shortened, the pager is first.
  • up == 0 does not fire for a pod that was removed; use absent() for the target-gone case.
  • Faster alerting trades against false pages and load. Pick for from how bad a false page is, and expect to revisit it.

Next in the series

This is post 6 of Core Skills. The previous one, Docker Best Practices: CMD vs ENTRYPOINT and ADD vs COPY, looked at what one word in a Dockerfile does to a container's shutdown; this one looked at how a cluster tells you a pod stopped, and who hears it first. Next: reading the three signals together, logs and traces alongside these metrics, when a single dashboard is not enough.

The code for this lab (the sample app, the ServiceMonitor and PrometheusRule, the kube-prometheus-stack values and the kind manifests) is shared with Lab Access members on Buy Me a Coffee; the repo invite comes by email after you join: https://buymeacoffee.com/miraccanyilmaz/membership

About the author

Mirac Can Yılmaz

Mirac Can Yılmaz DevOps & Platform Engineer

I work as a DevOps and platform engineer with Kubernetes, GitOps (Argo CD), CI/CD and Terraform. Every post here was tried and measured in my own lab, on kind and Talos clusters: the commands, the errors and the numbers as they happened. The lab code is shared with Lab Access members on Buy Me a Coffee.

Hayatta en hakiki mürşit ilimdir.
Mustafa Kemal Atatürk