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 matchingcloud_region. Cloud_regions sharing the same zone are atomically updated together, no more divergence on transient single-region network blips. isEstimatedparsing.CarbonIntensityResponsenow deserializes the optionalisEstimated: Option<bool>field surfaced by the API.Some(true)means estimated,Some(false)means measured,Nonemeans the API did not surface the field (forward-compatibility with future API versions that may stop emitting it).estimationMethodparsing. Same struct now deserializes the optionalestimationMethod: 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_estimatedfield ongreen_summary.regions[]. New optional field onRegionBreakdown, present only when the row'sintensity_sourceisRealTime.intensity_estimation_methodfield ongreen_summary.regions[]. New optional field onRegionBreakdown, also gated toRealTimerows.RealTimeIntensityEntrystruct inscore::carbon. Public type carryinggco2_per_kwh,is_estimated,estimation_method. Replaces the previousf64value type used byCarbonContext.real_time_intensity. Includes aRealTimeIntensityEntry::measured(f64)convenience constructor for tests and callers without metadata.ElectricityMapsState::snapshot_with_metadata. New method returningHashMap<String, RealTimeIntensityEntry>for the daemon scoring path. The originalsnapshot()(returningHashMap<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_intensityfield type went fromOption<HashMap<String, f64>>toOption<HashMap<String, RealTimeIntensityEntry>>. In-tree callers were updated to construct entries viaRealTimeIntensityEntry::measured(value)or the explicit struct literal when injecting estimation metadata. External consumers constructingCarbonContextdirectly must adapt. fetch_intensityreturn type is nowResult<FetchedReading, EmapsScraperError>instead ofResult<f64, EmapsScraperError>. Internal to the scraper module so no external impact.IntensityReading(state) dropsCopybecauseOption<String>does not implement it.Cloneis preserved. The struct ispub(super)so external consumers are not impacted.consecutive_failuressemantic 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 -> zonemapping see the same number of API calls per tick as before. - Users with multiple
cloud_regionentries 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 incrementingconsecutive_failuresand 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_msis 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-sentinelLinux 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-sentinelDocker:
docker run --rm -p 4317:4317 -p 4318:4318 \
ghcr.io/robintra/perf-sentinel:0.5.9 watch --listen-address 0.0.0.0Also 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-namespaceVerify 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-missingFull diff: v0.5.8...v0.5.9