New to KubeDB? Please start here.
Kafka Grafana Dashboard
KubeDB exposes Kafka metrics — including JVM, broker/topic, and KRaft controller/quorum metrics — through a JMX Exporter running as a Java agent inside each Kafka container, and its own view of each resource (status, phase, version) through Panopticon. Once Prometheus is scraping both, you can visualize them in Grafana using a pre-built KubeDB dashboard. This tutorial walks through the full setup: deploying the monitoring stack, enabling monitoring on a Kafka instance, and importing the Grafana dashboard.
Before You Begin
You need a Kubernetes cluster with
kubectlconfigured. If you do not already have a cluster, you can create one by using kind.KubeDB must be installed in your cluster with
kubedb-metricsenabled. Follow the setup guide here and make sure to include the flag below during installation:--set kubedb-metrics.enabled=truekubedb-metricscreatesMetricsConfigurationobjects for each database type, which Panopticon (see Configuration) uses to expose metrics to Prometheus.To keep monitoring resources isolated, we use a separate
monitoringnamespace and deploy the database in thedemonamespace.$ kubectl create ns monitoring namespace/monitoring created $ kubectl create ns demo namespace/demo created
- Before proceeding, complete the Configuration steps to deploy kube-prometheus-stack and Panopticon.
Note: YAML files used in this tutorial are stored in docs/examples/kafka/monitoring and docs/examples/kafka/tls folders in GitHub repository kubedb/docs.
Setup
Step 1: Deploy Kafka
Kafka runs in KRaft mode (no ZooKeeper), so the combined broker+controller nodes need at least 3 replicas to form a working Raft quorum — this is also what lets you see meaningful data in the dashboard’s KRaft Controller and KRaft Quorum panels later on.
This example also enables TLS, so first create a self-signed Issuer that KubeDB will use to issue certificates for the cluster:
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./ca.key -out ./ca.crt -subj "/CN=kafka/O=kubedb"
$ kubectl create secret tls kafka-ca \
--cert=ca.crt \
--key=ca.key \
--namespace=demo
secret/kafka-ca created
$ kubectl create -f https://github.com/kubedb/docs/raw/v2026.7.10/docs/examples/kafka/tls/kf-issuer.yaml
issuer.cert-manager.io/kafka-ca-issuer created
Below is the Kafka object with monitoring configured to use Prometheus Operator.
apiVersion: kubedb.com/v1alpha2
kind: Kafka
metadata:
name: kafka
namespace: demo
spec:
enableSSL: true
tls:
issuerRef:
apiGroup: cert-manager.io
name: kafka-ca-issuer
kind: Issuer
replicas: 3
version: 4.2.0
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageType: Durable
monitor:
agent: prometheus.io/operator
prometheus:
exporter:
port: 56790
serviceMonitor:
labels:
release: prometheus
interval: 10s
deletionPolicy: WipeOut
Here,
enableSSL: trueandtls.issuerRefturn on TLS using the self-signedIssuercreated above.replicas: 3deploys a 3-node combined KRaft cluster (each node acts as both broker and controller), which is required for the quorum panels to show meaningful data.monitor.agent: prometheus.io/operatortells KubeDB to create aServiceMonitorfor this instance.monitor.prometheus.exporter.portsets the port the JMX exporter serves metrics on.monitor.prometheus.serviceMonitor.labelsmust match theserviceMonitorSelectorlabel of your Prometheus (release: prometheus).monitor.prometheus.serviceMonitor.intervalsets the scrape interval to 10 seconds.
Create the Kafka instance:
$ kubectl create -f https://github.com/kubedb/docs/raw/v2026.7.10/docs/examples/kafka/monitoring/kf-with-monitoring.yaml
kafka.kubedb.com/kafka created
Wait for it to be Ready:
$ kubectl get kafka -n demo kafka
NAME VERSION STATUS AGE
kafka 4.2.0 Ready 5m
KubeDB creates a stats service named {kafka-name}-stats for monitoring:
$ kubectl get svc -n demo --selector="app.kubernetes.io/instance=kafka"
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kafka-pods ClusterIP None <none> 9092/TCP,9093/TCP,29092/TCP 5m
kafka-stats ClusterIP 10.96.10.2 <none> 56790/TCP 5m
KubeDB also creates a ServiceMonitor in the demo namespace:
$ kubectl get servicemonitor -n demo
NAME AGE
kafka-stats 5m
Verify it carries the correct label:
$ kubectl get servicemonitor -n demo kafka-stats -o jsonpath='{.metadata.labels}'
{"release":"prometheus", ...}
Step 2: Verify Prometheus is Scraping
Port-forward the Prometheus pod:
$ kubectl port-forward -n monitoring \
prometheus-prometheus-kube-prometheus-prometheus-0 9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
Open http://localhost:9090/targets in your browser. Look for an entry whose service label matches kafka-stats. Its state should be UP.

