From f03ff818c1c335c1a57f65cb3b52d8a9f3b6093e Mon Sep 17 00:00:00 2001 From: Ethan Olchik Date: Wed, 8 Jul 2026 12:18:51 +0100 Subject: [PATCH 1/3] feat(metrics-registry): add crate with Prometheus protobuf data model --- Cargo.toml | 5 + foundations-metrics-registry/Cargo.toml | 18 ++ foundations-metrics-registry/build.rs | 17 ++ .../proto/metrics.proto | 156 ++++++++++++++++++ foundations-metrics-registry/src/lib.rs | 22 +++ foundations-metrics-registry/src/proto/mod.rs | 60 +++++++ .../src/proto/model.rs | 7 + 7 files changed, 285 insertions(+) create mode 100644 foundations-metrics-registry/Cargo.toml create mode 100644 foundations-metrics-registry/build.rs create mode 100644 foundations-metrics-registry/proto/metrics.proto create mode 100644 foundations-metrics-registry/src/lib.rs create mode 100644 foundations-metrics-registry/src/proto/mod.rs create mode 100644 foundations-metrics-registry/src/proto/model.rs diff --git a/Cargo.toml b/Cargo.toml index f60e3eb..9c231cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "foundations", "foundations-macros", "foundations-sentry", + "foundations-metrics-registry", "examples", "tools/gen-syscall-enum", ] @@ -71,6 +72,10 @@ proc-macro2 = { version = "1.0.106", default-features = false } prometheus = { version = "0.14.0", default-features = false } prometheus-client = "0.18.1" prometools = "0.2.3" +prost = "0.14.4" +prost-build = "0.14.4" +prost-types = "0.14.4" +protox = "0.9.1" rand = "0.10.1" percent-encoding = "2.3.2" quote = "1.0.45" diff --git a/foundations-metrics-registry/Cargo.toml b/foundations-metrics-registry/Cargo.toml new file mode 100644 index 0000000..550c32c --- /dev/null +++ b/foundations-metrics-registry/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "foundations-metrics-registry" +version = "0.1.0" +edition = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +prost = { workspace = true } +prost-types = { workspace = true } + +[build-dependencies] +prost-build = { workspace = true } +protox = { workspace = true } diff --git a/foundations-metrics-registry/build.rs b/foundations-metrics-registry/build.rs new file mode 100644 index 0000000..013a89c --- /dev/null +++ b/foundations-metrics-registry/build.rs @@ -0,0 +1,17 @@ +//! Generates the `io.prometheus.client` protobuf model from the vendored +//! `proto/metrics.proto` into `OUT_DIR`, where `src/proto/model.rs` includes it. + +use std::error::Error; + +const PROTO_PATH: &str = "proto/metrics.proto"; +const INCLUDE: &str = "proto"; + +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed={PROTO_PATH}"); + println!("cargo:rerun-if-changed={INCLUDE}"); + + let file_descriptor_set = protox::compile([PROTO_PATH], [INCLUDE])?; + prost_build::Config::new().compile_fds(file_descriptor_set)?; + + Ok(()) +} diff --git a/foundations-metrics-registry/proto/metrics.proto b/foundations-metrics-registry/proto/metrics.proto new file mode 100644 index 0000000..2f2bff0 --- /dev/null +++ b/foundations-metrics-registry/proto/metrics.proto @@ -0,0 +1,156 @@ +// Copyright 2013 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package io.prometheus.client; +option java_package = "io.prometheus.client"; +option go_package = "github.com/prometheus/client_model/go;io_prometheus_client"; + +import "google/protobuf/timestamp.proto"; + +message LabelPair { + optional string name = 1; + optional string value = 2; +} + +enum MetricType { + // COUNTER must use the Metric field "counter". + COUNTER = 0; + // GAUGE must use the Metric field "gauge". + GAUGE = 1; + // SUMMARY must use the Metric field "summary". + SUMMARY = 2; + // UNTYPED must use the Metric field "untyped". + UNTYPED = 3; + // HISTOGRAM must use the Metric field "histogram". + HISTOGRAM = 4; + // GAUGE_HISTOGRAM must use the Metric field "histogram". + GAUGE_HISTOGRAM = 5; +} + +message Gauge { + optional double value = 1; +} + +message Counter { + optional double value = 1; + optional Exemplar exemplar = 2; + + optional google.protobuf.Timestamp created_timestamp = 3; +} + +message Quantile { + optional double quantile = 1; + optional double value = 2; +} + +message Summary { + optional uint64 sample_count = 1; + optional double sample_sum = 2; + repeated Quantile quantile = 3; + + optional google.protobuf.Timestamp created_timestamp = 4; +} + +message Untyped { + optional double value = 1; +} + +message Histogram { + optional uint64 sample_count = 1; + optional double sample_count_float = 4; // Overrides sample_count if > 0. + optional double sample_sum = 2; + // Buckets for the conventional histogram. + repeated Bucket bucket = 3; // Ordered in increasing order of upper_bound, +Inf bucket is optional. + + optional google.protobuf.Timestamp created_timestamp = 15; + + // Everything below here is for native histograms (formerly known as sparse histograms). + + // schema defines the bucket schema. Currently, valid numbers are -4 <= n <= 8. + // They are all for base-2 bucket schemas, where 1 is a bucket boundary in each case, and + // then each power of two is divided into 2^n logarithmic buckets. + // Or in other words, each bucket boundary is the previous boundary times 2^(2^-n). + // In the future, more bucket schemas may be added using numbers < -4 or > 8. + optional sint32 schema = 5; + optional double zero_threshold = 6; // Breadth of the zero bucket. + optional uint64 zero_count = 7; // Count in zero bucket. + optional double zero_count_float = 8; // Overrides sb_zero_count if > 0. + + // Negative buckets for the native histogram. + repeated BucketSpan negative_span = 9; + // Use either "negative_delta" or "negative_count", the former for + // regular histograms with integer counts, the latter for float + // histograms. + repeated sint64 negative_delta = 10; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double negative_count = 11; // Absolute count of each bucket. + + // Positive buckets for the native histogram. + // Use a no-op span (offset 0, length 0) for a native histogram without any + // observations yet and with a zero_threshold of 0. Otherwise, it would be + // indistinguishable from a classic histogram. + repeated BucketSpan positive_span = 12; + // Use either "positive_delta" or "positive_count", the former for + // regular histograms with integer counts, the latter for float + // histograms. + repeated sint64 positive_delta = 13; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). + repeated double positive_count = 14; // Absolute count of each bucket. + + // Only used for native histograms. These exemplars MUST have a timestamp. + repeated Exemplar exemplars = 16; +} + +// A Bucket of a conventional histogram, each of which is treated as +// an individual counter-like time series by Prometheus. +message Bucket { + optional uint64 cumulative_count = 1; // Cumulative in increasing order. + optional double cumulative_count_float = 4; // Overrides cumulative_count if > 0. + optional double upper_bound = 2; // Inclusive. + optional Exemplar exemplar = 3; +} + +// A BucketSpan defines a number of consecutive buckets in a native +// histogram with their offset. Logically, it would be more +// straightforward to include the bucket counts in the Span. However, +// the protobuf representation is more compact in the way the data is +// structured here (with all the buckets in a single array separate +// from the Spans). +message BucketSpan { + optional sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative). + optional uint32 length = 2; // Length of consecutive buckets. +} + +message Exemplar { + repeated LabelPair label = 1; + optional double value = 2; + optional google.protobuf.Timestamp timestamp = 3; // OpenMetrics-style. +} + +message Metric { + repeated LabelPair label = 1; + optional Gauge gauge = 2; + optional Counter counter = 3; + optional Summary summary = 4; + optional Untyped untyped = 5; + optional Histogram histogram = 7; + optional int64 timestamp_ms = 6; +} + +message MetricFamily { + optional string name = 1; + optional string help = 2; + optional MetricType type = 3; + repeated Metric metric = 4; + optional string unit = 5; +} diff --git a/foundations-metrics-registry/src/lib.rs b/foundations-metrics-registry/src/lib.rs new file mode 100644 index 0000000..2ff4ad2 --- /dev/null +++ b/foundations-metrics-registry/src/lib.rs @@ -0,0 +1,22 @@ +//! The stable core of the `foundations` metrics stack. +//! +//! `foundations-metrics-registry` holds the parts of the metrics stack that must +//! stay stable across `foundations` major versions. Today that is the [`proto`] +//! data model: the [`prometheus/client_model`] protobuf types that are the +//! canonical wire format for the protobuf `/metrics` endpoint. +//! +//! The crate is kept small and dependency-light on purpose. The metrics registry +//! is a process-global singleton, so when two `foundations` majors are linked +//! into the same binary they must resolve to the *same* version of this crate to +//! share one registry rather than splitting metrics between them. A minimal, +//! slow-moving crate is what keeps that shared version easy to hold still — the +//! one expected source-breaking change is a change to the protobuf data model. +//! +//! Everything that can evolve more freely — metric types, encoders, label +//! serialisation, and service-name handling — lives in the sibling +//! `foundations-metrics` crate, not here. +//! +//! [`prometheus/client_model`]: https://github.com/prometheus/client_model +#![warn(missing_docs)] + +pub mod proto; diff --git a/foundations-metrics-registry/src/proto/mod.rs b/foundations-metrics-registry/src/proto/mod.rs new file mode 100644 index 0000000..8e752a3 --- /dev/null +++ b/foundations-metrics-registry/src/proto/mod.rs @@ -0,0 +1,60 @@ +//! The Prometheus protobuf data model (package `io.prometheus.client`). +//! +//! These types mirror [`prometheus/client_model`] and are the canonical wire +//! format for the protobuf `/metrics` endpoint. They are generated from the vendored +//! `proto/metrics.proto` (via `protox`/`prost`.) +//! +//! [`prometheus/client_model`]: https://github.com/prometheus/client_model + +#[allow(missing_docs, unreachable_pub, clippy::all)] +mod model; + +pub use model::{BucketSpan, Histogram, Metric, MetricFamily, MetricType}; + +#[cfg(test)] +mod tests { + use super::*; + use prost::Message; + + #[test] + fn native_histogram_family_round_trips() { + let family = MetricFamily { + name: Some("requests_duration_seconds".to_owned()), + help: Some("Request duration.".to_owned()), + r#type: Some(MetricType::Histogram as i32), + unit: Some("seconds".to_owned()), + metric: vec![Metric { + histogram: Some(Histogram { + sample_count: Some(3), + sample_sum: Some(1.5), + schema: Some(2), + zero_threshold: Some(1e-9), + zero_count: Some(1), + positive_span: vec![BucketSpan { + offset: Some(0), + length: Some(2), + }], + positive_delta: vec![1, 0], + ..Default::default() + }), + ..Default::default() + }], + }; + + let decoded = MetricFamily::decode(family.encode_to_vec().as_slice()) + .expect("protobuf roundtrip should succeed"); + + assert_eq!(decoded, family); + assert_eq!(decoded.r#type(), MetricType::Histogram); + + let histogram = decoded.metric[0] + .histogram + .as_ref() + .expect("histogram present"); + + assert_eq!(histogram.schema, Some(2)); + assert_eq!(histogram.zero_count, Some(1)); + assert_eq!(histogram.positive_span.len(), 1); + assert_eq!(histogram.positive_delta, vec![1, 0]); + } +} diff --git a/foundations-metrics-registry/src/proto/model.rs b/foundations-metrics-registry/src/proto/model.rs new file mode 100644 index 0000000..004e17e --- /dev/null +++ b/foundations-metrics-registry/src/proto/model.rs @@ -0,0 +1,7 @@ +//! Raw `prost`-generated Prometheus protobuf types +//! +//! The code is generated at build time into `$OUT_DIR/io.prometheus.client.rs` +//! from the vendored `proto/metrics.proto` and included here. Prefer the +//! re-exports from the parent [`proto`](crate::proto) module over reaching into this one. + +include!(concat!(env!("OUT_DIR"), "/io.prometheus.client.rs")); From 2a99cae173058178b813fcce506339478b927d8c Mon Sep 17 00:00:00 2001 From: Ethan Olchik Date: Wed, 8 Jul 2026 15:23:51 +0100 Subject: [PATCH 2/3] feat(metrics-registry): vendor generated Prometheus protobuf model --- foundations-metrics-registry/Cargo.toml | 3 +- foundations-metrics-registry/build.rs | 17 -- foundations-metrics-registry/src/proto/mod.rs | 4 +- .../src/proto/model.rs | 232 +++++++++++++++++- .../tests/proto_codegen.rs | 46 ++++ 5 files changed, 275 insertions(+), 27 deletions(-) delete mode 100644 foundations-metrics-registry/build.rs create mode 100644 foundations-metrics-registry/tests/proto_codegen.rs diff --git a/foundations-metrics-registry/Cargo.toml b/foundations-metrics-registry/Cargo.toml index 550c32c..90e436c 100644 --- a/foundations-metrics-registry/Cargo.toml +++ b/foundations-metrics-registry/Cargo.toml @@ -13,6 +13,7 @@ workspace = true prost = { workspace = true } prost-types = { workspace = true } -[build-dependencies] +[dev-dependencies] prost-build = { workspace = true } protox = { workspace = true } +tempfile = { workspace = true } diff --git a/foundations-metrics-registry/build.rs b/foundations-metrics-registry/build.rs deleted file mode 100644 index 013a89c..0000000 --- a/foundations-metrics-registry/build.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Generates the `io.prometheus.client` protobuf model from the vendored -//! `proto/metrics.proto` into `OUT_DIR`, where `src/proto/model.rs` includes it. - -use std::error::Error; - -const PROTO_PATH: &str = "proto/metrics.proto"; -const INCLUDE: &str = "proto"; - -fn main() -> Result<(), Box> { - println!("cargo:rerun-if-changed={PROTO_PATH}"); - println!("cargo:rerun-if-changed={INCLUDE}"); - - let file_descriptor_set = protox::compile([PROTO_PATH], [INCLUDE])?; - prost_build::Config::new().compile_fds(file_descriptor_set)?; - - Ok(()) -} diff --git a/foundations-metrics-registry/src/proto/mod.rs b/foundations-metrics-registry/src/proto/mod.rs index 8e752a3..0b124a7 100644 --- a/foundations-metrics-registry/src/proto/mod.rs +++ b/foundations-metrics-registry/src/proto/mod.rs @@ -1,8 +1,8 @@ //! The Prometheus protobuf data model (package `io.prometheus.client`). //! //! These types mirror [`prometheus/client_model`] and are the canonical wire -//! format for the protobuf `/metrics` endpoint. They are generated from the vendored -//! `proto/metrics.proto` (via `protox`/`prost`.) +//! format for the protobuf `/metrics` endpoint. The generated Rust model is +//! checked in and verified against the vendored `proto/metrics.proto`. //! //! [`prometheus/client_model`]: https://github.com/prometheus/client_model diff --git a/foundations-metrics-registry/src/proto/model.rs b/foundations-metrics-registry/src/proto/model.rs index 004e17e..53ed6d8 100644 --- a/foundations-metrics-registry/src/proto/model.rs +++ b/foundations-metrics-registry/src/proto/model.rs @@ -1,7 +1,225 @@ -//! Raw `prost`-generated Prometheus protobuf types -//! -//! The code is generated at build time into `$OUT_DIR/io.prometheus.client.rs` -//! from the vendored `proto/metrics.proto` and included here. Prefer the -//! re-exports from the parent [`proto`](crate::proto) module over reaching into this one. - -include!(concat!(env!("OUT_DIR"), "/io.prometheus.client.rs")); +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LabelPair { + #[prost(string, optional, tag = "1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "2")] + pub value: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Gauge { + #[prost(double, optional, tag = "1")] + pub value: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Counter { + #[prost(double, optional, tag = "1")] + pub value: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub exemplar: ::core::option::Option, + #[prost(message, optional, tag = "3")] + pub created_timestamp: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Quantile { + #[prost(double, optional, tag = "1")] + pub quantile: ::core::option::Option, + #[prost(double, optional, tag = "2")] + pub value: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Summary { + #[prost(uint64, optional, tag = "1")] + pub sample_count: ::core::option::Option, + #[prost(double, optional, tag = "2")] + pub sample_sum: ::core::option::Option, + #[prost(message, repeated, tag = "3")] + pub quantile: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub created_timestamp: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct Untyped { + #[prost(double, optional, tag = "1")] + pub value: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Histogram { + #[prost(uint64, optional, tag = "1")] + pub sample_count: ::core::option::Option, + /// Overrides sample_count if > 0. + #[prost(double, optional, tag = "4")] + pub sample_count_float: ::core::option::Option, + #[prost(double, optional, tag = "2")] + pub sample_sum: ::core::option::Option, + /// Buckets for the conventional histogram. + /// + /// Ordered in increasing order of upper_bound, +Inf bucket is optional. + #[prost(message, repeated, tag = "3")] + pub bucket: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "15")] + pub created_timestamp: ::core::option::Option<::prost_types::Timestamp>, + /// schema defines the bucket schema. Currently, valid numbers are -4 <= n <= 8. + /// They are all for base-2 bucket schemas, where 1 is a bucket boundary in each case, and + /// then each power of two is divided into 2^n logarithmic buckets. + /// Or in other words, each bucket boundary is the previous boundary times 2^(2^-n). + /// In the future, more bucket schemas may be added using numbers < -4 or > 8. + #[prost(sint32, optional, tag = "5")] + pub schema: ::core::option::Option, + /// Breadth of the zero bucket. + #[prost(double, optional, tag = "6")] + pub zero_threshold: ::core::option::Option, + /// Count in zero bucket. + #[prost(uint64, optional, tag = "7")] + pub zero_count: ::core::option::Option, + /// Overrides sb_zero_count if > 0. + #[prost(double, optional, tag = "8")] + pub zero_count_float: ::core::option::Option, + /// Negative buckets for the native histogram. + #[prost(message, repeated, tag = "9")] + pub negative_span: ::prost::alloc::vec::Vec, + /// Use either "negative_delta" or "negative_count", the former for + /// regular histograms with integer counts, the latter for float + /// histograms. + /// + /// Count delta of each bucket compared to previous one (or to zero for 1st bucket). + #[prost(sint64, repeated, packed = "false", tag = "10")] + pub negative_delta: ::prost::alloc::vec::Vec, + /// Absolute count of each bucket. + #[prost(double, repeated, packed = "false", tag = "11")] + pub negative_count: ::prost::alloc::vec::Vec, + /// Positive buckets for the native histogram. + /// Use a no-op span (offset 0, length 0) for a native histogram without any + /// observations yet and with a zero_threshold of 0. Otherwise, it would be + /// indistinguishable from a classic histogram. + #[prost(message, repeated, tag = "12")] + pub positive_span: ::prost::alloc::vec::Vec, + /// Use either "positive_delta" or "positive_count", the former for + /// regular histograms with integer counts, the latter for float + /// histograms. + /// + /// Count delta of each bucket compared to previous one (or to zero for 1st bucket). + #[prost(sint64, repeated, packed = "false", tag = "13")] + pub positive_delta: ::prost::alloc::vec::Vec, + /// Absolute count of each bucket. + #[prost(double, repeated, packed = "false", tag = "14")] + pub positive_count: ::prost::alloc::vec::Vec, + /// Only used for native histograms. These exemplars MUST have a timestamp. + #[prost(message, repeated, tag = "16")] + pub exemplars: ::prost::alloc::vec::Vec, +} +/// A Bucket of a conventional histogram, each of which is treated as +/// an individual counter-like time series by Prometheus. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bucket { + /// Cumulative in increasing order. + #[prost(uint64, optional, tag = "1")] + pub cumulative_count: ::core::option::Option, + /// Overrides cumulative_count if > 0. + #[prost(double, optional, tag = "4")] + pub cumulative_count_float: ::core::option::Option, + /// Inclusive. + #[prost(double, optional, tag = "2")] + pub upper_bound: ::core::option::Option, + #[prost(message, optional, tag = "3")] + pub exemplar: ::core::option::Option, +} +/// A BucketSpan defines a number of consecutive buckets in a native +/// histogram with their offset. Logically, it would be more +/// straightforward to include the bucket counts in the Span. However, +/// the protobuf representation is more compact in the way the data is +/// structured here (with all the buckets in a single array separate +/// from the Spans). +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct BucketSpan { + /// Gap to previous span, or starting point for 1st span (which can be negative). + #[prost(sint32, optional, tag = "1")] + pub offset: ::core::option::Option, + /// Length of consecutive buckets. + #[prost(uint32, optional, tag = "2")] + pub length: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Exemplar { + #[prost(message, repeated, tag = "1")] + pub label: ::prost::alloc::vec::Vec, + #[prost(double, optional, tag = "2")] + pub value: ::core::option::Option, + /// OpenMetrics-style. + #[prost(message, optional, tag = "3")] + pub timestamp: ::core::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Metric { + #[prost(message, repeated, tag = "1")] + pub label: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "2")] + pub gauge: ::core::option::Option, + #[prost(message, optional, tag = "3")] + pub counter: ::core::option::Option, + #[prost(message, optional, tag = "4")] + pub summary: ::core::option::Option, + #[prost(message, optional, tag = "5")] + pub untyped: ::core::option::Option, + #[prost(message, optional, tag = "7")] + pub histogram: ::core::option::Option, + #[prost(int64, optional, tag = "6")] + pub timestamp_ms: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MetricFamily { + #[prost(string, optional, tag = "1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "2")] + pub help: ::core::option::Option<::prost::alloc::string::String>, + #[prost(enumeration = "MetricType", optional, tag = "3")] + pub r#type: ::core::option::Option, + #[prost(message, repeated, tag = "4")] + pub metric: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "5")] + pub unit: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum MetricType { + /// COUNTER must use the Metric field "counter". + Counter = 0, + /// GAUGE must use the Metric field "gauge". + Gauge = 1, + /// SUMMARY must use the Metric field "summary". + Summary = 2, + /// UNTYPED must use the Metric field "untyped". + Untyped = 3, + /// HISTOGRAM must use the Metric field "histogram". + Histogram = 4, + /// GAUGE_HISTOGRAM must use the Metric field "histogram". + GaugeHistogram = 5, +} +impl MetricType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Counter => "COUNTER", + Self::Gauge => "GAUGE", + Self::Summary => "SUMMARY", + Self::Untyped => "UNTYPED", + Self::Histogram => "HISTOGRAM", + Self::GaugeHistogram => "GAUGE_HISTOGRAM", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "COUNTER" => Some(Self::Counter), + "GAUGE" => Some(Self::Gauge), + "SUMMARY" => Some(Self::Summary), + "UNTYPED" => Some(Self::Untyped), + "HISTOGRAM" => Some(Self::Histogram), + "GAUGE_HISTOGRAM" => Some(Self::GaugeHistogram), + _ => None, + } + } +} diff --git a/foundations-metrics-registry/tests/proto_codegen.rs b/foundations-metrics-registry/tests/proto_codegen.rs new file mode 100644 index 0000000..1a2bbba --- /dev/null +++ b/foundations-metrics-registry/tests/proto_codegen.rs @@ -0,0 +1,46 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +const PROTO_PATH: &str = "proto/metrics.proto"; +const GENERATED_PATH: &str = "src/proto/model.rs"; +const PROST_OUTPUT_FILE: &str = "io.prometheus.client.rs"; + +#[test] +fn generated_proto_matches_vendored_proto() { + let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let checked_in_path = crate_dir.join(GENERATED_PATH); + let generated = generate_proto(&crate_dir); + + let checked_in = fs::read_to_string(&checked_in_path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", checked_in_path.display())); + + if generated == checked_in { + return; + } + + if std::env::var_os("CI").is_some() { + panic!("{GENERATED_PATH} is out of date; run `cargo test -p foundations-metrics-registry generated_proto_matches_vendored_proto` locally and commit the updated file."); + } + + fs::write(&checked_in_path, generated) + .unwrap_or_else(|e| panic!("failed to update {}: {e}", checked_in_path.display())); + + panic!("{GENERATED_PATH} was regenerated; commit the updated file and rerun this test"); +} + +fn generate_proto(crate_dir: &Path) -> String { + let out_dir = tempfile::tempdir().expect("failed to create temporary output directory"); + let proto_path = crate_dir.join(PROTO_PATH); + let include_path = crate_dir.join("proto"); + + let file_descriptor_set = protox::compile([proto_path.as_path()], [include_path.as_path()]) + .expect("failed to compile protobuf descriptors"); + + prost_build::Config::new() + .out_dir(out_dir.path()) + .compile_fds(file_descriptor_set) + .expect("failed to generate Rust protobuf model"); + + fs::read_to_string(out_dir.path().join(PROST_OUTPUT_FILE)) + .expect("prost-build did not write the expected generated file") +} From db7e080a8586eb8bfa19a12b36a37c31ef915e1f Mon Sep 17 00:00:00 2001 From: Ethan Olchik Date: Thu, 9 Jul 2026 16:28:14 +0100 Subject: [PATCH 3/3] fix(cargo): fix fmt & clippy --- foundations-metrics-registry/tests/proto_codegen.rs | 4 +++- foundations/src/telemetry/server/router.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/foundations-metrics-registry/tests/proto_codegen.rs b/foundations-metrics-registry/tests/proto_codegen.rs index 1a2bbba..c38edff 100644 --- a/foundations-metrics-registry/tests/proto_codegen.rs +++ b/foundations-metrics-registry/tests/proto_codegen.rs @@ -19,7 +19,9 @@ fn generated_proto_matches_vendored_proto() { } if std::env::var_os("CI").is_some() { - panic!("{GENERATED_PATH} is out of date; run `cargo test -p foundations-metrics-registry generated_proto_matches_vendored_proto` locally and commit the updated file."); + panic!( + "{GENERATED_PATH} is out of date; run `cargo test -p foundations-metrics-registry generated_proto_matches_vendored_proto` locally and commit the updated file." + ); } fs::write(&checked_in_path, generated) diff --git a/foundations/src/telemetry/server/router.rs b/foundations/src/telemetry/server/router.rs index 4658888..49dc96d 100644 --- a/foundations/src/telemetry/server/router.rs +++ b/foundations/src/telemetry/server/router.rs @@ -213,7 +213,7 @@ impl Routes { // Exact matches were allowed and ignored historically. Any other errors // should be reported up. Err(matchit::InsertError::Conflict { with }) if with == route.path => {} - Err(e) => anyhow::bail!("tried to insert route `{}`, but {}", &route.path, e), + Err(e) => anyhow::bail!("tried to insert route `{}`, but {}", route.path, e), } }