Skip to main content
Version: 1.0

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