github robintra/perf-sentinel v0.9.12

latest release: chart-v0.9.12
9 hours ago

What's new in v0.9.12

v0.9.12 adds Alumet as a sixth measured-energy source, at the top of the measured precedence chain. The integration is daemon-only and opt-in via [green.alumet], so nothing changes at upgrade time unless an operator enables it. The Kepler scraper hardens along the way: counters sharing a label value are now summed before the delta, and both scrapers gain a diagnostic for mappings that match nothing on the wire. One HTTP surface changes shape, GET /api/energy grows from five backend rows to six and reorders them. The workspace toolchain moves to Rust 1.97.1.

Alumet energy source

Alumet is a measurement framework from the French research ecosystem (EUPL-1.2), built as composable plugins: a source measures (RAPL among others), transforms attribute the readings to workloads, and outputs expose them. perf-sentinel scrapes the prometheus-exporter output and publishes a measured per-service energy coefficient tagged alumet_rapl, which outranks every other measured source. Both Alumet and Scaphandre read RAPL, but Alumet's sampling is measurably less error-prone (Raffin and Trystram, "Dissecting the Software-Based Measurement of CPU Energy Consumption", IEEE TPDS 2024) and it attributes per cgroup rather than per process.

Alumet exposes a third reading shape, distinct from both existing Prometheus sources. Scaphandre exports an instantaneous microwatt gauge and Kepler a cumulative joule counter, while Alumet publishes the joules of one source poll_interval, re-emitted unchanged as a gauge until the next flush. Summing scrapes would double-count or drop intervals depending on the scrape cadence, so the scraper divides each reading by the new mandatory energy_interval_secs field to recover watts, then integrates over its own scrape window. That field must mirror the Alumet-side poll_interval and nothing on the wire can verify it, a mismatch rescales energy and carbon silently under a measured provenance tag. The failure mode and its bounds are documented in docs/LIMITATIONS.md#alumet-precision-bounds, and the daemon echoes the value it uses at scraper startup.

metric_name and label_key are operator-supplied with no default, because none could be correct: Alumet's exporter shapes every metric name with a configurable prefix and suffix, and the per-service series is produced by an attribution formula the operator names. docs/CONFIGURATION.md shows the matching Alumet-side configuration (rapl plus k8s plus energy-attribution, the composition per-service attribution requires) next to the perf-sentinel side, along with the packaging pitfalls of the upstream .deb. Three Prometheus metrics mirror the Kepler set: perf_sentinel_alumet_scrape_total, perf_sentinel_alumet_scrape_failed_total and perf_sentinel_alumet_last_scrape_age_seconds.

Summed reads for shared label values

Both Prometheus scrapers now sum every row sharing a label value, with per-row validation: a NaN row no longer poisons the rows sharing its label, and a negative row no longer subtracts from an otherwise valid sum. For Alumet the collisions are routine, one pod carries a row per RAPL domain and label_key = "domain" on a dual-socket host carries one row per socket, and energy is additive, so the sum is the physically correct read. For Kepler this replaces a last-write-wins read: one container name repeated across pods yielded several cumulative series under one mapping value, and the previous read flipped between counters of different magnitudes with exposition order, producing meaningless deltas in both directions. The sum of cumulative counters stays monotonic while the series set is stable, and a vanishing series re-baselines through the existing negative-delta filter. A zero or invalid reading never publishes a measured-zero coefficient over a live one.

Scraper diagnostics

The Kepler and Alumet scrapers previously had one warning, fired when the exposition contained no matching samples at all. A mistyped service_mappings value, the likelier mistake, produced healthy counters and no diagnostic: samples kept flowing, the freshness gauge kept resetting, and no coefficient was ever published. Each scraper now runs two independent warn-once streaks, one for an empty exposition (the metric name is wrong on the wire) and one for samples flowing while no mapping value is present (mistyped values, or every mapped workload absent from the exposition), each message naming its own likely causes. An empty service_mappings table gets a single startup warning instead.

Operator-visible behavior change

GET /api/energy returns six backend rows instead of five, reordered to follow the measured precedence chain with alumet first. Consumers reading rows by their backend name are unaffected, consumers indexing by position must switch to the name. co2.model and per_service_energy_model can now carry alumet_rapl, both fields are documented as open string sets so existing consumers parse unchanged. On Kepler deployments where several containers shared one mapping value, the published deltas change value, the previous figures were order-dependent. For direct consumers of the perf-sentinel-core library, score::kepler::parser moved to score::prom_parser, parse_kepler_metrics is now parse_metric_samples and KeplerSample is now PromSample, the CLI is unaffected. The minimum supported Rust version moves to 1.97.1, and the dependency floors move with the lockfile (lru 0.18.1, uuid 1.24.0, http-body-util 0.1.4, bytes 1.12.1, tokio 1.52.4, clap 4.6.2), cargo audit and cargo deny both clean.

Why this is a patch and not a minor

Every externally-visible addition is opt-in. [green.alumet] is absent from the default configuration, so no existing deployment changes behavior at upgrade time. The CLI subcommand surface keeps its v0.9.11 shape, the report JSON schema gains no new field, and the co2.model enum gains one value in a set already documented as open. The one reshaped surface is /api/energy, whose row order was documented as fixed and changes deliberately with the new backend leading. The Kepler numerical change only affects deployments whose mapping values collided across series, where the previous figures were not defensible.

Validation

The integration was validated in the simulation lab against a real Alumet v0.9.5 agent installed from the upstream .deb, 17 of 17 assertions: the wire shape and label set, the daemon end to end over four scrape ticks, the summed-label math exact to machine epsilon against 18 live rows, a deliberate interval mismatch scaling the figures by exactly the expected factor, precedence over a Scaphandre reading on the same service, both warning latches, and the /api/energy row set. A new wire-conformance CI job installs the upstream .deb on every pull request touching the scrapers and asserts the parser contract against the live exposition, discovering the metric name on the wire since upstream naming still shifts before its 1.0. The Rust suite grows to 2659 tests, clippy is clean at -D warnings on Rust 1.97.1, and the default and --no-default-features builds both pass.

Verifying this release

# Binary integrity via SLSA Build L3 attestation
gh attestation verify perf-sentinel-linux-amd64 \
  --repo robintra/perf-sentinel

# A periodic disclosure produced by this binary
perf-sentinel verify-hash --report perf-sentinel-report.json \
  --expected-identity "https://github.com/robintra/perf-sentinel/.github/workflows/release.yml@refs/tags/v0.9.12" \
  --expected-issuer "https://token.actions.githubusercontent.com"

gh CLI 2.49 or newer required for gh attestation verify.

Full Changelog: v0.9.11...v0.9.12

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.