Skip to main content
Version: Next

Metrics

The Fluss Rust client is instrumented with client-side metrics for the connection layer, the write pipeline, and the read (scanner) pipeline. Metrics are emitted through the metrics crate facade, so collecting them is opt-in and costs nothing until you install a recorder.

How it works

The client never decides where metrics go. It only emits them via the metrics facade. Your application installs a global recorder (for example a Prometheus exporter), and that recorder decides how to store and expose the values.

  • No recorder installed — every metric call is a zero-cost no-op. This is the default, so the client adds no overhead unless you opt in.
  • Recorder installed — values flow to whatever backend the recorder represents (Prometheus, StatsD, OpenTelemetry, a test recorder, etc.).

This differs from the Fluss Java client, where metric reporters are configured server-side in conf/server.yaml (metrics.reporters: jmx,prometheus) and discovered through plugins. The Rust client instead follows the idiomatic Rust metrics ecosystem: the application owns recorder installation, and rate computation is left to the backend (e.g. PromQL rate()) instead of a built-in background rate thread.

Installing a recorder

Use any metrics-compatible exporter. The example below uses metrics-exporter-prometheus to expose a scrape endpoint:

use metrics_exporter_prometheus::PrometheusBuilder;

PrometheusBuilder::new()
.with_http_listener(([0, 0, 0, 0], 9000))
.install()
.expect("failed to install Prometheus recorder");

A full, runnable program is available as the example-prometheus-metrics example in the fluss-examples crate.

Install the recorder before writing or scanning

The client caches metric handles the first time a writer or scanner is created, binding them to whichever recorder is installed at that moment. Install your global recorder before calling FlussConnection::new (ideally as the very first thing in main). If you install it after creating a writer or scanner, those metrics will be bound to the no-op recorder and never appear.

Metric catalog

All metric names use the fluss.client. prefix. A Prometheus exporter translates . to _, so fluss.client.writer.send_latency_ms is scraped as fluss_client_writer_send_latency_ms.

Connection / RPC

Recorded per RPC for the four reportable API keys. Labeled with api_key (produce_log, fetch_log, put_kv, lookup).

MetricTypeDescription
fluss.client.requests.totalCounterRequests sent to a server.
fluss.client.responses.totalCounterResponses received from a server.
fluss.client.bytes_sent.totalCounterRequest body bytes sent (excludes protocol framing).
fluss.client.bytes_received.totalCounterResponse body bytes received (excludes protocol framing).
fluss.client.request_latency_msHistogramRound-trip latency per request, in milliseconds.
fluss.client.requests_in_flightGaugeRequests currently awaiting a response.

Writer

Recorded in the write pipeline. These metrics are unlabeled (one series per process), matching Java's WriterMetricGroup, which carries no table label.

MetricTypeDescription
fluss.client.writer.send_latency_msHistogramRound-trip latency of each write request (ProduceLog / PutKv).
fluss.client.writer.batch_queue_time_msHistogramTime a batch spent queued in the accumulator before being drained.
fluss.client.writer.records_send.totalCounterRecords handed to the cluster across all sent batches.
fluss.client.writer.bytes_send.totalCounterSerialized batch bytes sent.
fluss.client.writer.records_retry.totalCounterRecords re-enqueued for retry.
fluss.client.writer.records_per_batchHistogramRecords per sent batch.
fluss.client.writer.bytes_per_batchHistogramSerialized bytes per sent batch.
fluss.client.writer.buffer_total_bytesGaugeTotal writer buffer memory, in bytes.
fluss.client.writer.buffer_available_bytesGaugeCurrently available writer buffer memory, in bytes.
fluss.client.writer.buffer_waiting_threadsGaugeProducer threads blocked waiting for buffer memory (backpressure signal).

Scanner

Recorded in the read pipeline. Labeled with database and table, so each scanned table gets its own series.

MetricTypeDescription
fluss.client.scanner.time_between_poll_msGaugeMilliseconds between the start of consecutive poll() calls.
fluss.client.scanner.poll_idle_ratioGaugeFraction of wall-clock time spent inside poll() (near 1.0 means starved for data).
fluss.client.scanner.last_poll_seconds_agoGaugeSeconds since the most recent poll() started (stuck-consumer signal).
fluss.client.scanner.fetch_latency_msHistogramLatency of each successful FetchLog RPC, in milliseconds.
fluss.client.scanner.fetch_requests.totalCounterFetchLog RPC requests attempted.
fluss.client.scanner.bytes_per_requestHistogramSerialized bytes per successful FetchLog response.
fluss.client.scanner.remote_fetch_requests.totalCounterRemote log download attempts (includes per-segment retries).
fluss.client.scanner.remote_fetch_bytes.totalCounterBytes downloaded from remote log storage.
fluss.client.scanner.remote_fetch_errors.totalCounterRemote log download failures (each retry counts).

Differences from the Java client

The Rust client records the same events at the same points as Java, but uses metric types better suited to the metrics ecosystem:

AspectJavaRust
Latency metrics (send_latency_ms, batch_queue_time_ms, fetch_latency_ms)Volatile-long gauge (latest value only)Histogram (full p50/p95/p99 distribution)
Throughput / retry metricsMeterView rate computed on a background threadRaw counter; compute the rate with PromQL rate()
Writer table labelNo table labelNo table label (kept identical)

These are client-internal implementation choices; the values reported to the server are unaffected.

Tuning histogram buckets

The default buckets in metrics-exporter-prometheus may not give meaningful percentiles for sub-millisecond or multi-second latencies. Configure per-metric buckets when installing the recorder:

use metrics_exporter_prometheus::PrometheusBuilder;
use metrics::Unit;

PrometheusBuilder::new()
.set_buckets_for_metric(
metrics_exporter_prometheus::Matcher::Suffix("_ms".to_string()),
&[0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0],
)
.expect("invalid bucket configuration")
.with_http_listener(([0, 0, 0, 0], 9000))
.install()
.expect("failed to install Prometheus recorder");

let _ = Unit::Milliseconds; // optional: register units for richer metadata

Cardinality

Scanner metrics carry database and table labels, so the number of scanner series scales with the number of tables a single client scans. This is normally small, but if your client scans many short-lived tables, the series count can grow over time. Connection metrics are bounded (four API keys) and writer metrics are unlabeled, so neither contributes meaningfully to cardinality.

Grafana / PromQL tips

By default, metrics-exporter-prometheus emits histogram metrics as Prometheus summaries (no _bucket series). The _bucket-based histogram_quantile query only works if you enabled bucket mode via set_buckets(...) or set_buckets_for_metric(...) in the previous section.

# Write throughput (records/sec), Java-style rate from the raw counter
rate(fluss_client_writer_records_send_total[1m])

# p99 send latency (default summary mode)
fluss_client_writer_send_latency_ms{quantile="0.99"}

# p99 send latency (bucket mode enabled)
histogram_quantile(0.99, sum(rate(fluss_client_writer_send_latency_ms_bucket[5m])) by (le))

# Backpressure: producers blocked waiting for buffer memory
fluss_client_writer_buffer_waiting_threads

# Per-table fetch rate
sum(rate(fluss_client_scanner_fetch_requests_total[1m])) by (database, table)

# Stuck-consumer alert: no poll in the last 60s
fluss_client_scanner_last_poll_seconds_ago > 60