If the target is missing, check that the ServiceMonitor label (release: prometheus) matches the Prometheus serviceMonitorSelector.
Step 3: Access Grafana
Port-forward the Grafana service:
$ kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
Forwarding from 127.0.0.1:3000 -> 80
Open http://localhost:3000. The username is admin. Retrieve the auto-generated password from the secret:
$ kubectl get secret -n monitoring prometheus-grafana \
-o jsonpath='{.data.admin-password}' | base64 -d
| Field | Value |
|---|---|
| Username | admin |
| Password | output of the command above |

After a successful login you will see the Grafana home page:

Step 4: Configure Prometheus as a Data Source
If you installed Grafana via kube-prometheus-stack, Prometheus is already configured as the default data source — skip to Step 5.
If you’re using a different Grafana instance than the one installed in the Configuration prerequisite (linked in “Before You Begin” above), add Prometheus as a data source manually:
Go to Connections → Data sources → Add new data source.
Select Prometheus.
Set the URL to your Prometheus service:
http://prometheus-operated.monitoring.svc:9090Click Save & test. You should see
Data source is working.
Step 5: Import Dashboard — Option A: Automatic (chart)
Rather than downloading and uploading a JSON file by hand (Option B below), KubeDB ships a chart that creates matching dashboards for you as GrafanaDashboard custom resources. A separate controller, grafana-operator, watches these resources and pushes the actual dashboard JSON into your Grafana instance — both pieces are required.
Note: the chart’s bundled Kafka dashboards (
Summary/Pod/Database, installed by this option) are a different, newer set than the single combinedkafka_database_dashboard.jsondocumented in Option B below (sourced directly from opnpulse/dashboards). Both are valid, but they’re not the same dashboard — pick one option, not both, to avoid confusion in yourDashboardslist.
1. Install grafana-operator (skip if it’s already running in your cluster):
$ helm repo add appscode https://charts.appscode.com/stable/
$ helm repo update
$ helm upgrade --install grafana-operator appscode/grafana-operator \
--version v2026.6.12 \
--namespace kubeops --create-namespace
2. Register your Grafana instance as an AppBinding (skip if you’ve already done this in another guide on this cluster). grafana-operator needs to know where to push dashboards and how to authenticate — it reads this from an AppBinding object, not from the chart install command itself. Since Grafana here came bundled with kube-prometheus-stack, reuse its existing admin credentials:
$ kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80 &
$ curl -s -X POST -H "Content-Type: application/json" \
-u admin:<grafana_password> \
http://localhost:3000/api/auth/keys \
-d '{"name":"kubedb-dashboards","role":"Admin"}'
# Note the returned "key"
$ kill %1
$ kubectl create secret generic grafana-admin-token -n monitoring \
--from-literal=token='<key-from-above>'
$ cat <<EOF | kubectl apply -f -
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
metadata:
name: grafana
namespace: monitoring
spec:
type: monitoring.appscode.com/grafana
clientConfig:
url: http://prometheus-grafana.monitoring.svc:80
secret:
name: grafana-admin-token
EOF
3. Install the dashboards:
The chart packages dashboard JSON for every database it supports, so install it from a copy trimmed down to just Kafka’s dashboards (this also keeps grafana-operator from creating dashboards for databases you don’t run):
$ helm pull appscode/kubedb-grafana-dashboards --version v2026.7.10 --untar
$ cd kubedb-grafana-dashboards/dashboards
$ ls | grep -v '^kafka$' | xargs rm -rf # keep only dashboards/kafka (plain Kafka only, not ConnectCluster)
$ cd ../..
$ helm package kubedb-grafana-dashboards
Successfully packaged chart and saved it to: kubedb-grafana-dashboards-v2026.7.10.tgz
$ helm upgrade -i kubedb-grafana-dashboards-kafka ./kubedb-grafana-dashboards-v2026.7.10.tgz \
-n kubeops --create-namespace \
--set grafana.name=grafana \
--set grafana.namespace=monitoring
Use a release name unique to this database (
kubedb-grafana-dashboards-kafka), not the plainkubedb-grafana-dashboardsname — if you also follow another DB’s Grafana Dashboard guide on this same cluster, eachhelm upgrade -iunder a shared release name would prune the previous DB’s dashboards (Helm removes anything not in the new release’s manifest). A per-DB release name lets them coexist.
grafana.name/grafana.namespace point the chart’s GrafanaDashboard resources at the AppBinding created in step 2 (omitting them falls back to whichever AppBinding in your cluster is labeled as the cluster-default Grafana, if any — explicit is safer on a shared cluster). No need to touch featureGates — with every other database’s dashboards/ folder removed, their gates simply match zero files and render nothing, regardless of being true by default.
This creates KubeDB / Kafka / Summary, KubeDB / Kafka / Pod, and KubeDB / Kafka / Database — which grafana-operator then pushes into your Grafana instance automatically. No manual JSON download or upload needed.
Verify they landed:
$ kubectl get grafanadashboards.openviz.dev -n kubeops | grep -i "kafka-"
NAME TITLE SYNCED AGE
grafana-kubedb-kafka-summary KubeDB / Kafka / Summary Current 30s
grafana-kubedb-kafka-pod KubeDB / Kafka / Pod Current 30s
grafana-kubedb-kafka-database KubeDB / Kafka / Database Current 30s
SYNCED: Current confirms grafana-operator successfully pushed each dashboard into Grafana. Open Grafana — the dashboards are already there under Dashboards, fully wired to your Prometheus data source, ready to explore in Step 6 below.
If the SYNCED column is missing entirely from your output (not just showing a non-Current value), grafana-operator most likely never processed the resource at all. Check that the operator pod is actually running (kubectl get pods -n kubeops -l app.kubernetes.io/name=grafana-operator), inspect its logs for AppBinding/auth errors (kubectl logs -n kubeops deploy/grafana-operator), and check kubectl get grafanadashboards.openviz.dev -n kubeops <name> -o yaml for status.conditions — the CR existing only means kubectl accepted it, not that it reached Grafana.
Step 5: Import Dashboard — Option B: Manual (JSON upload)
If you’d rather not run grafana-operator, or specifically want the single combined dashboard documented here (see the note above), upload it by hand instead.
The KubeDB Kafka dashboard is distributed as a single JSON file: kafka_database_dashboard.json in the opnpulse/dashboards repository (kafka/ folder). The JSON file is a complete dashboard definition — panels, queries, variables, and layout — that Grafana loads in one shot. Without importing, you would have to build every panel and write every PromQL query by hand. Importing lets you skip that entirely.
Download kafka_database_dashboard.json from that repository, then:
- In Grafana, click the + icon in the left sidebar and select Import.
- Click Upload JSON file and select the downloaded file.
- In the datasource dropdown that appears, select your Prometheus data source.
- Click Import.
The import page looks like this:

