Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fccf41d
feat(metrics): add classic histogram support
ethanolchik Jul 15, 2026
fbee146
feat(metrics): add support for TimeHistogram
ethanolchik Jul 16, 2026
9129dda
fix(metrics): expose snapshots and fix timer must-use message
ethanolchik Jul 20, 2026
535ae20
feat(metrics): add native histogram support
ethanolchik Jul 20, 2026
75a677f
feat(metrics): accept iterable time histogram buckets
ethanolchik Jul 21, 2026
2954308
refactor(metrics): create seconds function
ethanolchik Jul 21, 2026
1da1721
refactor(metrics): pre-sort histogram buckets
ethanolchik Jul 21, 2026
9a48173
chore(metrics): accredit prometools adaptations
ethanolchik Jul 21, 2026
9e0797d
fix(metrics): sort TimeHistogram buckets
ethanolchik Jul 23, 2026
47dd783
fix(metrics): track native histogram panic callers
ethanolchik Jul 23, 2026
a94f100
feat(metrics): expose Histogram::snapshot
ethanolchik Jul 27, 2026
8cfd523
fix(metrics): report native histogram config panics at the caller
ethanolchik Jul 27, 2026
ce64737
feat(metrics): add collection pipeline
ethanolchik Jul 21, 2026
4198345
feat(metrics): wire up protobuf and text encoders
ethanolchik Jul 21, 2026
3e091e8
test(metrics): cover legacy info metric encoding
ethanolchik Jul 23, 2026
207ef1a
docs(metrics-registry): clarify that EncodeMetric::encode must emit c…
ethanolchik Jul 24, 2026
746496b
refactor(metrics-registry): derive Debug for RegistrationMetadata
ethanolchik Jul 24, 2026
68a9324
feat(metrics): encode summaries and gauge histograms
ethanolchik Jul 24, 2026
1746521
fix(metrics): omit the OpenMetrics help line when there is no help text
ethanolchik Jul 27, 2026
94f88a0
feat(metrics): validate and sanitize names, labels, and exemplars
ethanolchik Jul 21, 2026
8b3947b
refactor(metrics): add support for UTF-8 characters in metric & label…
ethanolchik Jul 23, 2026
db61f80
fix(validation): remove duplicate assertions
ethanolchik Jul 23, 2026
9795818
test(metrics): cover validated summary and gauge histogram output
ethanolchik Jul 24, 2026
c5b338a
fix(metrics): quote label names only when they need UTF-8 syntax
ethanolchik Jul 27, 2026
e11649d
feat(metrics): add exemplar metrics
ethanolchik Jul 23, 2026
aa9bb89
test(metrics): update exemplar fixtures for UTF-8 names
ethanolchik Jul 23, 2026
ea4b084
refactor(metrics): reuse native histogram builder for exemplars
ethanolchik Jul 24, 2026
e5a499d
feat(metrics): add info metrics
ethanolchik Jul 27, 2026
76a973f
refactor(macros): register metrics through a backend-agnostic shim
ethanolchik Jul 27, 2026
16ec324
feat(foundations): add foundations-metrics backend behind a feature flag
ethanolchik Jul 27, 2026
3e3a649
feat(metrics): support user-defined custom metrics
ethanolchik Jul 27, 2026
3c949ed
test(foundations): cover reported info metrics
ethanolchik Jul 27, 2026
ae3c893
chore(metrics): deprecate text-only extra producers
ethanolchik Jul 27, 2026
736ed9b
feat(foundations): negotiate the scrape format from the Accept header
ethanolchik Jul 28, 2026
1c8f9f0
refactor(metrics): decide the service label from its first occurrence
ethanolchik Jul 28, 2026
70e7b10
fix(foundations): drop media ranges with an unparseable weight
ethanolchik Jul 28, 2026
b9427b4
docs(metrics): explain non-obvious validation and encoding decisions
ethanolchik Jul 28, 2026
250a3d7
docs(metrics): add a migration guide off the telemetry facade
ethanolchik Jul 28, 2026
d12a194
fix(foundations): only offer protobuf when it can carry the exposition
ethanolchik Jul 28, 2026
607ab3d
chore: make foundations-metrics and foundations-metrics-registry publ…
ethanolchik Jul 28, 2026
4234ce5
Merge branch 'main' into validation-and-utf8
ethanolchik Jul 28, 2026
28ab496
Merge branch 'validation-and-utf8' into metrics/exemplar-metrics
ethanolchik Jul 28, 2026
9840d12
Merge branch 'metrics/exemplar-metrics' into metrics/runtime
ethanolchik Jul 28, 2026
f321e4e
chore: follow RFC-047
ethanolchik Jul 28, 2026
352ddd8
feat(foundations): export native histograms from metrics facade
ethanolchik Jul 29, 2026
f959af3
refactor: respond with text format as opposed to 406
ethanolchik Jul 29, 2026
b452fef
feat(example): add native histogram example
ethanolchik Jul 29, 2026
8905aff
chore(foundations): cleanup telemetry server and comments
ethanolchik Jul 29, 2026
f3a1301
fix(foundations): refuse Accept weights that cannot be ranked
ethanolchik Jul 29, 2026
981f212
refactor(metrics): keep parking_lot out of the public API
ethanolchik Jul 29, 2026
b9ce5d5
feat(foundations): reject an unencodable service label name at startup
ethanolchik Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ anyhow = "1.0.102"
backtrace = "0.3.76"
foundations = { version = "5.8.0", path = "./foundations", default-features = false }
foundations-macros = { version = "=5.8.0", path = "./foundations-macros", default-features = false }
foundations-metrics-registry = { version = "0.1.0", path = "./foundations-metrics-registry" }
foundations-metrics-registry = { version = "0.1.0", path = "./foundations-metrics-registry", default-features = false }
foundations-metrics = { version = "0.1.0", path = "./foundations-metrics", default-features = false }
bindgen = { version = "0.72.1", default-features = false }
cc = "1.2.61"
cf-rustracing = "1.4.0"
Expand Down
12 changes: 10 additions & 2 deletions examples/http_server/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use hyper_util::rt::{TokioExecutor, TokioIo};
use std::convert::Infallible;
use std::net::{SocketAddr, TcpListener as StdTcpListener};
use std::sync::Arc;
use std::time::Instant;
use tokio::net::{TcpListener, TcpStream};

