Metrics

Scrape StoatFlow's Prometheus /metrics endpoint — Micrometer integration, the key meters the engine emits, license metrics, and common tags.

The :runtime module exports metrics in Prometheus text format on GET /metrics. Internally it's a Micrometer PrometheusMeterRegistry — the engine reports events to a Micrometer instrumentation callback, the runtime binds JVM and Kafka-client metrics to the same registry, and the handler serves the registry's scrape output. Point your existing Prometheus stack at the endpoint and the StoatFlow meters appear alongside everything else.

Enable and scrape

Metrics are enabled by default and live under runtime.metrics:

runtime:
  http:
    enabled: true
    port: 8080            # default
  metrics:
    enabled: true         # default
    prefix: stoatflow     # default — prefix on all metric names
    recording-level: info # info | debug | trace (default: info)
    bind-jvm-metrics: true # default — JVM memory/GC/thread binders
    common-tags:          # extra tags applied to every meter
      environment: production
      service: word-count

Scrape it from the shell:

curl -s localhost:8080/metrics | head -40

A minimal Prometheus scrape config:

scrape_configs:
  - job_name: stoatflow
    metrics_path: /metrics
    static_configs:
      - targets: ["my-app:8080"]

The endpoint returns the standard exposition format, so any Prometheus-compatible collector (Prometheus, Grafana Agent, OpenTelemetry Collector with the Prometheus receiver, the VictoriaMetrics agent) scrapes it without extra translation. On Kubernetes, add prometheus.io/scrape / prometheus.io/path pod annotations or a ServiceMonitor pointing at the HTTP port — see Observability.

Disabling metrics (runtime.metrics.enabled: false) un-registers the /metrics endpoint — it returns 404 — and switches the engine to no-op instrumentation, so there's no measurement overhead when metrics are off.

Recording levels

recording-level controls how much detail the engine reports, mirroring Kafka Streams' levels. A higher level is a superset of the ones below it.

LevelWhat it addsUse
infoEssential operational meters — throughput, barrier/commit latency, watermark progress, restoration, consumer/producer, errors.Production default.
debugPer-store, per-partition, per-processor detail and dispatch internals. Higher cardinality.Targeted troubleshooting.
traceDeepest internals; highest overhead.Development only.

Tags like lane_id, store_name, topic, and partition multiply series count. On large topologies, keep production at info and switch to debug only while investigating.

Naming and tags

All StoatFlow meters use a dotted name with the configured prefix (stoatflow by default). The Prometheus registry rewrites dots to underscores and appends the conventional suffix (_total for counters; _seconds_count/_seconds_sum/_seconds_max for timers). So stoatflow.barrier.completed.total is scraped as stoatflow_barrier_completed_total, and the timer stoatflow.barrier.commit.latency produces stoatflow_barrier_commit_latency_seconds_* series. The latency timers publish client-side percentiles as {quantile="0.5|0.95|0.99"} labels on the base _seconds name rather than _bucket histograms, so use rate(_sum)/rate(_count) (or the {quantile=...} series) — histogram_quantile() over _bucket won't match.

Two application tags are present on StoatFlow meters:

TagSourceNotes
applicationRegistry-level common tagCarries application-id; applied to every meter, including JVM and Kafka-client metrics.
application_idPer-meter tagApplied by the StoatFlow instrumentation callback.

Anything you add under common-tags (for example environment, region, service) is also applied to every meter — handy for selecting one deployment in a shared Prometheus. Per-meter tags vary by group: lane_id, store_name, topic, partition, error_type, trigger, type, result, and others are listed with each meter below.

Lane IDs follow the Kafka Streams task-ID convention {subtopology}_{lane} — e.g. 0_0, 0_15, 1_7. Aggregate across a sub-topology in PromQL with a regex matcher: sum(stoatflow_lane_records_processed_total{lane_id=~"1_.*"}).

Key meters

The dotted names below are the canonical metric IDs the engine emits. Remember the Prometheus rewrite (._, plus the type suffix) when writing queries. This is a working set, not the exhaustive list — the full catalogue is on the metrics reference, and your live scrape stays authoritative for exact tag sets.