After importing, the dashboard appears as KubeDB Kafka Dashboard under Dashboards in the left sidebar. It contains four sections: Kafka Server, Broker Topic Metrics, KRaft Controller Monitoring Metrics, and KRaft Quorum Monitoring Metrics.
Step 6: Explore the Dashboard
Use the dropdown filters at the top of the dashboard to focus on a specific instance.
| Variable | What to select |
|---|---|
| datasource | Your Prometheus data source |
| namespace | Namespace where your Kafka is deployed (e.g., demo) |
| service | Stats service of your Kafka instance (e.g., kafka-stats) |
| pod | A specific broker pod, or All for an aggregated view |
| container | Container to inspect (kafka) |
Kafka Server — JVM and process-level health of the selected broker:
- Status / Uptime / Start time — whether the broker’s JMX exporter is reachable, and how long it has been up
- JVM Version — the JVM the broker is running on
- Average number of CPUs used — CPU consumption of the process
- Memory area [heap] / [nonheap] — heap and non-heap JVM memory usage over time
- GC time increase / GC count increase — garbage collection pauses and frequency, by generation
- JVM classes loaded — number of loaded classes (a rough proxy for a leak if it keeps growing)
- Threads used — current and daemon JVM thread counts


Broker Topic Metrics — throughput at the broker/topic level:
- Messages in topics — incoming message rate
- Byte in / out rate from clients — client-facing produce/consume throughput
- Byte in / out rate from / to other brokers — inter-broker replication traffic
- Fetch request rate / Produce request rate — request throughput
- Failed produce request rate — produce requests that failed (should stay at 0)