#[tokio::main]
Expand Down Expand Up @@ -218,6 +219,8 @@ async fn respond(
req: Request<Incoming>,
routes: Arc<Map<String, ResponseSettings>>,
) -> Result<Response<Full<Bytes>>, Infallible> {
let started = Instant::now();

log::add_fields! {
"request_uri" => req.uri().to_string(),
"method" => req.method().to_string()
Expand All @@ -233,7 +236,7 @@ async fn respond(
routes.get(req.uri().path())
};

Ok(match route {
let response = match route {
Some(route) => {
log::info!("sending response to the client"; "status_code" => route.status_code);

Expand All @@ -247,7 +250,12 @@ async fn respond(
metrics::http_server::requests_failed_total(&endpoint_name, 404).inc();
Response::builder().status(404).body("".into()).unwrap()
}
})
};

metrics::http_server::request_latency_seconds(&endpoint_name)
.observe(started.elapsed().as_secs_f64());

Ok(response)
}

#[cfg(target_os = "linux")]
Expand Down
12 changes: 11 additions & 1 deletion examples/http_server/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use foundations::telemetry::metrics::{Counter, Gauge, metrics};
use foundations::telemetry::metrics::{
Counter, Gauge, NativeHistogram, NativeHistogramBuilder, metrics,
};
use std::sync::Arc;

#[metrics]
Expand All @@ -14,4 +16,12 @@ pub(crate) mod http_server {

/// Number of failed requests.
pub fn requests_failed_total(endpoint_name: &Arc<String>, status_code: u16) -> Counter;

/// Time spent handling a request, in seconds.
#[ctor = NativeHistogramBuilder {
bucket_factor: 1.1,
zero_threshold: 0.0,
max_buckets: 160,
}]
pub fn request_latency_seconds(endpoint_name: &Arc<String>) -> NativeHistogram;
}
207 changes: 116 additions & 91 deletions foundations-macros/src/metrics/mod.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions foundations-metrics-registry/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "foundations-metrics-registry"
description = "Process-global metric registry and Prometheus protobuf data model for Foundations."
version = "0.1.0"
edition = { workspace = true }
repository = { workspace = true }
Expand Down
47 changes: 47 additions & 0 deletions foundations-metrics-registry/src/encode_metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,54 @@ use crate::proto::MetricFamily;
///
/// Encoding is best-effort: implementations skip (and internally report) any
/// metric or series that fails, so an empty `Vec` is a valid result.
///
/// This is the trait the registry stores, so an implementor controls its entire
/// output: it names its own families and owns any label set it exposes.
///
/// `foundations-metrics` also provides an `EncodeMetricValue` trait, whose
/// implementors encode only a *value* under a relative name. Pairing that with
/// `NamedMetric` supplies the name, and storing it in a `Family` supplies label
/// set storage and serialization. Prefer `EncodeMetricValue` unless a metric
/// needs to control naming or emit several families itself.
///
/// # Examples
///
/// ```
/// use foundations_metrics_registry::proto::{Gauge, LabelPair, Metric, MetricType};
/// use foundations_metrics_registry::{EncodeMetric, MetricFamily, RegistrationMetadata, register};
///
/// struct BuildRevision(&'static str);
///
/// impl EncodeMetric for BuildRevision {
/// fn encode(&self) -> Vec<MetricFamily> {
/// vec![MetricFamily {
/// // Complete producer-level name: nothing is prepended to it.
/// name: Some("build_revision".to_owned()),
/// help: Some("Revision the binary was built from.".to_owned()),
/// r#type: Some(MetricType::Gauge as i32),
/// metric: vec![Metric {
/// // Labels are this implementation's own responsibility.
/// label: vec![LabelPair {
/// name: Some("revision".to_owned()),
/// value: Some(self.0.to_owned()),
/// }],
/// gauge: Some(Gauge { value: Some(1.0) }),
/// ..Default::default()
/// }],
/// unit: None,
/// }]
/// }
/// }
///
/// register(
/// Box::new(BuildRevision("9f2c1ab")) as Box<dyn EncodeMetric>,
/// RegistrationMetadata::default(),
/// );
/// ```
pub trait EncodeMetric: Send + Sync + 'static {
/// Encodes this metric into zero or more [`MetricFamily`] messages.
///
/// Every returned [`MetricFamily`] must set `name` to a complete, non-empty
/// producer-level name.
fn encode(&self) -> Vec<MetricFamily>;
}
16 changes: 15 additions & 1 deletion foundations-metrics-registry/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
/// [`register`](crate::register). Build it from [`default`](Self::default) plus
/// the setters, since downstream crates can't use a struct literal.
#[non_exhaustive]
#[derive(Clone, Default)]
#[derive(Clone, Debug, Default)]
pub struct RegistrationMetadata {
/// Whether the metric is exported only when optional metrics are requested.
pub optional: bool,

/// Whether to suppress the service-name prefix for this metric.
pub unprefixed: bool,

/// Whether to suppress the service-name label for this metric.
///
/// Independent of [`unprefixed`](Self::unprefixed): a metric that opts out
/// of the service-name prefix is still labelled with the service name when
/// collection represents it as a label. Info metrics opt out of both.
pub unlabeled: bool,
}

