Challenge
DifficultyMedium
Learner activity No attempts yet

Kubernetes Observability On-Call: Six Tickets with Prometheus, Loki, and Jaeger

Test your Kubernetes observability skills on a real 3-node cluster. Work through six on-call tickets: fix a broken scrape, repair an alert that never fires, find a hidden slowdown in traces, answer a question with LogQL, and fix a throttled workload. Every ticket is checked automatically.

You're on call today at HotROD, a small ride-hailing company. Six support tickets are waiting for you. Each one is a real problem in the cluster in front of you. To solve them, you'll use metrics, logs and traces, and part of the job is choosing the right one.

This lab tests what you know. It doesn't teach the theory. If a term is new to you, read the companion post, How observability works in Kubernetes. Each ticket links to the section you need.

How a ticket works

  1. Read the message from the person who opened the ticket.
  2. Do the steps under See the problem. They tell you where to look and what you'll see while the problem is still there. Seeing it first is part of the job, and it gives you something to compare with later.
  3. Read the Done when line. It says exactly what the lab checks, and what you'll see once it's fixed.
  4. Investigate and fix the problem, any way you like.
  5. The lab checks the cluster every few seconds and closes the ticket by itself when the problem is fixed.

Two tickets ask a question instead. Type your answer and click Submit. If the box turns red, that isn't the answer yet, so look again and try another.

Tickets unlock one at a time, in order. If you get stuck, open the Nudge first, and the Solution only if you need it.

Where things run

Every command and query in this lab says where it goes:

  • Terminal: the dev-machine tab. Commands in sh blocks, such as kubectl, run here.
  • Prometheus tab, Query page: PromQL queries. Paste the query into the box at the top, click Execute, and read the Table view.
  • Grafana tab, Explore: LogQL queries for logs. Pick the Loki data source and switch the editor to Code.
  • Jaeger tab: traces. There are no queries to type; you search and click.
Note

💡 Give the cluster a couple of minutes after it starts. The first ticket unlocks when everything is ready.

The System You're On Call For

HotROD, the main product

HotROD ("Rides on Demand") is the company's app. A customer asks for a ride, and HotROD finds the driver who can get there first. It's Jaeger's demo application: four small services that run together in one Deployment, hotrod, in the demo namespace.

Open the HotROD tab and click a customer to book a ride yourself.

Note

💡 After each ride, HotROD shows find trace and open trace links. They point to localhost:16686, so they don't work in this playground. To see a ride's trace, right-click open trace and copy the link. The long ID at the end of the link is the trace ID. Paste it into the search box in the Jaeger tab.

Every ride request follows the same path:

 customer ──▶ frontend  GET /dispatch?customer=123
                │
                ├─1─▶ customer   GET /customer       "where is this customer?"
                │        └─▶ MySQL   SQL SELECT
                │
                ├─2─▶ driver     FindNearest (gRPC)  "which drivers are close?"
                │        └─▶ Redis   FindDriverIDs, then GetDriver for each one
                │
                ├─3─▶ route      GET /route          "how long would this driver take?"
                │                (one call per candidate driver)
                │
                └──▶ answer: the driver with the shortest ETA

A few things to know about HotROD:

  • MySQL and Redis aren't real databases. HotROD simulates them. They appear in traces as the services mysql and redis-manual, but there are no Pods for them.
  • HotROD produces all three signals: Prometheus metrics, structured logs that include a trace ID, and a trace for every request.
  • A Deployment called loadgen books a ride every 2 seconds, so there's always traffic to look at.

The other services

Three smaller services run next to HotROD in demo. They're stand-ins: each does just enough to act like the real thing.

ServiceIts job at HotRODWhat it produces
checkoutTakes payment when a ride ends. Its team has just moved it to Kubernetes.Prometheus metrics on /metrics
api-gatewaySends each payment to the card company and records whether it was accepted.One JSON log line per payment
image-resizerResizes the photos drivers upload to their profiles. It runs in the background and has no web page.One JSON log line per image

The observability stack

Everything in the observability namespace is there to watch the services above:

ComponentWhat it does
PrometheusCollects and stores metrics, and checks alert rules. It's run by the Prometheus Operator, so what to scrape and when to alert are Kubernetes objects: ServiceMonitor and PrometheusRule.
AlertmanagerReceives alerts from Prometheus and would send them on to people.
kube-state-metrics, node-exporter, cAdvisorProduce metrics about Kubernetes objects, nodes and containers. cAdvisor is built into the kubelet.
Loki and AlloyLoki stores logs. Alloy runs on every node and sends every container's logs to Loki.
JaegerStores traces and shows them.
GrafanaOne web UI over all of the above, including a K8s Observability Playground dashboard.

Before You Start: Your Tools

You'll switch between these tabs. Take a minute to find your way around them before the first ticket.