KRaft Controller Monitoring Metrics — health of the KRaft metadata quorum’s controller layer:
- Number of Active Brokers / Number of active controllers — cluster membership; exactly one broker should be the active controller
- Fenced Broker Count — brokers the controller has fenced out (non-zero means a broker is unhealthy)
- Metadata Error Count — errors while applying metadata records (should stay at 0)
- Global Partition Count / Global Topic count — partitions and topics tracked cluster-wide
- Offline Partition Count — partitions with no leader (non-zero means data unavailability)
- Preferred Replica Imbalance Count — partitions not currently led by their preferred replica

KRaft Quorum Monitoring Metrics — health of the Raft replication protocol itself:
- Current Leader ID / Current quorum epoch — which node is the metadata leader, and the current election epoch
- High Watermark / Raft Log End Offset — how far the metadata log has been committed and written
- Number of Unknown Voter Connections — connections to voters outside the current quorum (should stay at 0)
- Average Commit Latency — time to commit a metadata record to the quorum
- Append Records Rate — rate of metadata records appended to the log, per node
- Current Voted — which candidate each node voted for in the current epoch
- Average Poll Idle Ratio — fraction of time each node’s Raft I/O thread spends idle (a low value means the thread is saturated)

Cleaning up
# Remove the Kafka instance
kubectl delete kafka -n demo kafka
# Remove the issuer and CA secret
kubectl delete issuer -n demo kafka-ca-issuer
kubectl delete secret -n demo kafka-ca
# Remove namespaces
kubectl delete ns demo
# Uninstall the Grafana dashboards chart, if you used Option A
helm uninstall kubedb-grafana-dashboards-kafka -n kubeops
# Uninstall grafana-operator (optional — skip if other DB guides on this cluster still use it)
helm uninstall grafana-operator -n kubeops
# Uninstall monitoring stack (optional)
helm uninstall prometheus -n monitoring
helm uninstall panopticon -n kubeops
kubectl delete ns monitoring kubeops
Next Steps
- Monitor your Kafka instance with KubeDB using built-in Prometheus.
- Monitor your Kafka instance with KubeDB using Prometheus Operator.
- Want to hack on KubeDB? Check our contribution guidelines.
































