Kafka UI (also known as kafka-ui by Provectus Labs) is the most-starred open-source Kafka web console on GitHub. Unlike Kafdrop, it's built as a full-featured management interface: multi-cluster support, ACL management, schema registry integration, Kafka Connect management, and consumer group lag visualization. It runs as a Spring Boot service and talks directly to Kafka brokers and, optionally, Schema Registry and Kafka Connect.
Because Kafka UI sits at the intersection of multiple infrastructure services, it has more failure surfaces than a simple read-only UI: it can be up but unable to reach one cluster, show correct topic metadata but stale consumer lag, or proxy Schema Registry requests while the registry itself is down. This tutorial shows you how to monitor Kafka UI's uptime, cluster connectivity, consumer group health, and broker metrics display with Vigilmon HTTP monitors.
Why Kafka UI Needs Its Own Monitoring Layer
Kafka UI's Spring Boot process staying alive does not mean it's working correctly:
- Cluster disconnection — Kafka UI retains its process but returns error cards for all cluster-specific views if the Kafka bootstrap address is unreachable
- Stale consumer group data — consumer lag tables cache data from the Kafka admin client; a client connection drop causes stale display without surfacing an error
- Schema Registry proxy failures — if Schema Registry is down, Kafka UI schema browser returns errors while all other views remain functional
- Multi-cluster partial failure — in a multi-cluster Kafka UI deployment, one cluster can be unreachable while others appear healthy
- Metrics endpoint degradation — broker JMX metrics displayed in Kafka UI require a separate polling path; loss of that path silently zeros out the metrics charts
Vigilmon catches these conditions by probing the API paths that back each UI view.
Step 1: Verify Kafka UI's Built-In Health Endpoints
Kafka UI exposes Spring Boot Actuator health endpoints at /actuator:
# Spring Boot health endpoint
curl -s http://localhost:8080/actuator/health | jq .
# Expected output:
# { "status": "UP" }
# Full details (requires management.endpoint.health.show-details=always)
curl -s http://localhost:8080/actuator/health | jq '.components'
Verify the cluster-specific API endpoint that powers the cluster overview page:
# List configured clusters
curl -s http://localhost:8080/api/clusters | jq '[.[] | {name, status}]'
# Expected output:
# [
# {"name": "local", "status": "online"},
# {"name": "staging", "status": "online"}
# ]
The /api/clusters endpoint is the most important signal for Kafka UI health: a cluster shown as "status": "offline" means Kafka UI cannot reach the Kafka bootstrap servers for that cluster.
Step 2: Build a Kafka UI Health Probe
Create a probe that aggregates Kafka UI's health signals into Vigilmon-readable endpoints:
# kafka_ui_probe.py
import os
import json
import requests
from http.server import HTTPServer, BaseHTTPRequestHandler
KAFKA_UI_URL = os.environ.get('KAFKA_UI_URL', 'http://localhost:8080')
PROBE_PORT = int(os.environ.get('PROBE_PORT', '8090'))
EXPECTED_CLUSTERS = os.environ.get('EXPECTED_CLUSTERS', '').split(',')
EXPECTED_CLUSTERS = [c.strip() for c in EXPECTED_CLUSTERS if c.strip()]
def check_actuator():
"""Check Spring Boot health."""
try:
r = requests.get(f'{KAFKA_UI_URL}/actuator/health', timeout=5)
if r.status_code == 200 and r.json().get('status') == 'UP':
return True, None
return False, f'actuator_{r.status_code}'
except Exception as e:
return False, str(e)
def check_clusters():
"""Verify all configured clusters are online."""
try:
r = requests.get(f'{KAFKA_UI_URL}/api/clusters', timeout=10)
if r.status_code != 200:
return False, [], f'clusters_api_{r.status_code}'
clusters = r.json()
offline = [c['name'] for c in clusters if c.get('status') != 'online']
return len(offline) == 0, clusters, offline
except Exception as e:
return False, [], str(e)
def check_expected_clusters(clusters):
"""Verify that all expected clusters are present and online."""
if not EXPECTED_CLUSTERS:
return []
names = {c['name']: c.get('status') for c in clusters}
missing = []
for expected in EXPECTED_CLUSTERS:
if expected not in names:
missing.append(f'{expected}_not_found')
elif names[expected] != 'online':
missing.append(f'{expected}_status_{names[expected]}')
return missing
def check_consumer_groups(cluster_name):
"""Verify consumer group data is accessible for a cluster."""
try:
r = requests.get(
f'{KAFKA_UI_URL}/api/clusters/{cluster_name}/consumer-groups',
timeout=10,
)
if r.status_code == 200:
groups = r.json()
return True, len(groups), None
return False, 0, f'consumer_groups_api_{r.status_code}'
except Exception as e:
return False, 0, str(e)
def check_broker_metrics(cluster_name):
"""Check that broker metric data is being returned."""
try:
r = requests.get(
f'{KAFKA_UI_URL}/api/clusters/{cluster_name}/metrics',
timeout=10,
)
if r.status_code == 200:
metrics = r.json()
# Verify at least some broker metrics are non-null
broker_count = metrics.get('brokerCount', 0)
active_controllers = metrics.get('activeControllers', None)
return True, {
'broker_count': broker_count,
'active_controllers': active_controllers,
}, None
return False, {}, f'metrics_api_{r.status_code}'
except Exception as e:
return False, {}, str(e)
def check_topics(cluster_name):
"""Verify topic list API responds."""
try:
r = requests.get(
f'{KAFKA_UI_URL}/api/clusters/{cluster_name}/topics',
timeout=10,
)
if r.status_code == 200:
data = r.json()
topics = data.get('topics', data) if isinstance(data, dict) else data
return True, len(topics) if isinstance(topics, list) else 0, None
return False, 0, f'topics_api_{r.status_code}'
except Exception as e:
return False, 0, str(e)
class ProbeHandler(BaseHTTPRequestHandler):
def do_GET(self):
from urllib.parse import urlparse, parse_qs
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
if parsed.path == '/health/kafka-ui':
self._check_all()
elif parsed.path == '/health/kafka-ui/clusters':
self._check_clusters()
elif parsed.path == '/health/kafka-ui/consumers':
cluster = (params.get('cluster') or [''])[0]
self._check_consumers(cluster)
elif parsed.path == '/health/kafka-ui/metrics':
cluster = (params.get('cluster') or [''])[0]
self._check_metrics(cluster)
elif parsed.path == '/health/kafka-ui/topics':
cluster = (params.get('cluster') or [''])[0]
self._check_topics(cluster)
else:
self.send_response(404)
self.end_headers()
def _respond(self, code, body):
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(body).encode())
def _check_all(self):
issues = []
up, err = check_actuator()
if not up:
return self._respond(503, {'status': 'down', 'reason': err})
clusters_ok, clusters, offline = check_clusters()
if not clusters_ok and not clusters:
return self._respond(503, {'status': 'down', 'reason': 'clusters_api_failed'})
if offline:
issues.append(f'offline_clusters: {",".join(offline)}')
missing = check_expected_clusters(clusters)
if missing:
issues.append(f'expected_clusters_missing: {",".join(missing)}')
# Spot-check the first online cluster
online_clusters = [c['name'] for c in clusters if c.get('status') == 'online']
if online_clusters:
cluster = online_clusters[0]
cg_ok, cg_count, cg_err = check_consumer_groups(cluster)
if not cg_ok:
issues.append(f'consumer_groups_unavailable: {cg_err}')
if issues:
return self._respond(503, {
'status': 'degraded',
'issues': issues,
'total_clusters': len(clusters),
'online_clusters': len([c for c in clusters if c.get('status') == 'online']),
})
self._respond(200, {
'status': 'ok',
'total_clusters': len(clusters),
'online_clusters': len(online_clusters),
})
def _check_clusters(self):
ok, clusters, offline = check_clusters()
if not ok and not clusters:
return self._respond(503, {'status': 'down', 'reason': 'clusters_api_failed'})
if offline:
return self._respond(503, {
'status': 'degraded',
'offline_clusters': offline,
'total': len(clusters),
})
self._respond(200, {
'status': 'ok',
'clusters': [{'name': c['name'], 'status': c.get('status')} for c in clusters],
})
def _check_consumers(self, cluster):
if not cluster:
_, clusters, _ = check_clusters()
online = [c['name'] for c in clusters if c.get('status') == 'online']
cluster = online[0] if online else ''
if not cluster:
return self._respond(503, {'status': 'down', 'reason': 'no_online_cluster'})
ok, count, err = check_consumer_groups(cluster)
if not ok:
return self._respond(503, {'status': 'down', 'reason': err, 'cluster': cluster})
self._respond(200, {'status': 'ok', 'consumer_group_count': count, 'cluster': cluster})
def _check_metrics(self, cluster):
if not cluster:
_, clusters, _ = check_clusters()
online = [c['name'] for c in clusters if c.get('status') == 'online']
cluster = online[0] if online else ''
if not cluster:
return self._respond(503, {'status': 'down', 'reason': 'no_online_cluster'})
ok, metrics, err = check_broker_metrics(cluster)
if not ok:
return self._respond(503, {'status': 'down', 'reason': err, 'cluster': cluster})
if metrics.get('broker_count', 0) == 0:
return self._respond(503, {
'status': 'degraded',
'reason': 'zero_brokers_in_metrics',
'cluster': cluster,
})
self._respond(200, {'status': 'ok', 'cluster': cluster, **metrics})
def _check_topics(self, cluster):
if not cluster:
_, clusters, _ = check_clusters()
online = [c['name'] for c in clusters if c.get('status') == 'online']
cluster = online[0] if online else ''
if not cluster:
return self._respond(503, {'status': 'down', 'reason': 'no_online_cluster'})
ok, count, err = check_topics(cluster)
if not ok:
return self._respond(503, {'status': 'down', 'reason': err, 'cluster': cluster})
self._respond(200, {'status': 'ok', 'topic_count': count, 'cluster': cluster})
def log_message(self, *args):
pass
if __name__ == '__main__':
print(f'Kafka UI probe listening on :{PROBE_PORT}')
HTTPServer(('0.0.0.0', PROBE_PORT), ProbeHandler).serve_forever()
Run it alongside Kafka UI:
pip install requests
KAFKA_UI_URL=http://localhost:8080 \
PROBE_PORT=8090 \
EXPECTED_CLUSTERS=local,staging \
python kafka_ui_probe.py
Or in Docker Compose:
# docker-compose.yml
version: '3.8'
services:
kafka:
image: bitnami/kafka:3.7
environment:
KAFKA_CFG_PROCESS_ROLES: "broker,controller"
KAFKA_CFG_NODE_ID: "1"
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093"
KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093"
KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092"
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
kafka-ui:
image: provectuslabs/kafka-ui:latest
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: "health,info,prometheus"
depends_on:
- kafka
kafka-ui-probe:
image: python:3.12-slim
ports:
- "8090:8090"
environment:
KAFKA_UI_URL: "http://kafka-ui:8080"
PROBE_PORT: "8090"
EXPECTED_CLUSTERS: "local"
command: ["sh", "-c", "pip install requests -q && python /probe/kafka_ui_probe.py"]
volumes:
- ./kafka_ui_probe.py:/probe/kafka_ui_probe.py
depends_on:
- kafka-ui
Step 3: Monitor Consumer Group Lag Display Health
Consumer group lag is one of Kafka UI's most-used views. Verify it returns data:
# Test consumer group API for a cluster named "local"
curl -s http://localhost:8080/api/clusters/local/consumer-groups | jq 'length'
# Inspect lag for a specific group
curl -s "http://localhost:8080/api/clusters/local/consumer-groups/my-consumer-group" \
| jq '{groupId:.groupId, status:.state, partitionAssignmentStrategy:.partitionAssignmentStrategy}'
The Vigilmon monitor at /health/kafka-ui/consumers?cluster=local probes this path automatically.
Step 4: Configure Vigilmon Monitors
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your probe:
https://your-host.example.com/health/kafka-ui - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty integration
Complete monitor table:
| Monitor URL | Purpose | Interval | Alert |
|---|---|---|---|
| /health/kafka-ui | Full health check | 1 min | P1 – PagerDuty |
| http://kafka-ui:8080/actuator/health | Spring Boot process health | 30 sec | P1 – PagerDuty |
| /health/kafka-ui/clusters | Cluster connectivity status | 1 min | P1 – PagerDuty |
| /health/kafka-ui/consumers?cluster=local | Consumer group data availability | 2 min | P2 – Slack |
| /health/kafka-ui/metrics?cluster=local | Broker metrics display health | 5 min | P2 – Slack |
| /health/kafka-ui/topics?cluster=local | Topic list API health | 5 min | P2 – Slack |
Step 5: Heartbeat Monitor for Kafka UI
Use a Vigilmon heartbeat to detect silent JVM crashes between HTTP check intervals:
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Name it
kafka-ui-liveness - Expected interval: 3 minutes, grace period: 6 minutes
- Copy the heartbeat URL
Send the heartbeat from a health-gated script:
# heartbeat.sh — run every 2 minutes via cron or Docker CMD wrapper
#!/bin/bash
KAFKA_UI_URL="${KAFKA_UI_URL:-http://localhost:8080}"
VIGILMON_URL="${VIGILMON_HEARTBEAT_URL}"
health=$(curl -sf --max-time 5 "$KAFKA_UI_URL/actuator/health" 2>/dev/null)
if echo "$health" | grep -q '"status":"UP"'; then
curl -sf "$VIGILMON_URL" || true
fi
For Kubernetes:
# kafka-ui-heartbeat-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: kafka-ui-heartbeat
namespace: kafka
spec:
schedule: "*/2 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: heartbeat
image: curlimages/curl:8.5.0
command:
- sh
- -c
- |
health=$(curl -sf --max-time 5 "$KAFKA_UI_URL/actuator/health")
if echo "$health" | grep -q '"status":"UP"'; then
curl -sf "$VIGILMON_HEARTBEAT_URL"
fi
env:
- name: KAFKA_UI_URL
value: "http://kafka-ui.kafka.svc:8080"
- name: VIGILMON_HEARTBEAT_URL
valueFrom:
secretKeyRef:
name: vigilmon-secrets
key: kafka-ui-heartbeat-url
Step 6: Multi-Cluster Alert Strategy
For Kafka UI deployments managing multiple clusters, configure per-cluster monitors:
# Check each cluster individually
for cluster in production staging dev; do
echo "Cluster: $cluster"
curl -s "http://localhost:8080/api/clusters/$cluster/metrics" \
| jq '{brokerCount:.brokerCount, activeControllers:.activeControllers}'
done
Vigilmon alert routing by cluster criticality:
| Cluster | Monitor | Alert Channel | Priority |
|---|---|---|---|
| production | /health/kafka-ui/clusters + all sub-checks | PagerDuty | P1 |
| staging | /health/kafka-ui/clusters | Slack | P2 |
| dev | Heartbeat only | Slack | P3 |
Summary
Kafka UI by Provectus is the most capable open-source Kafka management console, but its multi-service dependencies (Kafka, Schema Registry, Kafka Connect) mean a single downstream failure can degrade the UI in ways that aren't obvious from the outside. The Vigilmon monitors in this tutorial give you full coverage: Spring Boot process health, cluster connectivity, consumer group data availability, broker metrics, and a heartbeat liveness check.
| Monitor | What It Catches |
|---|---|
| Spring Boot actuator | JVM crashes, OOM restarts, startup failures |
| /health/kafka-ui | Cluster offline, consumer data unavailable |
| /health/kafka-ui/clusters | Any cluster disconnected from Kafka UI |
| /health/kafka-ui/consumers | Consumer lag view serving errors |
| /health/kafka-ui/metrics | Broker JMX metrics zeroed or unavailable |
| Heartbeat: kafka-ui-liveness | Silent crashes between HTTP check windows |
Get started free at vigilmon.online — Kafka UI monitoring deployed and running in under five minutes.