This is an automated email from the ASF dual-hosted git repository.
hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 3cd0860a9 fix(connectors): stop rendering counter metrics with a
doubled _total (#3921)
3cd0860a9 is described below
commit 3cd0860a97492cc566d3c2e3b1752c18168b7580
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Wed Aug 19 13:09:30 2026 +0200
fix(connectors): stop rendering counter metrics with a doubled _total
(#3921)
The runtime registered its six counter families under names that
already ended in _total, and the OpenMetrics encoder appends that
suffix itself, so every scrape exposed names like
iggy_connector_errors_total_total. The runtime README documented the
un-doubled names all along, so no scraper ever saw what the docs
promised.
Drop the literal suffix at registration and let the encoder add it.
Gauges stay as they are: the encoder appends nothing to them, so
their names already rendered correctly, including the ones that end
in _total.
The existing tests could not catch this. They asserted with
contains() on a name that is a substring of the doubled one, which
passes on broken and on fixed code alike. The new test compares the
full set of rendered series names, so a silent rename fails too, and
the live-runtime scrape test now rejects a doubled suffix on the
wire.
This changes operator-facing series names, so anything scraping the
doubled names has to be updated.
---
.claude/skills/connector-runtime/SKILL.md | 4 +-
core/connectors/runtime/src/metrics.rs | 91 ++++++++++++++++++++--
.../tests/connectors/runtime/benchmark.rs | 7 ++
3 files changed, 94 insertions(+), 8 deletions(-)
diff --git a/.claude/skills/connector-runtime/SKILL.md
b/.claude/skills/connector-runtime/SKILL.md
index ce194c08e..cee028180 100644
--- a/.claude/skills/connector-runtime/SKILL.md
+++ b/.claude/skills/connector-runtime/SKILL.md
@@ -211,7 +211,7 @@ Fatal errors propagate to `main` and exit. Per-connector /
per-message errors ar
All families labeled by `connector_key` + `connector_type` (histogram adds
`stage`):
-- **Counters**:
`iggy_connector_messages_{produced,sent,consumed,processed,filtered,errors}_total`.
+- **Counters**:
`iggy_connector_messages_{produced,sent,consumed,processed,filtered}_total` and
`iggy_connector_errors_total`. These are the *rendered* names; each is
registered without the `_total`, which the OpenMetrics encoder appends.
- `messages_filtered_total` - intentional drops via transform `Ok(None)`.
- `errors_total` - unexpected drops (decode/encode/build failure, missing
field, ...) + batch-level failures.
- **Histograms**: `iggy_connector_stage_duration_seconds{stage}` (snake_case
stage labels - `prepare`, `ffi`, `decode`, `iggy_send`, `state_save`, `total`).
Buckets `STAGE_BUCKETS_SECONDS`. Always populated regardless of any flag.
Scraped at `/metrics` when `[http.metrics] enabled = true`.
@@ -221,7 +221,7 @@ All families labeled by `connector_key` + `connector_type`
(histogram adds `stag
When adding a metric:
-- Add family to `Metrics` struct + `init`, register with name + help text.
+- Add family to `Metrics` struct + `init`, register with name + help text.
Never end a `Counter` family's registered name in `_total`: the encoder appends
it and the series renders `_total_total`. Gauges get no suffix, so a gauge name
may end in `_total` literally.
- New label sets define `EncodeLabelSet` struct + label enum (hand-impl
`EncodeLabelValue` for snake_case values - the derive emits PascalCase).
- Histograms: pass `fn() -> Histogram` to `Family::new_with_constructor`.
- Add unit tests under `mod tests` with `given_*_when_*_should_*` BDD names.
diff --git a/core/connectors/runtime/src/metrics.rs
b/core/connectors/runtime/src/metrics.rs
index ca6d79515..71b5f9309 100644
--- a/core/connectors/runtime/src/metrics.rs
+++ b/core/connectors/runtime/src/metrics.rs
@@ -208,33 +208,35 @@ impl Metrics {
"Sinks in Running status",
sinks_running.clone(),
);
+ // Counter families are registered without the `_total` suffix: the
+ // OpenMetrics encoder appends it, so a literal one renders doubled.
registry.register(
- "iggy_connector_messages_produced_total",
+ "iggy_connector_messages_produced",
"Messages received from source plugin poll",
messages_produced.clone(),
);
registry.register(
- "iggy_connector_messages_sent_total",
+ "iggy_connector_messages_sent",
"Messages sent to Iggy (source)",
messages_sent.clone(),
);
registry.register(
- "iggy_connector_messages_consumed_total",
+ "iggy_connector_messages_consumed",
"Messages consumed from Iggy (sink)",
messages_consumed.clone(),
);
registry.register(
- "iggy_connector_messages_processed_total",
+ "iggy_connector_messages_processed",
"Messages processed and sent to sink plugin",
messages_processed.clone(),
);
registry.register(
- "iggy_connector_messages_filtered_total",
+ "iggy_connector_messages_filtered",
"Messages intentionally dropped by transforms returning Ok(None)",
messages_filtered.clone(),
);
registry.register(
- "iggy_connector_errors_total",
+ "iggy_connector_errors",
"Errors encountered",
errors.clone(),
);
@@ -520,6 +522,83 @@ fn stage_histogram() -> Histogram {
mod tests {
use super::*;
+ /// Every series the registry renders once each family holds a sample.
+ /// Counters carry exactly one `_total`, gauges and histogram buckets carry
+ /// the suffixes the OpenMetrics encoder gives them.
+ const RENDERED_SERIES_NAMES: [&str; 13] = [
+ "iggy_connectors_sources_total",
+ "iggy_connectors_sources_running",
+ "iggy_connectors_sinks_total",
+ "iggy_connectors_sinks_running",
+ "iggy_connector_messages_produced_total",
+ "iggy_connector_messages_sent_total",
+ "iggy_connector_messages_consumed_total",
+ "iggy_connector_messages_processed_total",
+ "iggy_connector_messages_filtered_total",
+ "iggy_connector_errors_total",
+ "iggy_connector_stage_duration_seconds_sum",
+ "iggy_connector_stage_duration_seconds_count",
+ "iggy_connector_stage_duration_seconds_bucket",
+ ];
+
+ /// Exercise every family so each one renders at least one sample: a
+ /// `Family` with no label set encodes its `# TYPE` header and nothing
else,
+ /// which would hide the rendered series names from any assertion.
+ fn populated_metrics() -> Metrics {
+ let metrics = Metrics::init();
+ metrics.set_sources_total(1);
+ metrics.set_sinks_total(1);
+ metrics.increment_sources_running();
+ metrics.increment_sinks_running();
+ metrics.increment_messages_produced("k", 1);
+ metrics.increment_messages_sent("k", 1);
+ metrics.increment_messages_consumed("k", 1);
+ metrics.increment_messages_processed("k", 1);
+ metrics.increment_messages_filtered("k", ConnectorType::Sink, 1);
+ metrics.increment_errors("k", ConnectorType::Sink);
+ metrics.observe_stage_duration(
+ "k",
+ ConnectorType::Sink,
+ Stage::Ffi,
+ Duration::from_micros(120),
+ );
+ metrics
+ }
+
+ /// Sample lines are `name{labels} value` or `name value`; comments and the
+ /// `# EOF` trailer are skipped.
+ fn rendered_series_names(output: &str) -> Vec<String> {
+ let mut names: Vec<String> = output
+ .lines()
+ .filter(|line| !line.is_empty() && !line.starts_with('#'))
+ .map(|line| line.split(['{', '
']).next().unwrap_or_default().to_owned())
+ .collect();
+ names.sort_unstable();
+ names.dedup();
+ names
+ }
+
+ #[test]
+ fn given_every_family_when_encoded_should_render_one_total_suffix() {
+ let output = populated_metrics().get_formatted_output();
+
+ assert!(
+ !output.contains("_total_total"),
+ "counter registered with a literal `_total` renders
doubled:\n{output}"
+ );
+
+ let mut expected: Vec<String> = RENDERED_SERIES_NAMES
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect();
+ expected.sort_unstable();
+ assert_eq!(
+ rendered_series_names(&output),
+ expected,
+ "rendered series names drifted; dashboards key off
these:\n{output}"
+ );
+ }
+
#[test]
fn test_metrics_init() {
let metrics = Metrics::init();
diff --git a/core/integration/tests/connectors/runtime/benchmark.rs
b/core/integration/tests/connectors/runtime/benchmark.rs
index 77225a365..063d83f40 100644
--- a/core/integration/tests/connectors/runtime/benchmark.rs
+++ b/core/integration/tests/connectors/runtime/benchmark.rs
@@ -435,6 +435,13 @@ async fn
given_runtime_processing_batches_when_metrics_scraped_should_expose_sta
"expected stage histogram in /metrics, got:\n{metrics_body}"
);
+ // Both connectors have processed batches by now, so every counter family
+ // renders a sample and a doubled suffix would be visible on the wire.
+ assert!(
+ !metrics_body.contains("_total_total"),
+ "counter names must carry exactly one `_total`, got:\n{metrics_body}"
+ );
+
let sink_total_count = parse_metric_value(
&metrics_body,
"iggy_connector_stage_duration_seconds_count",