Skip to content

feat(foundations): use foundations-metrics as the macro backend + expose a foundations-metrics facade - #240

Draft
ethanolchik wants to merge 51 commits into
cloudflare:mainfrom
ethanolchik:metrics/runtime
Draft

feat(foundations): use foundations-metrics as the macro backend + expose a foundations-metrics facade#240
ethanolchik wants to merge 51 commits into
cloudflare:mainfrom
ethanolchik:metrics/runtime

Conversation

@ethanolchik

@ethanolchik ethanolchik commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Overview

Wires foundations-metrics in behind foundations::telemetry::metrics via a new foundations-metrics-backend feature, enabled by default, replacing the legacy prometheus-client/prometools stack while keeping call sites source-compatible. Also adds Accept-header negotiation to /metrics, a public API for custom metrics, and makes both new crates publishable.

The metric types themselves (counters, gauges, classic/time/native histograms, exemplars, info metrics) already existed in foundations-metrics before this branch point. This is the integration work, not the implementation.

Behaviour changes

Worth reviewing closely - all follow from the backend being on by default.

Exposition output differs. Verified by capturing real collect() output in both configs:

legacy new backend
integral sample value metric 1 metric 1.0
empty help text # HELP name . (omitted)
family whose every row fails label serialisation metadata, zero samples family dropped

Values are protobuf doubles, hence 1.0. Parsers see identical series, but consumers asserting on exact /metrics strings will notice; three existing tests needed updating.

The prometheus crate's global registry is no longer drained. The legacy collector exported prometheus::gather() alongside its own. Anything registered there - including the process feature's collector — silently disappears and must be re-registered through register. The most likely surprise for an existing service; documented in foundations-metrics/README.md.

/metrics honours Accept. It previously ignored it and always returned application/openmetrics-text; version=1.0.0; charset=utf-8 - still what a scraper sending no Accept, or one accepting text, receives. New: length-delimited protobuf and UTF-8-unescaped OpenMetrics text on request. A weight invalidates its range rather than ranking it last when it is q=0, unparseable, or outside RFC 9110's 0–1 — the last case covering nan and inf, which parse as floats but cannot be ranked. The negotiated escaping is reported but not enforced.

Negotiation never fails a scrape. An Accept matching nothing available is served the text fallback with a logged warning. A 406 was rejected: a scraper counts it as a failed scrape and records nothing, whereas an unrequested format is at worst ignored. The log is the only signal, since the response is an ordinary 200.

Protobuf is withheld while any ExtraProducer is registered, since that extension point is text-only and cannot be transcoded. All-or-nothing: one producer disables protobuf process-wide, and a protobuf-only client gets text rather than a truncated exposition. Evaluated per scrape, not a setting. Native histograms are not representable in OpenMetrics text, so a process with both cannot export the full set in either format.

Telemetry init now fails on a service label name that cannot be encoded. service_name_format is fixed for the process lifetime, but LabelWithName("") was only caught at collect time, where it skips every family — a scrape that parses as a healthy process exporting nothing. Rejected at startup instead, naming the setting at fault.

New API

Custom metrics: implement EncodeMetricValue (widened from pub(crate)) and wrap in NamedMetric, or implement EncodeMetric directly to control naming. Register via register(Box<dyn EncodeMetric>, RegistrationMetadata).

Newly public through telemetry::metrics, only with the backend enabled — this module's surface is now feature-dependent: NativeHistogram, NativeHistogramBuilder, NativeHistogramWithExemplars, EncodeMetric, EncodeMetricValue, IntoMetrics, LabelError, MetricFamily, NamedMetric, RegistrationMetadata, proto, register, to_label_pairs.

The 12 pre-existing names (Counter, Family, Gauge, the histogram and exemplar types, MetricConstructor, GaugeGuard, RangeGauge, InfoMetric) now resolve to different concrete types. Source-compatible in typical use; affects code naming the underlying prometheus_client/prometools types or implementing MetricConstructor for a foreign type.