impl RegistrationMetadata {
Expand All @@ -27,4 +34,11 @@ impl RegistrationMetadata {
self.unprefixed = unprefixed;
self
}

/// Sets [`unlabeled`](Self::unlabeled)
#[must_use]
pub fn unlabeled(mut self, unlabeled: bool) -> Self {
self.unlabeled = unlabeled;
self
}
}
3 changes: 2 additions & 1 deletion foundations-metrics-registry/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
mod model;

pub use model::{
Bucket, BucketSpan, Counter, Gauge, Histogram, LabelPair, Metric, MetricFamily, MetricType,
Bucket, BucketSpan, Counter, Exemplar, Gauge, Histogram, LabelPair, Metric, MetricFamily,
MetricType, Quantile, Summary,
};

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions foundations-metrics/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[package]
name = "foundations-metrics"
description = "Concrete Prometheus metric types and encoders for the Foundations stack."
version = "0.1.0"
readme = "README.md"
repository.workspace = true
edition.workspace = true
authors.workspace = true
Expand All @@ -15,6 +17,8 @@ prometheus-client = { version = "0.25.0", features = [
serde = { workspace = true, features = ["derive"] }
ryu = "1.0.23"
parking_lot = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }

[lints]
workspace = true
76 changes: 76 additions & 0 deletions foundations-metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# foundations-metrics

The evolving layer of the `foundations` metrics stack: the concrete metric types
and the logic that encodes them into the Prometheus protobuf data model. It sits
on top of `foundations-metrics-registry`, which owns the process-global registry
and the stable wire format.

## Migrating from `foundations::telemetry::metrics`

`foundations` re-exports this crate's items through
`foundations::telemetry::metrics` when its default `foundations-metrics-backend`
feature is enabled. The substitution is shape-compatible, so existing code keeps
compiling without changes — which makes the feature a transition aid rather than
the destination. The facade is slated for removal in the next major release.

To finish the move, depend on this crate directly and import from it:

```toml
[dependencies]
foundations-metrics = "0.1"
```

```rust
// Before
use foundations::telemetry::metrics::{Counter, Family, Gauge, InfoMetric, report_info};

// After
use foundations_metrics::{Counter, Family, Gauge, InfoMetric, report_info};
```

Two things stay with `foundations` and are not part of this crate:

- `#[metrics]` and `#[info_metric]`, which expand to paths that `foundations`
resolves. Keep invoking them through `foundations::telemetry::metrics`.
- Telemetry setup, including the service name that prefixes or labels collected
metrics. This crate takes that name through
[`CollectionOptions`](https://docs.rs/foundations-metrics/latest/foundations_metrics/struct.CollectionOptions.html)
at collection time instead of discovering it itself.

Mixing the two paths works only with `foundations-metrics-backend` enabled;
without it, `foundations::telemetry::metrics` names its own legacy types and
those are distinct from this crate's.

### Deprecated items

Most imports move by renaming the path. These have no drop-in equivalent:

| Deprecated | Replacement |
| --- | --- |
| `foundations::telemetry::metrics::add_extra_producer` | [`register`](https://docs.rs/foundations-metrics/latest/foundations_metrics/fn.register.html) |
| `foundations::telemetry::metrics::ExtraProducer` | [`EncodeMetric`](https://docs.rs/foundations-metrics/latest/foundations_metrics/trait.EncodeMetric.html) or [`EncodeMetricValue`](https://docs.rs/foundations-metrics/latest/foundations_metrics/trait.EncodeMetricValue.html) |

Extra producers append pre-encoded Prometheus *text* to the scrape buffer, which
bypasses validation and has no protobuf representation — so their output is
silently absent from a protobuf scrape. Implementing `EncodeMetric` returns
structured metric families instead, which both encoders can serve. Prefer
`EncodeMetricValue` paired with `NamedMetric` and `Family` unless a metric needs
to control its own naming or emit several families.

Enabling the feature also stops draining the `prometheus` crate's global
registry, which the legacy collector exported alongside its own. Anything
registered there, including that crate's process collector, needs to be
re-registered through `register`.

For further guidance, each deprecated item names its own replacement in the
[`foundations` API docs](https://docs.rs/foundations/), and the traits above are
documented with examples in the [API docs for this
crate](https://docs.rs/foundations-metrics/).

## Documentation

https://docs.rs/foundations-metrics/

## License

BSD-3 licensed. See the [LICENSE](../LICENSE) file for details.
Loading