Kafka / 12 Monitoring lab
All modules
Module 12 · Depth lab · Kafka 4.3

Monitoring lab

Module 09 listed the signals to watch but could not read them. This lab opens the brokers' metrics, reads each signal by its real name, runs Prometheus with alert rules, and stops a broker to watch the alerts fire and clear.

Try it: read the dashboard, name the problem

Pick a situation. The six tiles change to the values you would see. Decide what is wrong before you read the explanation.

offset 0

What it is and why it exists

Kafka hides trouble well. A cluster with one broker down still serves every request. A follower that is falling behind causes no error. A disk that fills in six hours looks fine now. Without measurements you learn about each of these when the second fault arrives and something stops.

Monitoring means three things: metrics that the brokers publish, a system that collects them over time, and alerts that tell a person when a number means trouble. The skill is not in collecting everything. It is in choosing a few signals, knowing what each one means, and deciding which ones are worth waking someone for.

The one-sentence version. Read a short list of broker and controller metrics, collect them over time, and alert on lost availability at once and on lost redundancy during working hours.

offset 1

Mental model

Sort every signal into one of three levels. The level decides the response.

LevelMeaningExamplesResponse
Availability lostSome clients are failing now.Offline partitions. Partitions below the in-sync minimum. No active controller.Page a person.
Redundancy lostEverything works, with less protection.Under-replicated partitions. A broker down. A fenced broker.A ticket; fix in hours.
Capacity shrinkingSomething will run out.Disk filling. Request threads busy. Lag growing.A ticket; fix in days.

Use the tiles at the top of the page. In each situation, find the first tile that turned red and decide its level.

Two sources of numbers

The two can give different numbers for what sounds like the same thing. In the lab, with one broker stopped, the exporter counted 57 under-replicated partitions for the cluster, and broker 1's own metric said 31. Both were right: a broker counts only the partitions it leads.

offset 2

How it works internally

Metric names

A broker metric has a domain, a type and a name, written like kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions. Some have more labels, such as the request type. Each has one or more attributes: a simple Value, or for rates and timings a Count, rates over 1, 5 and 15 minutes, and percentiles.

The signals, by their real names

Every name in this table was read from a lab broker with kafka-jmx.sh. The value column shows what a healthy lab cluster returned.

MetricHealthy value seenMeaning
kafka.controller:type=KafkaController,name=OfflinePartitionsCount0Partitions with no leader.
kafka.controller:type=KafkaController,name=ActiveControllerCount0 on broker 1; 1 on the active controller1 on exactly one node. Add it up over all controllers: the sum must be 1.
kafka.controller:type=KafkaController,name=FencedBrokerCount0Registered brokers that may not lead.
kafka.server:type=ReplicaManager,name=UnderMinIsrPartitionCount0Partitions this broker leads that are below the in-sync minimum.
kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions0Partitions this broker leads with fewer in-sync replicas than replicas.
kafka.server:type=ReplicaManager,name=IsrShrinksPerSecCount 0How often in-sync sets shrink. A steady rate with no broker failure means a broker at its limit.
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercentabout 0.98Share of time the request threads are idle, from 0 to 1. Low means saturated.
kafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercent0.48 during a testThe same for network threads.
kafka.network:type=RequestMetrics,name=TotalTimeMs,request=ProducepercentilesTime to serve a produce request. Use the 99th percentile. The same exists for request=FetchConsumer. unverified: only Produce was read
kafka.server:type=BrokerTopicMetrics,name=BytesInPerSeca rateBytes written to this broker.
kafka.server:type=raft-metricscurrent-leader=2, current-epoch=1, current-state=followerThe controller quorum as this node sees it. A rising epoch means elections.

From metrics to alerts

  1. A collector, here Prometheus, reads the numbers every few seconds and stores them with a timestamp.
  2. A rule is an expression over those numbers, for example "the sum of under-replicated partitions is above 0".
  3. A rule has a for time. The condition must hold that long before the alert fires. This filters out a restart that lasts ten seconds.
  4. A firing alert carries a severity label, which decides if it pages a person or opens a ticket.
  5. When the condition stops, the alert clears by itself.