Throughput and end-to-end latency

MetricTypeLevelTagsMeaning
stoatflow.lane.records.processed.totalCounterinfolane_idRecords processed by a lane.
stoatflow.lane.bytes.processed.totalCounterinfolane_idBytes processed by a lane.
stoatflow.lane.process.latencyTimerinfolane_idPer-record processing latency.
stoatflow.lane.queue.sizeGaugeinfolane_idCurrent lane queue depth (backpressure signal).
stoatflow.lane.queue.capacityGaugeinfolane_idConfigured lane queue capacity.
stoatflow.lane.queue.full.totalCounterinfolane_idTimes a lane queue was full (backpressure events).
stoatflow.e2e.latencyTimerinfoPer-record end-to-end latency, source timestamp to processing completion (analogous to KS record-e2e-latency).
# Total processing throughput (records/sec)
sum(rate(stoatflow_lane_records_processed_total[1m]))

# Average per-record processing latency (this timer emits _sum/_count/_max, not _bucket)
sum(rate(stoatflow_lane_process_latency_seconds_sum[5m])) / sum(rate(stoatflow_lane_process_latency_seconds_count[5m]))

Barrier commits

Commit barriers are StoatFlow's exactly-once mechanism (see Exactly-once). These meters track barrier lifecycle and commit-phase latency. The latency timers publish client-side P50/P95/P99 percentiles.

MetricTypeLevelTagsMeaning
stoatflow.barrier.initiated.totalCounterinfoBarriers initiated.
stoatflow.barrier.completed.totalCounterinfoBarriers successfully completed.
stoatflow.barrier.failed.totalCounterinfoerror_typeBarrier failures, by error type.
stoatflow.barrier.in.flightGaugeinfoCurrently pending barriers.
stoatflow.barrier.latencyTimerinfoInitiation to completion.
stoatflow.barrier.alignment.latencyTimerinfoTime for all lanes to receive the barrier.
stoatflow.barrier.commit.latencyTimerinfoCommit latency (producer drain + Kafka TX + state flush). Comparable to KS commit-latency.
stoatflow.barrier.commit.changelog.latencyTimerinfoAggregate changelog serialization+send latency per commit epoch.
stoatflow.barrier.commit.state.flush.latencyTimerinfoAggregate state-store flush latency per commit epoch.
stoatflow.barrier.commit.producer.flush.latencyTimerinfoPre-commit producer flush latency.
stoatflow.barrier.commit.send.offsets.latencyTimerinfosendOffsetsToTransaction RPC latency.
stoatflow.barrier.records.committed.totalCounterinfoRecords committed across barriers.
stoatflow.barrier.bytes.committed.totalCounterinfoBytes committed across barriers.
stoatflow.barrier.trigger.totalCounterinfotriggerBarrier creation count by trigger — TIME, RECORD_COUNT, MEMORY_PRESSURE, CACHE_PRESSURE (state-cache cap forced an early commit).
stoatflow.transaction.commit.totalCounterinfoKafka transactions committed.
stoatflow.transaction.abort.totalCounterinfoKafka transactions aborted.
stoatflow.transaction.commit.latencyTimerinfoTransaction commit latency.
# Commit rate vs. failure rate
rate(stoatflow_barrier_completed_total[1m])
rate(stoatflow_barrier_failed_total[5m])
The commit cadence and epoch size self-tune within their configured bounds; the scheduling gauges (stoatflow.barrier.interval.target.ms, stoatflow.barrier.epoch.max.records, and related) report what the engine is currently doing so you can see the cadence on a dashboard. The bound knobs themselves are documented under Tuning and the configuration reference.

Watermarks

Event-time progress for windowing and late-event detection (see Event time and watermarks).

MetricTypeLevelTagsMeaning
stoatflow.watermark.currentGaugeinfoCurrent global watermark (epoch ms).
stoatflow.watermark.lag.msGaugeinfoWall-clock time minus watermark (event-time lag).
stoatflow.watermark.advance.totalCounterinfoWatermark advancement events.
stoatflow.watermark.late.records.totalCounterdebugtopic, partitionRecords arriving after the watermark.
# Alert when the watermark stalls while the app is running (RUNNING == 4)
delta(stoatflow_watermark_current[5m]) == 0 and stoatflow_client_state == 4