TabUse it toWhere to click
dev-machineRun kubectl. It's already set up with admin access.Just type.
PrometheusRun PromQL queries, see what's scraped, see alertsQuery to run a query (switch between Table and Graph). Status > Target health lists every scrape target. Alerts shows each rule's state.
GrafanaQuery logs, look at dashboardsExplore (compass icon), then pick Loki as the data source and switch the editor to Code to type a LogQL query. Dashboards lists the dashboards. You're logged in as an anonymous admin.
JaegerFind and read tracesPick a Service, click Find Traces, then click a trace to see its spans as a timeline. The search box at the top takes a trace ID.
HotRODUse the productClick a customer to book a ride.

Ticket 1: "We can't see checkout's metrics"

From: Priya, lead developer of checkout

We deployed checkout to demo and a colleague added a ServiceMonitor for it. It's been an hour and there's nothing in Prometheus. The app serves metrics on /metrics. Can you get it scraped?

See the problem. In the Prometheus tab, Query page, run:

up{namespace="demo", service="checkout"}

You'll see Empty query result. up is the series Prometheus records for every target it scrapes, so an empty result means Prometheus isn't scraping checkout at all. Under Status > Target health, there's no checkout target either.

Done when: Prometheus scrapes checkout. Run the same query again: it returns one row with the value 1. A new target can take a minute or two to appear.

Theory: how Prometheus finds what to scrape.

Nudge

A ServiceMonitor doesn't look at Pods. It finds Services by their labels, then uses the Service port with the name it gives. Compare the two objects. In the terminal:

kubectl -n demo get servicemonitor checkout -o yaml
kubectl -n demo get service checkout -o yaml
Solution

The Service has the label app.kubernetes.io/name: checkout, and its port is named http. Replace the ServiceMonitor's selector and fix the port name. Use a JSON patch, because a merge patch would keep the old app label in the selector. In the terminal:

kubectl -n demo patch servicemonitor checkout --type=json -p '[
  {"op": "replace", "path": "/spec/selector/matchLabels", "value": {"app.kubernetes.io/name": "checkout"}},
  {"op": "replace", "path": "/spec/endpoints/0/port", "value": "http"}
]'

The target appears in Status > Target health within a minute or two.

Ticket 2: "Checkout was down and nobody got paged"

From: Sam, engineering manager (after this morning's incident review)

Last night a bad rollout scaled checkout to zero for five minutes. The CheckoutDown alert exists, but it never fired. We want it to fire within 2 minutes whenever checkout has no healthy target - including when there are no pods at all.

See the problem.

  1. In the Prometheus tab, open the Alerts page. CheckoutDown is listed as inactive: its condition isn't true, so it isn't about to fire. Expand it to see its expression and its for time.
  2. In the terminal, look at the rule as a Kubernetes object:
    kubectl -n demo get prometheusrule checkout-alerts -o yaml
    
  3. In the Prometheus tab, Query page, run the rule's expression, up{job="checkout-svc"} == 0. You'll see Empty query result, and it stays empty whether checkout is up or down. Then run up{job="checkout"} and compare.

Done when: all three of these are true:

  1. you've fixed the CheckoutDown alert rule,
  2. you've scaled checkout to 0 replicas to test it, and
  3. CheckoutDown is firing. On the Alerts page it turns pending first, then firing.

You'll bring checkout back at the very end of the lab.

Theory: alerting on symptoms.

Nudge

Run up{job="checkout"} on the Prometheus tab's Query page twice: once with checkout running, and once after scaling it to 0 in the terminal (kubectl -n demo scale deployment checkout --replicas=0). What does it return when there are no pods?

Solution

In the terminal:

kubectl -n demo patch prometheusrule checkout-alerts --type=json -p '[
  {"op": "replace", "path": "/spec/groups/0/rules/0/expr", "value": "absent(up{job=\"checkout\"} == 1)"},
  {"op": "replace", "path": "/spec/groups/0/rules/0/for", "value": "1m"}
]'
kubectl -n demo scale deployment checkout --replicas=0

Prometheus loads the new rule in about a minute. The alert is then pending for 1 minute, and then firing.

Ticket 3: "Rides got slower after this morning's change"

From: Lena, HotROD backend team

Someone changed HotROD's configuration this morning. Since then, booking a ride takes about a second instead of 0.7 s. Nothing is failing and the logs look normal. Which service is now the bottleneck?

See the problem. In the Jaeger tab, choose the service frontend and click Find Traces. Each frontend: GET /dispatch trace takes about 1 second. Click one to open its timeline: every bar is a span, and its position shows when it started.

Done when: you type the name of the service that is causing the extra time into the answer box below, and the lab accepts it.

Theory: reading a trace.

Nudge

Open a recent frontend trace in Jaeger. Look at when each span starts, not only how long it takes.

Solution

In a recent GET /dispatch trace, the ten route spans don't overlap any more. Each one starts when the previous one ends. The answer is route.