Lag is measured elsewhere

Consumer lag is not a broker metric. It is the difference between a partition's end offset and a group's committed offset, so something must read both. The command-line tool does this (module 03). An exporter can do it too; the one in this lab could not read groups from Kafka 4.3, so the lab has no lag metric.

offset 3

Every relevant config

These are the settings of the lab's monitoring stack, in docker-compose.yml and the folder monitoring/. All of them ran as written.

WhereSettingLab valueWhat it does
Broker containerKAFKA_JMX_PORT9101Opens the broker's metrics port.
Broker containerKAFKA_JMX_HOSTNAMElocalhostThe name the metrics interface announces. With localhost, read it from inside the same container.
Exporter--kafka.serverthe three brokersWhere the exporter connects, as a client.
Exporter--kafka.version3.6.0The protocol version its client library uses.
Prometheusscrape_interval5 secondsHow often numbers are read. 15 to 60 seconds is usual in production.
Prometheusevaluation_interval5 secondsHow often rules are checked.
Alert rulefor15 to 30 secondsHow long a condition must hold before the alert fires. Short here so that the lab is quick; minutes in production.
Alert ruleseverity labelpage or ticketRoutes the alert to the right response.
Grafanadata source filepoints at PrometheusLets you build dashboards at http://localhost:3005. The lab ships no ready-made dashboard.

The alert rules in the lab

- alert: KafkaBrokerDown
  expr: kafka_brokers < 3
  for: 15s
  labels: { severity: page }

- alert: KafkaUnderReplicatedPartitions
  expr: sum(kafka_topic_partition_under_replicated_partition) > 0
  for: 30s
  labels: { severity: ticket }

- alert: KafkaPartitionBelowMinimum
  expr: sum(kafka_topic_partition_in_sync_replica < 2) > 0
  for: 15s
  labels: { severity: page }

In the lab, "broker down" pages, so that you see a page-level alert fire. On a real cluster with three copies and a minimum of 2, one broker down is redundancy lost: a ticket, not a page. Decide severity from what the alert means for clients.

offset 4

Failure scenarios

The monitoring tool cannot read the new Kafka version

You observe
In the lab, the exporter logged "Cannot get consumer group: kafka: broker not connected" every few seconds and published no lag metric. Topic and broker metrics worked.
Cause
The exporter's client library and Kafka 4.3 do not agree on a request. Kafka 4 removed old protocol versions. unverified: I did not find the exact cause
Lesson
Test your monitoring against a new Kafka version before the upgrade, with the same care as an application. A dashboard that goes blank during an upgrade is the worst moment to lose it.

An alert rule that can never fire

What breaks
A rule refers to a metric that does not exist. It evaluates to nothing, shows as "inactive", and looks healthy for ever.
Evidence
My first version of the lab's rules included a lag alert on a metric the exporter never published. Prometheus loaded it without complaint.
Fix
Test every alert by causing the fault, as this lab does. Add an alert for "metric absent".

Counting a per-broker metric as a cluster total

What breaks
A dashboard shows one broker's "under-replicated partitions" and someone reads it as the cluster's number. Or it shows ActiveControllerCount of 0 from one node and someone declares an emergency.
Evidence
Broker 1 reported 31 while the cluster had 57. Broker 1 reported an active controller count of 0 while node 2 was the active controller.
Fix
Add per-broker metrics up over all brokers. For the controller count, alert when the sum is not exactly 1.

Alerts that nobody believes

What breaks
Every rolling restart fires ten alerts. People mute them. The real alert, a week later, is muted too.
Fix
Give each rule a for time longer than a normal restart. Silence alerts during planned maintenance. Remove any alert that has never led to an action.

Watching the average

What breaks
Average produce time is 4 ms and looks fine while one request in a hundred takes two seconds.
Fix
Alert on the 99th percentile. The timing metrics publish percentiles for this reason.

The metrics port is open to everyone