Changelog writes and state restoration

Changelog write counters track state durability traffic; restoration meters track recovery progress on startup (see State stores and State and thread safety).

MetricTypeLevelTagsMeaning
stoatflow.store.changelog.records.written.totalCounterinfostore_nameChangelog records written.
stoatflow.store.changelog.bytes.written.totalCounterinfostore_nameChangelog bytes written.
stoatflow.restoration.in.progressGaugeinfo1 while restoration is running, 0 otherwise.
stoatflow.restoration.stores.totalGaugeinfoTotal stores to restore.
stoatflow.restoration.stores.completedGaugeinfoStores that have finished restoring.
stoatflow.restoration.records.restored.totalCounterinfostore_nameRecords restored, per store.
stoatflow.restoration.bytes.restored.totalCounterinfostore_nameBytes restored, per store.
stoatflow.restoration.duration.secondsTimerinfostore_namePer-store restoration duration.
# Restoration progress as a fraction of stores completed
stoatflow_restoration_stores_completed / stoatflow_restoration_stores_total

Consumer, producer, and dispatch

MetricTypeLevelTagsMeaning
stoatflow.consumer.records.consumed.totalCounterinfotopic, partitionRecords consumed.
stoatflow.consumer.bytes.consumed.totalCounterinfotopic, partitionBytes consumed.
stoatflow.consumer.lag.recordsGaugeinfotopic, partitionConsumer lag, in records.
stoatflow.consumer.poll.latencyTimerinfoConsumer poll latency.
stoatflow.consumer.assigned.partitionsGaugeinfoNumber of assigned partitions.
stoatflow.producer.records.produced.totalCounterinfotopicRecords produced.
stoatflow.producer.bytes.produced.totalCounterinfotopicBytes produced.
stoatflow.dispatcher.dispatch.latencyTimerinfoPer-batch dispatch latency.
# Worst-case consumer lag across partitions
max(stoatflow_consumer_lag_records)

Errors

Error classification for alerting (see Error handling).

MetricTypeLevelTagsMeaning
stoatflow.error.totalCounterinfoerror_type, topicErrors by type (and topic where applicable).
stoatflow.error.typed.totalCounterinfocategory, error_typeCategorised errors (e.g. deserialization, processing, production).
stoatflow.queue.offer.failure.totalCounterinfoqueue_typeInternal queue offer rejections.

Client state

MetricTypeLevelTagsMeaning
stoatflow.client.stateGaugeinfoApplication state ordinal: CREATED=0, STARTING=1, VALIDATING_STATE=2, RESTORING=3, RUNNING=4, DRAINING=5, PAUSED=6, STOPPING=7, STOPPED=8, ERROR=9.
stoatflow.lanesGaugeinfoConfigured lane count.
stoatflow.client.lane.aliveGaugeinfoCurrently running lanes.
stoatflow.client.uptime.secondsGaugeinfoSeconds since the client was created.
stoatflow.engine.restart.totalCounterinfodisposition, target, triggerSuccessful in-place engine restarts (fault recovery via REPLACE_THREAD, HA demote-in-place).

High availability

Bound only when hot standby is enabled (ha.mode != off).

MetricTypeLevelTagsMeaning
stoatflow.ha.roleGaugeinfo1 when this pod is the active, 0 when standby.
stoatflow.ha.standby.replication.lag.recordsGaugeinfoStandby replication lag in records (0 when active or caught up).
stoatflow.ha.standby.replication.lag.msGaugeinfoStandby replication lag in milliseconds.
stoatflow.ha.peer.freshness.msGaugeinfoTime since the freshest observed peer's last heartbeat.
stoatflow.ha.peersGaugeinfoNumber of observed HA peers.
stoatflow.ha.redundancy.ready.standbysGaugeinfoCaught-up (READY_STANDBY) standby count, including self.
stoatflow.ha.redundancy.below.desiredGaugeinfo1 when the active has fewer ready standby spares than desired-standbys.
stoatflow.ha.token.epochGaugeinfoCurrent promotion-token epoch (increments once per election).
stoatflow.ha.election.outcomeCounterinfooutcomeCumulative election outcomes (won / reclaimed / stood_down_fresh / stood_down_lost).
stoatflow.ha.election.latency.msGaugeinfoLast claim→ACTIVE election latency in ms (-1 until the first promotion).
stoatflow.ha.restart.requiredGaugeinfo1 when this pod requires a restart (e.g. source-topic partition count changed).

