github robintra/perf-sentinel v0.5.9

latest releases: v0.8.9, chart-v0.2.53, v0.8.8...
one month ago

What's new in v0.5.9

The [green.electricity_maps] real-time intensity scraper landed in the 0.4.x series and has been steadily exercised by daemon users since. Cross-referencing the implementation with the official API documentation surfaced two gaps that v0.5.9 closes.

The first one is per-zone API call deduplication. Until v0.5.8, the scraper iterated over region_map (cloud_region -> zone) and made one HTTP call per pair, even when several cloud_region entries pointed at the same zone (typical multi-AZ setups, or aws:eu-west-3 and local-k3d both pinned to FR, or staging+prod sharing a country code). On the free tier capped at one zone the practical impact was zero, but on quota-constrained paid tiers the call count grew with region_map size instead of with the number of distinct zones. v0.5.9 builds a BTreeSet of unique zones from region_map.values(), fetches each zone exactly once per tick, and dispatches the resulting reading to every cloud_region mapped to that zone. A region_map with aws:eu-west-3 -> FR, local-k3d -> FR, aws:eu-central-1 -> DE now hits the API twice per tick instead of three times. Both FR cloud_regions resolve to the same intensity in the published state.

The second one is estimation metadata. The Electricity Maps API surfaces two optional fields alongside carbonIntensity: isEstimated: bool (true when the value was filled in from a model rather than measured directly, typical for Tier B/C zones or for temporal holes bridged by an algorithm) and estimationMethod: string (the algorithm tag, e.g. "TIME_SLICER_AVERAGE", "GENERAL_PURPOSE_ZONE_DEVELOPMENT"). Until v0.5.8 the scraper only parsed carbonIntensity and dropped the rest. v0.5.9 plumbs both fields end-to-end: scraper parsing → IntensityReading state → new ElectricityMapsState::snapshot_with_metadata → new RealTimeIntensityEntry type on CarbonContext.real_time_intensity → new intensity_estimated and intensity_estimation_method optional fields on every green_summary.regions[] row whose intensity_source ends up RealTime. This is the signal Scope 2 reporting needs to distinguish measured emissions from modeled ones, auditors typically allow estimated values when the methodology is documented and surfacing the algorithm tag makes the audit trail self-contained.

The wire format change is fully additive: the new fields use #[serde(skip_serializing_if = "Option::is_none")] so older consumers continue to deserialize the breakdown unchanged. JSON output sees the two new fields appear only when the API actually returned them and the row's source is RealTime. GreenOps aggregates (avoidable_io_ops, IIS, waste ratio, top_offenders ranking) are untouched: the metadata is read-only, never multiplied into the carbon math.

Added

  • Per-zone API call dedup in crates/sentinel-core/src/score/electricity_maps/scraper.rs::run_scraper_loop. BTreeSet<&str> of unique zones, one fetch per zone per tick, dispatch to every matching cloud_region. Cloud_regions sharing the same zone are atomically updated together, no more divergence on transient single-region network blips.
  • isEstimated parsing. CarbonIntensityResponse now deserializes the optional isEstimated: Option<bool> field surfaced by the API. Some(true) means estimated, Some(false) means measured, None means the API did not surface the field (forward-compatibility with future API versions that may stop emitting it).
  • estimationMethod parsing. Same struct now deserializes the optional estimationMethod: Option<String> tag. Values are passed through verbatim, no whitelist is enforced so the scraper survives the addition of new methods upstream. Sanitized at the API boundary: capped at 64 bytes and rejected when the string contains control characters (defense-in-depth against log forging or downstream rendering surprises).
  • intensity_estimated field on green_summary.regions[]. New optional field on RegionBreakdown, present only when the row's intensity_source is RealTime.
  • intensity_estimation_method field on green_summary.regions[]. New optional field on RegionBreakdown, also gated to RealTime rows.
  • RealTimeIntensityEntry struct in score::carbon. Public type carrying gco2_per_kwh, is_estimated, estimation_method. Replaces the previous f64 value type used by CarbonContext.real_time_intensity. Includes a RealTimeIntensityEntry::measured(f64) convenience constructor for tests and callers without metadata.
  • ElectricityMapsState::snapshot_with_metadata. New method returning HashMap<String, RealTimeIntensityEntry> for the daemon scoring path. The original snapshot() (returning HashMap<String, f64>) stays unchanged for callers that do not need the metadata.

Changed

  • BREAKING (perf-sentinel-core, pre-1.0 so minor-bump allowed): CarbonContext.real_time_intensity field type went from Option<HashMap<String, f64>> to Option<HashMap<String, RealTimeIntensityEntry>>. In-tree callers were updated to construct entries via RealTimeIntensityEntry::measured(value) or the explicit struct literal when injecting estimation metadata. External consumers constructing CarbonContext directly must adapt.
  • fetch_intensity return type is now Result<FetchedReading, EmapsScraperError> instead of Result<f64, EmapsScraperError>. Internal to the scraper module so no external impact.
  • IntensityReading (state) drops Copy because Option<String> does not implement it. Clone is preserved. The struct is pub(super) so external consumers are not impacted.
  • consecutive_failures semantic is now zone-set-level instead of request-level. With the dedup pass, a partial-success tick (zone FR ok, zone DE ko) resets the counter because at least one zone returned data. Only a tick where all unique zones fail will increment, matching the operator-facing intent of the diagnostic warn.

Behavior

  • Default behavior unchanged for the common one-region-per-zone setup. Users with a single cloud_region -> zone mapping see the same number of API calls per tick as before.
  • Users with multiple cloud_region entries pointing at the same zone see fewer API calls per tick (down from N to the number of unique zones).
  • Empty region_map: the scraper now skips the tick instead of incrementing consecutive_failures and eventually firing a misleading "3 consecutive failures" warning. No real API call was attempted, no failure recorded.
  • Stale-data precedence on partial failure preserved: when one zone fails mid-tick, the missed cloud_regions retain their previous reading (and previous metadata) until the next successful fetch. Their last_update_ms is not refreshed so the staleness filter eventually evicts them at the configured threshold.

Install

Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):

curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.9/perf-sentinel-linux-amd64
chmod +x perf-sentinel-linux-amd64
sudo mv perf-sentinel-linux-amd64 /usr/local/bin/perf-sentinel

Linux binaries are statically linked against musl and run on any distribution (Alpine, Debian, RHEL, Ubuntu any version) regardless of glibc version, and inside FROM scratch images.

From crates.io:

cargo install perf-sentinel

Docker:

docker run --rm -p 4317:4317 -p 4318:4318 \
  ghcr.io/robintra/perf-sentinel:0.5.9 watch --listen-address 0.0.0.0

Also available on Docker Hub: robintrassard/perf-sentinel:0.5.9.

Helm (chart 0.2.12 ships 0.5.9 as its appVersion default):

helm install perf-sentinel oci://ghcr.io/robintra/charts/perf-sentinel \
  --version 0.2.12 \
  --namespace observability --create-namespace

Verify the binary against SHA256SUMS.txt:

curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.9/SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missing

Full diff: v0.5.8...v0.5.9

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.