What breaks
The Java management interface can do more than read numbers. An open, unauthenticated port is a way into the broker.
Fix
Keep it on a private network, or require authentication for it. The lab opens it only inside the Docker network.
offset 5

Hands-on lab

Every command below was run on 5 October 2026 on the main lab cluster. The monitoring containers are in the main docker-compose.yml but start only when you ask for the monitoring profile.

Step 1. Start the cluster with monitoring

cd ~/Desktop/kafka-knowledge-base
docker compose --profile monitoring up -d
k() { docker exec -i kafka-1 /opt/kafka/bin/"$@"; }
B=kafka-1:19092

Prometheus is at http://localhost:9095 and Grafana at http://localhost:3005.

Step 2. Read broker metrics by name

J() { docker exec -i kafka-1 timeout 20 /opt/kafka/bin/kafka-jmx.sh \
  --jmx-url service:jmx:rmi:///jndi/rmi://localhost:9101/jmxrmi \
  --one-time true --report-format properties --object-name "$1"; }

J 'kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions'
J 'kafka.controller:type=KafkaController,name=ActiveControllerCount'
J 'kafka.controller:type=KafkaController,name=OfflinePartitionsCount'
J 'kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent'
J 'kafka.server:type=raft-metrics'
kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions:Value=0
kafka.controller:type=KafkaController,name=ActiveControllerCount:Value=0
kafka.controller:type=KafkaController,name=OfflinePartitionsCount:Value=0
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent:FifteenMinuteRate=0.9778922787520811
kafka.server:type=raft-metrics:current-leader=2
kafka.server:type=raft-metrics:current-epoch=1
kafka.server:type=raft-metrics:current-state=follower

Look at the second line. The active controller count on node 1 is 0, and that is healthy: the quorum's leader is node 2, as the last lines say. Run the same command in kafka-2 and you get 1.

Step 3. Check the collector

curl -s http://localhost:9095/api/v1/targets | python3 -c "import sys,json;[print(t['labels']['job'],t['health']) for t in json.load(sys.stdin)['data']['activeTargets']]"
curl -s http://localhost:9095/api/v1/rules   | python3 -c "import sys,json;[print(r['name'],r['state']) for g in json.load(sys.stdin)['data']['groups'] for r in g['rules']]"
kafka up

KafkaBrokerDown inactive
KafkaUnderReplicatedPartitions inactive
KafkaPartitionBelowMinimum inactive

The exporter publishes these metric names: kafka_brokers, kafka_topic_partitions, kafka_topic_partition_current_offset, kafka_topic_partition_oldest_offset, kafka_topic_partition_leader, kafka_topic_partition_leader_is_preferred, kafka_topic_partition_replicas, kafka_topic_partition_in_sync_replica and kafka_topic_partition_under_replicated_partition.

Step 4. Cause a fault and watch the alerts