Deprecations

add_extra_producer and ExtraProducer, in favour of EncodeMetric + register — text output bypasses validation and is absent from protobuf scrapes. Still functional under both backends. Downstreams building with -D warnings will need #[allow(deprecated)].

Other

  • Service name is applied at collection time rather than registration time. The backend reads it from a OnceLock without initialising, so collecting before init no longer permanently poisons the prefix with undefined_.
  • Collection diagnostics route through telemetry instead of stderr; a service-installed hook still wins.
  • #[metrics] names metric types through the facade rather than reexports_for_macros, which it still uses for serde. No change to its accepted input — same crate_path, unprefixed, #[ctor], #[optional], #[with_removal].
  • Service labels are decided from the first occurrence. Exported series are unchanged; only the diagnostic message differs, and no longer depends on label order.
  • No lock type reaches foundations-metrics' public API, so RFC-047 needs no parking_lot reexport: CounterWithExemplar::get/inner now return the guard Family already used, renamed FamilyMetricGuardMetricReadGuard. Not exposed through the facade.
  • Added foundations-metrics/README.md with a migration guide, and documented existing decisions that read as arbitrary (UTF-8 name validation, NUL rejection, f64::MAX as the terminal bucket bound).
  • Added the description fields required by cargo publish. No new third-party dependencies.
  • Unrelated: build.rs now compiles the linux/seccomp.h probe into a scratch directory so its artefacts don't linger in OUT_DIR.

Tests

New coverage for custom metrics registered from outside the crate and through the facade, info metrics keeping unprefixed names, Accept negotiation including a genuinely binary protobuf body, protobuf withholding, the fallback warning, un-rankable Accept weights, and startup rejection of an un-encodable service label name.

Use cargo nextest run. Two tests in foundations/tests/metrics.rs fail under plain cargo test because they share the global registry in one process; this reproduces at the base commit and is not introduced here.

Add owned fixed-bucket storage, direct protobuf encoding, and support for the legacy HistogramBuilder API (only classic histograms in this commit).
Choose to pre-sort histogram buckets over a sorted boolean variable.

Also satisfies cargo clippy & cargo fmt
… names

Legacy metric and label names must match Prometheus' name grammars. Names outside these grammars cause Prometheus to reject the scrape, and the grammars exclude non-ASCII characters.

Foundations now quotes label names and uses OpenMetrics' quoted metric-name syntax when required. This allows UTF-8 and other nonstandard names without affecting legacy compatibility.
Add registerable counter, classic histogram, and native histogram
types that retain exemplars alongside observations.

Support typed and legacy label sets, Family usage, OpenMetrics text,
and Prometheus protobuf encoding. Validate exemplars during collection
and report serialisation failures as non-fatal diagnostics.
@ethanolchik ethanolchik changed the title Metrics/runtime feat(foundations): use foundations-metrics as the macro backend + expose a foundations-metrics facade Jul 28, 2026
ethanolchik and others added 17 commits July 28, 2026 15:35
`nan` and `inf` parse as floats, so an unranked weight reached the
comparison: `NaN` lost none of them and pinned itself as the winner, and
`inf` outranked a legitimate `q=1.0`. Bound the weight to the range RFC
9110 defines instead.
`CounterWithExemplar::get` and `inner` returned a
`parking_lot::MappedRwLockReadGuard`, so naming either return type meant
depending on `parking_lot` directly, which RFC-047 requires a reexport
for. Generalise the guard `Family` already wrapped instead, so no lock
type reaches the public API and none has to be reexported.
`service_name_format` is fixed for the process lifetime, but an
unencodable label name was only found at collect time, which skips every
metric family. The scrape that results parses as a healthy process
exporting nothing, so the misconfiguration shows up as silent staleness.
Check it during telemetry init and name the setting instead.

Reexports `is_valid_name` and `NAME_REQUIREMENT` so the rule stays with
the crate that enforces it rather than being duplicated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant