Reusing your Kafka Streams dashboards

Turn on Kafka Streams-compatible metrics so your existing Grafana dashboards, Prometheus rules, and alerts light up against a StoatFlow app with at most threshold-level edits.

You arrive from Kafka Streams with an observability estate — Grafana dashboards, Prometheus recording rules, alerts — all built against Kafka Streams metric names. By default none of it lights up against StoatFlow: every StoatFlow meter lives under the stoatflow.* namespace with StoatFlow-native structure. This page turns on the opt-in Kafka Streams-compatible metrics mode, which emits KS-named, KS-shaped, KS-unit series derived from StoatFlow's own meters — so your existing dashboards work with little or no editing.

Turn it on

The mode is a config knob under runtime.metrics. Zero code changes.

runtime:
  metrics:
    enabled: true
    naming: both               # stoatflow | kafka-streams | both   (default: stoatflow)
    ks-compat:
      shape: micrometer-binder # micrometer-binder | jmx-exporter   (default: micrometer-binder)
      sample-window-ms: 30000  # KS metrics.sample.window.ms parity
      kafka-version: "4.1.0"   # value of the synthetic kafka_version tag (micrometer-binder shape)

The three naming modes:

naming/metrics containsUse it when
stoatflowNative stoatflow.* only (default — today's behaviour)You have no KS dashboards to reuse.
bothNative + KS-namedMigration default. Your KS dashboards work and your StoatFlow dashboards keep working.
kafka-streamsKS-named only; the mapped stoatflow.* families are hiddenA deliberate end state, or when scrape/ingest size matters. Note: StoatFlow's own reference dashboards and support runbooks key on stoatflow_* and won't work here.

Start with both. Move to kafka-streams only once you're sure nothing you own reads the native names.

Which shape does your Prometheus have?

"The KS metric name" in Prometheus depends on how the KS app was scraped. StoatFlow renders either shape:

ShapeExample seriesThis is what you have if…
micrometer-binderkafka_stream_thread_process_latency_avgYour KS app used Micrometer / Spring Boot's KafkaStreamsMetrics binder.
jmx-exporterkafka_streams_stream_thread_metrics_process_rateYou scraped KS JMX via the Prometheus jmx_exporter (Strimzi / Confluent-style).

Look at one panel query in your existing dashboard and match the prefix. (The jmx-exporter shape targets the common default-config output; heavily customised jmx rules files may need panel tweaks.)

The two edits that cover most dashboards

StoatFlow has no stream threads and no tasks — it runs one instance with virtual-thread lanes. The compat layer synthesises the KS identity tags, with two consequences worth knowing before you read a dashboard:

task-id is a lane, not a partition. Sums and aggregations (sum(...), rate(...)) match exactly. But a per-task drill-down enumerates lanes (0_5 is lane 5, not partition 5), and the lane count is not the partition count. Panels that break down by task show lanes.
One synthetic thread-id. StoatFlow collapses the whole engine into a single <application-id>-StreamThread-1. Per-thread breakdowns become one series, and any alert like kafka_stream_alive_stream_threads < N must have its threshold set to 1.

Alert-edit checklist

  • alive-stream-threads alerts → threshold 1 (single synthetic thread).
  • failed-stream-threads → maps from StoatFlow's engine-restart counter (a replace_thread restart is the KS thread-replacement recovery event); keep the alert, it fires on real fault storms.
  • Per-task-id / per-thread-id breakdown panels → read them as per-lane / single-series.
  • Thread process-latency → StoatFlow measures per record, KS per iteration-batch, so absolute values are smaller than a real KS app; keep the shape, re-baseline any hard thresholds.
  • Ratios (process-ratio, active-process-ratio, punctuate-ratio) are not emitted (lane parallelism makes true utilization >1); remove or ignore those panels.

What maps, and how well

The full per-metric compatibility table is the canonical mapping in the engine; the tiers are:

  • ✅ full — same meaning, same math (poll, store range/all/flush latency, topic consumed/produced totals, node e2e latency, …).
  • ⚠️ approximate — emitted, semantics differ, documented per row (thread commit-latency = barrier commit; dropped-records covers null-key + join + late-window drops; per-lane task metrics; store get/put/delete latency; RocksDB rows).
  • ❌ not mappable — architecturally absent (no rebalancing, no enforced-processing, no iterator metrics); their absence is parity — a Micrometer-instrumented KS app skips the same non-numeric metrics.

State-store panels key on the scope-dependent tag (rocksdb-window-state-id, in-memory-session-state-id, …); StoatFlow synthesises the correct scope key per store, so those panels populate.

One thing to know: the native surface shifts slightly

With naming != stoatflow a compat filter tightens the mapped native timers' stoatflow_*_seconds_max to a ~30 s window (fresher than the default 2 min × 3) and adds a p0 quantile to stoatflow.e2e.latency (used to approximate record-e2e-latency-min). This is bounded and arguably an improvement, but it is an observable change to a few native _max series — expect them to be a little "twitchier".

Sizing

both mode roughly adds: thread ≈ 25 series; task ≈ 8-10 × lanes (64 lanes ≈ 600 series); store ≈ 10-14 × stores (plus ~44 × stores for RocksDB when statistics-enabled); topic ≈ 4 × topics; cache ≈ 3 × stores. Lane count dominates. At recording-level: debug with many lanes this is thousands of extra series — which is remote-write / ingest cost, not just scrape payload. The recording level gates the sources (see Metrics).

Next steps

  • Metrics — the native stoatflow.* catalogue and recording levels.
  • Observability — alerting and the Grafana view.