Ticket 4: "...and fix it, please"

From: Lena, HotROD backend team

Thanks! Can you undo whatever causes it?

See the problem. In the Prometheus tab, Query page, run this query. It's the average time HotROD took to answer a ride request over the last minute, in seconds:

sum(rate(hotrod_request_latency_sum{endpoint="GET_/dispatch"}[1m]))
  / sum(rate(hotrod_request_latency_count{endpoint="GET_/dispatch"}[1m]))

Right now it's about 1 (one second).

Done when: both of these are true:

  1. HotROD no longer runs with the setting that slowed it down, and
  2. the same query returns less than 0.85. Give it a minute after your fix, because it's an average over the last minute.
Nudge

The route calls behave as if only one may run at a time. Look at the options HotROD is started with. In the terminal:

kubectl -n demo get deploy hotrod -o jsonpath='{.spec.template.spec.containers[0].args}'
Solution

--fix-route-worker-pool-size=1 allows only one worker for route calls. The default is 3. Remove the option. In the terminal:

kubectl -n demo patch deploy hotrod --type=json \
  -p '[{"op": "replace", "path": "/spec/template/spec/containers/0/args", "value": ["all"]}]'

The latency is an average over the last minute, so it takes about a minute to come down.

Ticket 5: "Who keeps getting declined?"

From: Marco, customer support

api-gateway logs every payment as JSON. We want to call the customer with the most rejected payments in the last 15 minutes.

See the problem. In the Grafana tab, open Explore, pick the Loki data source, switch the editor to Code, and run:

{app="api-gateway"}

You'll see a stream of JSON lines, one per payment, with fields such as level, msg and customer. There are far too many to count by eye.

Done when: you type that customer's ID into the answer box below, and the lab accepts it.

Theory: labels versus log content.

Nudge

Start from the query above, in the Grafana tab's Explore view. Build on it in three steps:

  1. pick the gateway's logs by label,
  2. keep only the rejected payments, and
  3. count them per customer with count_over_time and sum by.
Solution

In the Grafana tab's Explore view, with Loki selected:

topk(3, sum by (customer) (count_over_time({app="api-gateway"} | json | level="error" [15m])))

Run it as an Instant query (one result, for now) rather than a Range query (a graph over time). The top result is customer 4711.

Ticket 6: "image-resizer is slow, but nothing's wrong with it"

From: Aisha, driver onboarding team (owners of image-resizer)

Resizing an image takes about 7 seconds. On a laptop it takes one. The Pod is Running, it never restarts, and the logs are clean.

See the problem. In the terminal, read the last few log lines:

kubectl -n demo logs deploy/image-resizer --tail=5

Each line shows how long one image took, in the seconds field. Right now it's about 7.

Done when: the same command shows seconds of 2 or less on the last three lines, from a Pod that's running your fix.

Theory: saturation and the USE method.

Nudge

Compare how much CPU the container uses with how much it's allowed. In the Prometheus tab, Query page, run this. The result is in CPU cores, so 0.05 means 50m:

rate(container_cpu_usage_seconds_total{namespace="demo", container="resizer"}[1m])

Then compare the result with the container's CPU limit, and look at container_cpu_cfs_throttled_periods_total.

Solution

The CPU use stays flat at the 50m limit. Raise the limit, or remove it. In the terminal:

kubectl -n demo set resources deployment image-resizer -c resizer --limits=cpu=1
kubectl -n demo logs deploy/image-resizer -f    # "seconds" drops to 0 or 1

The old Pod takes about 30 seconds to stop. Until it's gone, kubectl logs may still read from it and show 7-second jobs, so give it a moment before you look.

Close out

See the problem. checkout is still scaled to 0 from Ticket 2, so on the Prometheus tab's Alerts page, CheckoutDown is still firing.

Done when: all three of these are true:

  1. checkout is running again,
  2. up{namespace="demo", service="checkout"} returns 1 on the Prometheus tab's Query page, and
  3. CheckoutDown is back to inactive on the Alerts page, by itself.
Solution

In the terminal:

kubectl -n demo scale deployment checkout --replicas=1

CheckoutDown resolves once Prometheus has scraped the new Pod.

Shift Report

TicketWhat solved itThe trap
1. Missing metricsPrometheus targetsTwo silent mismatches between the ServiceMonitor and the Service
2. Silent alertPromQLNo pods means no series: == 0 can't fire, but absent() can
3. Slow ridesA traceNo span got slower: the calls stopped running at the same time
4. The fixHotROD's configurationA worker pool of 1
5. Declined paymentsA LogQL queryThe busiest customer isn't the one with the most rejections
6. Slow resizercAdvisor metricsSaturation, with no error anywhere

Want more? Run o11y-lab mystery in the terminal for a random incident with no ticket at all.

Problem
Start