k kafka-topics.sh --bootstrap-server $B --create --topic watched --partitions 6 --replication-factor 3
q() { curl -s --data-urlencode "query=$1" http://localhost:9095/api/v1/query; echo; }

q 'kafka_brokers'
docker stop kafka-3
# wait 50 seconds
q 'kafka_brokers'
q 'sum(kafka_topic_partition_under_replicated_partition)'
q 'ALERTS{alertstate="firing"}'
J 'kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions'
Output, reduced to the values
before:   kafka_brokers = 3

after:    kafka_brokers = 2
          under-replicated partitions, cluster total = 57
          firing: KafkaBrokerDown, KafkaUnderReplicatedPartitions
          broker 1's own UnderReplicatedPartitions = 31

Two alerts fired within 50 seconds. KafkaPartitionBelowMinimum did not, and that is correct: with one broker down, every partition still has two in-sync replicas. The cluster total, 57, counts internal topics too. Broker 1 reports only the 31 partitions it leads.

Step 5. Repair and watch them clear

docker start kafka-3
# wait 40 seconds
q 'kafka_brokers'
q 'sum(kafka_topic_partition_under_replicated_partition)'
q 'ALERTS{alertstate="firing"}'
kafka_brokers = 3
under-replicated partitions = 0
firing: (nothing)

Step 6. Look in the browser, then stop

Open http://localhost:9095/alerts to see the three rules and their state. In Grafana, the Prometheus data source is already connected: make a panel with the query kafka_brokers and repeat step 4 to watch the line drop.

docker compose --profile monitoring down
offset 6

Common mistakes and misconceptions

People thinkWhat is true
More dashboards mean better monitoring.A few signals that people understand beat fifty that nobody reads.
ActiveControllerCount of 0 on a node is an emergency.It is 1 on one node and 0 on the others. The sum must be 1.
A broker's under-replicated count is the cluster's.Each broker counts the partitions it leads. Add them up.
An alert rule that loads without error works.A rule on a missing metric loads and never fires. Test by causing the fault.
Every alert should page.Page for lost availability. Lost redundancy is a ticket.
Lag is a broker metric.It needs the end offset and the group's committed offset, read by a tool or an exporter.
Monitoring tools keep working across Kafka upgrades.The lab's exporter lost its group metrics on Kafka 4.3.
offset 7

Staff-level questions

You may have five alerts for a Kafka cluster. Which five?

Offline partitions above 0. The sum of active controllers not equal to 1. Partitions below the in-sync minimum above 0. Disk predicted to fill within a day. Consumer lag in time above each critical application's limit.

Those five mean clients are failing or are about to. Everything else, such as under-replicated partitions or thread saturation, is a ticket or a dashboard.

How do you alert on a disk before it is full?

Not on a level alone. Alert on the predicted time to full, from the growth rate over the last hours: a disk at 70% that grows 5% each hour is urgent, and a disk at 85% that has been flat for a month is not. Keep a simple level alert as a backstop.

What is the right way to measure consumer lag?

In time, not in records: how old is the oldest record the group has not processed? Record counts mean different things on a busy and a quiet topic. Compare the time with what the business accepts for that application, and with the topic's retention, because lag beyond retention is data loss.

Also alert when a partition's committed offset has not moved while its end offset has. That catches a stuck consumer before the backlog is large.

Produce latency at the 99th percentile doubled. How do you find the cause from metrics?

The request time metric splits into parts: time waiting in the queue, time on the local disk, time waiting for followers, time to send the answer. Queue time points to saturated request threads. Local time points to this broker's disk. Follower wait points to a slow follower or the network between brokers.

Then compare brokers. One broker different from the rest is a hardware or placement problem. All brokers together is load. unverified: the lab read only the total time

How do you keep monitoring correct through a Kafka upgrade?

Treat the monitoring stack as a client of the cluster with its own compatibility check. Before the upgrade, run the exporters and dashboards against a test cluster on the new version and cause each fault. Add alerts for missing metrics, so that a silent gap is itself an alert. Keep the command-line checks from module 09 in the runbook as a fallback that needs no extra tooling.

What do you show application teams, and what do you keep for the platform team?

Application teams: their own consumer lag in time, their produce error rate and latency, the size and retention of their topics, their quota use. Platform team: broker and controller health, disk, threads, replication.

Teams act on what they can change. A team that sees broker thread metrics can only worry about them.

offset 8

Self-test

1. Name the three levels of signal and the response to each.

Availability lost: page. Redundancy lost: ticket, hours. Capacity shrinking: ticket, days.

2. Which metric counts partitions with no leader?

kafka.controller:type=KafkaController,name=OfflinePartitionsCount.

3. What should the active controller count add up to?

Exactly 1 over all controllers.

4. Why did broker 1 report 31 under-replicated partitions when the cluster had 57?

A broker counts only the partitions it leads.

5. What does a rule's for time do?

The condition must hold that long before the alert fires, which filters out short events such as a restart.

6. With one of three brokers down, which lab alert stayed quiet, and why?

KafkaPartitionBelowMinimum. Every partition still had two in-sync replicas.

7. How do you know an alert rule works?

Cause the fault and watch it fire.

8. Where does a lag number come from?

From the partition's end offset minus the group's committed offset, read by a tool or an exporter.