License metrics

When a license is configured, the runtime binds license meters to the same registry (see License configuration). These use the <prefix>.license prefix (default stoatflow.license, tracking runtime.metrics.prefix) and carry the registry common tags (application, plus your common-tags). They're exported even when recording-level is info.

MetricTypeTagsMeaning
stoatflow.license.validGaugetier1 when operating under a VALID or GRACE_PERIOD license, else 0.
stoatflow.license.expiry_timestamp_secondsGaugeUnix epoch seconds of the entitlement deadline.
stoatflow.license.days_remainingGaugeWhole days until expiry; negative within the post-expiry grace window.
stoatflow.license.grace_remaining_secondsGaugeSeconds left in the offline-grace budget; 0 when exhausted.
stoatflow.license.heartbeat_consecutive_failuresGaugeHeartbeat ticks failed since the last success; resets to 0 on success.
stoatflow.license.heartbeat_last_success_timestamp_secondsGaugeUnix epoch seconds of the most recent successful heartbeat.
stoatflow.license.validations_totalCounterresultCold-start validation outcomes, by result label.
stoatflow.license.heartbeats_totalCounterresultHeartbeat tick outcomes, by result label.
# Alert when the license is not valid, or when expiry is within 14 days
stoatflow_license_valid == 0
stoatflow_license_days_remaining < 14
License meters carry tier and timing detail. Scrape /metrics only from your in-cluster monitoring stack, not through internet-facing ingress. See Kubernetes.

JVM and Kafka-client metrics

With bind-jvm-metrics: true (the default), the runtime binds Micrometer's standard JVM binders to the registry, so the usual jvm_* and system_* series appear on the same endpoint:

  • jvm_memory_used_bytes / jvm_memory_max_bytes (heap and non-heap)
  • jvm_gc_pause_seconds and related GC metrics
  • jvm_threads_live_threads, jvm_threads_states_threads
  • jvm_classes_loaded_classes
  • system_cpu_usage, process_cpu_usage, system_load_average_1m

These carry the registry common tags (application + your common-tags) but not application_id. Native Kafka client metrics are also surfaced through Micrometer, so producer/consumer client internals appear alongside the StoatFlow meters.

Dashboards and alerts

A starting overview combines throughput, latency, backpressure, and commit health:

# Throughput
sum(rate(stoatflow_lane_records_processed_total[1m]))

# Backpressure — total queued vs. capacity
sum(stoatflow_lane_queue_size) / sum(stoatflow_lane_queue_capacity)

# Commit health
rate(stoatflow_barrier_completed_total[1m])
rate(stoatflow_barrier_failed_total[5m])

# Event-time lag
stoatflow_watermark_lag_ms

Useful alert conditions:

AlertConditionSeverity
Barrier failuresrate(stoatflow_barrier_failed_total[5m]) > 0Critical
License invalidstoatflow_license_valid == 0Critical
High consumer lagmax(stoatflow_consumer_lag_records) > 100000Warning
Watermark stalldelta(stoatflow_watermark_current[5m]) == 0 and stoatflow_client_state == 4Warning
Backpressuresum(stoatflow_lane_queue_size) > 0.8 * sum(stoatflow_lane_queue_capacity)Warning

Next steps

  • Observability — wiring /metrics, logs, and probes into a monitoring stack.
  • Health checks — the readiness and liveness signals that complement metrics.
  • Tuning — which bound knobs the self-tuning cadence respects.
  • The REST API — the other operational endpoints on the same HTTP server.