commit 0fbd0a5084beab1c7a67ad2afcdb813f84676aba
parent 260d429596562d90ef460fbc8c86750e6e2348da
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 05:11:37 +0000
service-host: add bounded metrics contract
- close common metric groups and label vocabularies
- require canonical component and relay identities
- bound iterator ingestion and snapshot rendering
- verify deterministic Prometheus-compatible output
Diffstat:
4 files changed, 1161 insertions(+), 5 deletions(-)
diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs
@@ -50,11 +50,15 @@ pub use lifecycle::{
TaskRegistrationError, TaskSupervisor, UnfinishedWork,
};
pub use operations::{
- LIVEZ_PATH, OPERATIONS_HEALTH_CONTENT_TYPE, OperationsBindPolicy, OperationsConfigError,
- OperationsConfigField, OperationsHealthResponse, OperationsListenAddress,
- OperationsListenAddressError, OperationsListenerConfig, OperationsTransportLimitField,
- OperationsTransportLimitValues, OperationsTransportLimits, OperationsTransportLimitsError,
- READYZ_PATH, livez, readyz,
+ BoundedMetricsSnapshot, CommonMetricGroup, LIVEZ_PATH, METRICS_CONTENT_TYPE,
+ METRICS_MAX_DESCRIPTORS, METRICS_MAX_LABELS_PER_SAMPLE, METRICS_MAX_RENDER_UTF8_BYTES,
+ METRICS_MAX_SAMPLES, MetricComponentId, MetricDescriptor, MetricHealthState, MetricKind,
+ MetricLabel, MetricLabelKey, MetricName, MetricSample, MetricTaskOutcome, MetricValue,
+ MetricsContractError, MetricsRenderError, OPERATIONS_HEALTH_CONTENT_TYPE, OperationsBindPolicy,
+ OperationsConfigError, OperationsConfigField, OperationsHealthResponse,
+ OperationsListenAddress, OperationsListenAddressError, OperationsListenerConfig,
+ OperationsTransportLimitField, OperationsTransportLimitValues, OperationsTransportLimits,
+ OperationsTransportLimitsError, READYZ_PATH, StableRelayId, livez, readyz,
};
pub use status::{
CONFIGURATION_SCHEMA_VERSION, CachedServiceState, CachedServiceStatePublisher,
diff --git a/crates/service_host/src/operations/metrics.rs b/crates/service_host/src/operations/metrics.rs
@@ -0,0 +1,1143 @@
+//! Bounded, deterministic Prometheus-compatible metrics snapshots.
+
+use core::fmt;
+use std::collections::BTreeMap;
+use std::error::Error;
+
+use crate::{BuildInfo, ServicePhase, TaskClassification, TaskName};
+
+pub const METRICS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8";
+pub const METRICS_MAX_DESCRIPTORS: usize = 64;
+pub const METRICS_MAX_SAMPLES: usize = 512;
+pub const METRICS_MAX_LABELS_PER_SAMPLE: usize = 8;
+pub const METRICS_MAX_RENDER_UTF8_BYTES: usize = 1_048_576;
+
+const METRIC_NAME_MAX_BYTES: usize = 128;
+const METRIC_HELP_MAX_BYTES: usize = 512;
+const METRIC_LABEL_VALUE_MAX_BYTES: usize = 128;
+const STABLE_RELAY_ID_MAX_BYTES: usize = 64;
+
+/// One of the five service-neutral metric interfaces owned by the host.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum CommonMetricGroup {
+ Build,
+ Phase,
+ Task,
+ Storage,
+ Transport,
+}
+
+/// Prometheus metric type supported by the bounded snapshot.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricKind {
+ Counter,
+ Gauge,
+}
+
+impl MetricKind {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Counter => "counter",
+ Self::Gauge => "gauge",
+ }
+ }
+}
+
+/// Closed label-key inventory. Services cannot add arbitrary label dimensions.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum MetricLabelKey {
+ Version,
+ Revision,
+ Phase,
+ Task,
+ Classification,
+ Storage,
+ Transport,
+ RelayId,
+ State,
+ Outcome,
+}
+
+/// A bounded canonical name for a storage or transport component.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct MetricComponentId(String);
+
+impl MetricComponentId {
+ pub fn new(value: impl Into<String>) -> Result<Self, MetricsContractError> {
+ let value = value.into();
+ if value.is_empty()
+ || value.len() > METRIC_LABEL_VALUE_MAX_BYTES
+ || !value.bytes().enumerate().all(|(index, byte)| {
+ if index == 0 {
+ byte.is_ascii_lowercase()
+ } else {
+ byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_'
+ }
+ })
+ {
+ return Err(MetricsContractError::InvalidComponentId);
+ }
+ Ok(Self(value))
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+/// Closed storage and transport health vocabulary.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricHealthState {
+ Ready,
+ Degraded,
+ Unready,
+ ReadOnly,
+ RepairRequired,
+ Unavailable,
+}
+
+impl MetricHealthState {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Ready => "ready",
+ Self::Degraded => "degraded",
+ Self::Unready => "unready",
+ Self::ReadOnly => "read_only",
+ Self::RepairRequired => "repair_required",
+ Self::Unavailable => "unavailable",
+ }
+ }
+}
+
+/// Closed supervisor outcome vocabulary for task metrics.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricTaskOutcome {
+ ExpectedCompletion,
+ OptionalFailure,
+ TaskReturnedError,
+ TaskPanicked,
+ UnexpectedCompletion,
+ UnexpectedCancellation,
+ JoinFailed,
+}
+
+impl MetricTaskOutcome {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::ExpectedCompletion => "expected_completion",
+ Self::OptionalFailure => "optional_failure",
+ Self::TaskReturnedError => "task_returned_error",
+ Self::TaskPanicked => "task_panicked",
+ Self::UnexpectedCompletion => "unexpected_completion",
+ Self::UnexpectedCancellation => "unexpected_cancellation",
+ Self::JoinFailed => "join_failed",
+ }
+ }
+}
+
+impl MetricLabelKey {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Version => "version",
+ Self::Revision => "revision",
+ Self::Phase => "phase",
+ Self::Task => "task",
+ Self::Classification => "classification",
+ Self::Storage => "storage",
+ Self::Transport => "transport",
+ Self::RelayId => "relay_id",
+ Self::State => "state",
+ Self::Outcome => "outcome",
+ }
+ }
+
+ const fn allowed_for(self, group: CommonMetricGroup) -> bool {
+ match group {
+ CommonMetricGroup::Build => matches!(self, Self::Version | Self::Revision),
+ CommonMetricGroup::Phase => matches!(self, Self::Phase),
+ CommonMetricGroup::Task => {
+ matches!(self, Self::Task | Self::Classification | Self::Outcome)
+ }
+ CommonMetricGroup::Storage => matches!(self, Self::Storage | Self::State),
+ CommonMetricGroup::Transport => {
+ matches!(self, Self::Transport | Self::RelayId | Self::State)
+ }
+ }
+ }
+}
+
+/// A validated Prometheus metric name.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct MetricName(String);
+
+impl MetricName {
+ pub fn new(value: impl Into<String>) -> Result<Self, MetricsContractError> {
+ let value = value.into();
+ if value.is_empty() || value.len() > METRIC_NAME_MAX_BYTES || !valid_metric_name(&value) {
+ return Err(MetricsContractError::InvalidMetricName);
+ }
+ Ok(Self(value))
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+/// A canonical bounded relay identity suitable for a label value.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct StableRelayId(String);
+
+impl StableRelayId {
+ pub fn new(value: impl Into<String>) -> Result<Self, MetricsContractError> {
+ let value = value.into();
+ if value.is_empty()
+ || value.len() > STABLE_RELAY_ID_MAX_BYTES
+ || !value.bytes().enumerate().all(|(index, byte)| {
+ if index == 0 {
+ byte.is_ascii_lowercase() || byte.is_ascii_digit()
+ } else {
+ byte.is_ascii_lowercase()
+ || byte.is_ascii_digit()
+ || matches!(byte, b'.' | b'_' | b'-')
+ }
+ })
+ {
+ return Err(MetricsContractError::InvalidStableRelayId);
+ }
+ Ok(Self(value))
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
+struct MetricLabelValue(String);
+
+impl MetricLabelValue {
+ fn new(value: impl Into<String>) -> Result<Self, MetricsContractError> {
+ let value = value.into();
+ if value.is_empty()
+ || value.len() > METRIC_LABEL_VALUE_MAX_BYTES
+ || !value.bytes().enumerate().all(|(index, byte)| {
+ if index == 0 {
+ byte.is_ascii_alphanumeric()
+ } else {
+ byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')
+ }
+ })
+ {
+ return Err(MetricsContractError::InvalidLabelValue);
+ }
+ Ok(Self(value))
+ }
+
+ fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+/// One validated label with a closed key and bounded value.
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct MetricLabel {
+ key: MetricLabelKey,
+ value: MetricLabelValue,
+}
+
+impl MetricLabel {
+ pub fn build_version(build: &BuildInfo) -> Result<Self, MetricsContractError> {
+ Self::validated(MetricLabelKey::Version, build.service_version())
+ }
+
+ pub fn build_revision(build: &BuildInfo) -> Result<Self, MetricsContractError> {
+ Self::validated(MetricLabelKey::Revision, build.service_commit())
+ }
+
+ #[must_use]
+ pub fn phase(phase: ServicePhase) -> Self {
+ Self::fixed(
+ MetricLabelKey::Phase,
+ match phase {
+ ServicePhase::Starting => "starting",
+ ServicePhase::Ready => "ready",
+ ServicePhase::Degraded => "degraded",
+ ServicePhase::Unready => "unready",
+ ServicePhase::Stopping => "stopping",
+ ServicePhase::Failed => "failed",
+ },
+ )
+ }
+
+ pub fn task(task: &TaskName) -> Result<Self, MetricsContractError> {
+ Self::validated(MetricLabelKey::Task, task.as_str())
+ }
+
+ #[must_use]
+ pub fn classification(classification: TaskClassification) -> Self {
+ Self::fixed(
+ MetricLabelKey::Classification,
+ match classification {
+ TaskClassification::Critical => "critical",
+ TaskClassification::Optional => "optional",
+ TaskClassification::OneShot => "one_shot",
+ },
+ )
+ }
+
+ #[must_use]
+ pub fn storage(storage: MetricComponentId) -> Self {
+ Self::fixed(MetricLabelKey::Storage, storage.0)
+ }
+
+ #[must_use]
+ pub fn transport(transport: MetricComponentId) -> Self {
+ Self::fixed(MetricLabelKey::Transport, transport.0)
+ }
+
+ #[must_use]
+ pub fn relay_id(value: StableRelayId) -> Self {
+ Self::fixed(MetricLabelKey::RelayId, value.0)
+ }
+
+ #[must_use]
+ pub fn health_state(state: MetricHealthState) -> Self {
+ Self::fixed(MetricLabelKey::State, state.as_str())
+ }
+
+ #[must_use]
+ pub fn task_outcome(outcome: MetricTaskOutcome) -> Self {
+ Self::fixed(MetricLabelKey::Outcome, outcome.as_str())
+ }
+
+ #[must_use]
+ pub const fn key(&self) -> MetricLabelKey {
+ self.key
+ }
+
+ #[must_use]
+ pub fn value(&self) -> &str {
+ self.value.as_str()
+ }
+
+ fn validated(key: MetricLabelKey, value: &str) -> Result<Self, MetricsContractError> {
+ Ok(Self {
+ key,
+ value: MetricLabelValue::new(value)?,
+ })
+ }
+
+ fn fixed(key: MetricLabelKey, value: impl Into<String>) -> Self {
+ Self {
+ key,
+ value: MetricLabelValue(value.into()),
+ }
+ }
+}
+
+impl fmt::Debug for MetricLabel {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MetricLabel")
+ .field("key", &self.key)
+ .field("value", &"[redacted]")
+ .finish()
+ }
+}
+
+/// One validated metric family descriptor.
+#[derive(Clone, PartialEq, Eq)]
+pub struct MetricDescriptor {
+ group: CommonMetricGroup,
+ name: MetricName,
+ help: String,
+ kind: MetricKind,
+ label_keys: Vec<MetricLabelKey>,
+}
+
+impl MetricDescriptor {
+ pub fn new(
+ group: CommonMetricGroup,
+ name: MetricName,
+ help: impl Into<String>,
+ kind: MetricKind,
+ label_keys: impl IntoIterator<Item = MetricLabelKey>,
+ ) -> Result<Self, MetricsContractError> {
+ let help = help.into();
+ if help.is_empty()
+ || help.len() > METRIC_HELP_MAX_BYTES
+ || help
+ .chars()
+ .any(|character| character.is_control() && character != '\n')
+ {
+ return Err(MetricsContractError::InvalidHelp);
+ }
+ let mut label_keys: Vec<_> = label_keys
+ .into_iter()
+ .take(METRICS_MAX_LABELS_PER_SAMPLE + 1)
+ .collect();
+ if label_keys.len() > METRICS_MAX_LABELS_PER_SAMPLE {
+ return Err(MetricsContractError::TooManyLabels);
+ }
+ label_keys.sort_unstable();
+ if label_keys.windows(2).any(|pair| pair[0] == pair[1]) {
+ return Err(MetricsContractError::DuplicateLabelKey);
+ }
+ if label_keys.iter().any(|key| !key.allowed_for(group)) {
+ return Err(MetricsContractError::ForbiddenLabelKey);
+ }
+ Ok(Self {
+ group,
+ name,
+ help,
+ kind,
+ label_keys,
+ })
+ }
+
+ #[must_use]
+ pub const fn group(&self) -> CommonMetricGroup {
+ self.group
+ }
+
+ #[must_use]
+ pub fn name(&self) -> &MetricName {
+ &self.name
+ }
+
+ #[must_use]
+ pub const fn kind(&self) -> MetricKind {
+ self.kind
+ }
+
+ #[must_use]
+ pub fn label_keys(&self) -> &[MetricLabelKey] {
+ &self.label_keys
+ }
+}
+
+impl fmt::Debug for MetricDescriptor {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MetricDescriptor")
+ .field("group", &self.group)
+ .field("name", &self.name)
+ .field("help", &"[redacted]")
+ .field("kind", &self.kind)
+ .field("label_keys", &self.label_keys)
+ .finish()
+ }
+}
+
+/// Exact integer sample value; floating-point ambiguity is intentionally absent.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricValue {
+ Counter(u64),
+ Gauge(i64),
+}
+
+impl MetricValue {
+ const fn kind(self) -> MetricKind {
+ match self {
+ Self::Counter(_) => MetricKind::Counter,
+ Self::Gauge(_) => MetricKind::Gauge,
+ }
+ }
+}
+
+/// One bounded sample associated with a descriptor by exact name.
+#[derive(Clone, PartialEq, Eq)]
+pub struct MetricSample {
+ name: MetricName,
+ value: MetricValue,
+ labels: Vec<MetricLabel>,
+}
+
+impl MetricSample {
+ pub fn new(
+ name: MetricName,
+ value: MetricValue,
+ labels: impl IntoIterator<Item = MetricLabel>,
+ ) -> Result<Self, MetricsContractError> {
+ let mut labels: Vec<_> = labels
+ .into_iter()
+ .take(METRICS_MAX_LABELS_PER_SAMPLE + 1)
+ .collect();
+ if labels.len() > METRICS_MAX_LABELS_PER_SAMPLE {
+ return Err(MetricsContractError::TooManyLabels);
+ }
+ labels.sort_unstable();
+ if labels.windows(2).any(|pair| pair[0].key == pair[1].key) {
+ return Err(MetricsContractError::DuplicateLabelKey);
+ }
+ Ok(Self {
+ name,
+ value,
+ labels,
+ })
+ }
+
+ #[must_use]
+ pub fn name(&self) -> &MetricName {
+ &self.name
+ }
+
+ #[must_use]
+ pub const fn value(&self) -> MetricValue {
+ self.value
+ }
+
+ #[must_use]
+ pub fn labels(&self) -> &[MetricLabel] {
+ &self.labels
+ }
+}
+
+impl fmt::Debug for MetricSample {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MetricSample")
+ .field("name", &self.name)
+ .field("value", &self.value)
+ .field("label_count", &self.labels.len())
+ .finish()
+ }
+}
+
+/// An immutable validated metrics snapshot with deterministic ordering.
+#[derive(Clone, PartialEq, Eq)]
+pub struct BoundedMetricsSnapshot {
+ descriptors: Vec<MetricDescriptor>,
+ samples: Vec<MetricSample>,
+}
+
+impl BoundedMetricsSnapshot {
+ pub fn new(
+ descriptors: impl IntoIterator<Item = MetricDescriptor>,
+ samples: impl IntoIterator<Item = MetricSample>,
+ ) -> Result<Self, MetricsContractError> {
+ let mut descriptors: Vec<_> = descriptors
+ .into_iter()
+ .take(METRICS_MAX_DESCRIPTORS + 1)
+ .collect();
+ let mut samples: Vec<_> = samples.into_iter().take(METRICS_MAX_SAMPLES + 1).collect();
+ if descriptors.len() > METRICS_MAX_DESCRIPTORS {
+ return Err(MetricsContractError::TooManyDescriptors);
+ }
+ if samples.len() > METRICS_MAX_SAMPLES {
+ return Err(MetricsContractError::TooManySamples);
+ }
+ descriptors.sort_by(|left, right| left.name.cmp(&right.name));
+ if descriptors
+ .windows(2)
+ .any(|pair| pair[0].name == pair[1].name)
+ {
+ return Err(MetricsContractError::DuplicateDescriptor);
+ }
+ let descriptor_by_name: BTreeMap<_, _> = descriptors
+ .iter()
+ .map(|descriptor| (&descriptor.name, descriptor))
+ .collect();
+ for sample in &samples {
+ let descriptor = descriptor_by_name
+ .get(&sample.name)
+ .ok_or(MetricsContractError::UnknownDescriptor)?;
+ if sample.value.kind() != descriptor.kind {
+ return Err(MetricsContractError::ValueKindMismatch);
+ }
+ let actual_keys: Vec<_> = sample.labels.iter().map(|label| label.key).collect();
+ if actual_keys != descriptor.label_keys {
+ return Err(MetricsContractError::LabelSetMismatch);
+ }
+ }
+ samples.sort_by(|left, right| {
+ left.name
+ .cmp(&right.name)
+ .then_with(|| left.labels.cmp(&right.labels))
+ });
+ if samples
+ .windows(2)
+ .any(|pair| pair[0].name == pair[1].name && pair[0].labels == pair[1].labels)
+ {
+ return Err(MetricsContractError::DuplicateSample);
+ }
+ Ok(Self {
+ descriptors,
+ samples,
+ })
+ }
+
+ #[must_use]
+ pub fn descriptors(&self) -> &[MetricDescriptor] {
+ &self.descriptors
+ }
+
+ #[must_use]
+ pub fn samples(&self) -> &[MetricSample] {
+ &self.samples
+ }
+
+ /// Renders deterministic Prometheus text without exceeding `maximum_bytes`.
+ pub fn render(&self, maximum_bytes: usize) -> Result<Vec<u8>, MetricsRenderError> {
+ if maximum_bytes == 0 || maximum_bytes > METRICS_MAX_RENDER_UTF8_BYTES {
+ return Err(MetricsRenderError::InvalidMaximum);
+ }
+ let mut output = BoundedOutput::new(maximum_bytes);
+ for descriptor in &self.descriptors {
+ output.push_str("# HELP ")?;
+ output.push_str(descriptor.name.as_str())?;
+ output.push_byte(b' ')?;
+ output.push_escaped_help(&descriptor.help)?;
+ output.push_byte(b'\n')?;
+ output.push_str("# TYPE ")?;
+ output.push_str(descriptor.name.as_str())?;
+ output.push_byte(b' ')?;
+ output.push_str(descriptor.kind.as_str())?;
+ output.push_byte(b'\n')?;
+
+ for sample in self
+ .samples
+ .iter()
+ .filter(|sample| sample.name == descriptor.name)
+ {
+ output.push_str(sample.name.as_str())?;
+ if !sample.labels.is_empty() {
+ output.push_byte(b'{')?;
+ for (index, label) in sample.labels.iter().enumerate() {
+ if index != 0 {
+ output.push_byte(b',')?;
+ }
+ output.push_str(label.key.as_str())?;
+ output.push_str("=\"")?;
+ output.push_escaped_label_value(label.value.as_str())?;
+ output.push_byte(b'\"')?;
+ }
+ output.push_byte(b'}')?;
+ }
+ output.push_byte(b' ')?;
+ match sample.value {
+ MetricValue::Counter(value) => output.push_str(&value.to_string())?,
+ MetricValue::Gauge(value) => output.push_str(&value.to_string())?,
+ }
+ output.push_byte(b'\n')?;
+ }
+ }
+ Ok(output.finish())
+ }
+}
+
+impl fmt::Debug for BoundedMetricsSnapshot {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("BoundedMetricsSnapshot")
+ .field("descriptor_count", &self.descriptors.len())
+ .field("sample_count", &self.samples.len())
+ .finish()
+ }
+}
+
+/// Safe construction failure for the metrics contract.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricsContractError {
+ InvalidMetricName,
+ InvalidHelp,
+ InvalidLabelValue,
+ InvalidStableRelayId,
+ InvalidComponentId,
+ ForbiddenLabelKey,
+ DuplicateLabelKey,
+ TooManyLabels,
+ TooManyDescriptors,
+ TooManySamples,
+ DuplicateDescriptor,
+ UnknownDescriptor,
+ ValueKindMismatch,
+ LabelSetMismatch,
+ DuplicateSample,
+}
+
+impl fmt::Display for MetricsContractError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("metrics snapshot violates the bounded host contract")
+ }
+}
+
+impl Error for MetricsContractError {}
+
+/// Safe deterministic-rendering failure.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MetricsRenderError {
+ InvalidMaximum,
+ ResponseTooLarge,
+}
+
+impl fmt::Display for MetricsRenderError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("metrics snapshot cannot be rendered within the response limit")
+ }
+}
+
+impl Error for MetricsRenderError {}
+
+struct BoundedOutput {
+ bytes: Vec<u8>,
+ maximum: usize,
+}
+
+impl BoundedOutput {
+ fn new(maximum: usize) -> Self {
+ Self {
+ bytes: Vec::with_capacity(maximum.min(4096)),
+ maximum,
+ }
+ }
+
+ fn push_byte(&mut self, byte: u8) -> Result<(), MetricsRenderError> {
+ if self.bytes.len() == self.maximum {
+ return Err(MetricsRenderError::ResponseTooLarge);
+ }
+ self.bytes.push(byte);
+ Ok(())
+ }
+
+ fn push_str(&mut self, value: &str) -> Result<(), MetricsRenderError> {
+ let remaining = self.maximum.saturating_sub(self.bytes.len());
+ if value.len() > remaining {
+ return Err(MetricsRenderError::ResponseTooLarge);
+ }
+ self.bytes.extend_from_slice(value.as_bytes());
+ Ok(())
+ }
+
+ fn push_escaped_help(&mut self, value: &str) -> Result<(), MetricsRenderError> {
+ for character in value.chars() {
+ match character {
+ '\\' => self.push_str("\\\\")?,
+ '\n' => self.push_str("\\n")?,
+ _ => {
+ let mut bytes = [0; 4];
+ self.push_str(character.encode_utf8(&mut bytes))?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn push_escaped_label_value(&mut self, value: &str) -> Result<(), MetricsRenderError> {
+ for character in value.chars() {
+ match character {
+ '\\' => self.push_str("\\\\")?,
+ '\"' => self.push_str("\\\"")?,
+ '\n' => self.push_str("\\n")?,
+ _ => {
+ let mut bytes = [0; 4];
+ self.push_str(character.encode_utf8(&mut bytes))?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn finish(self) -> Vec<u8> {
+ self.bytes
+ }
+}
+
+fn valid_metric_name(value: &str) -> bool {
+ value.bytes().enumerate().all(|(index, byte)| {
+ if index == 0 {
+ byte.is_ascii_alphabetic() || matches!(byte, b'_' | b':')
+ } else {
+ byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':')
+ }
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use std::collections::BTreeSet;
+
+ use super::*;
+
+ fn name(value: &str) -> MetricName {
+ MetricName::new(value).unwrap()
+ }
+
+ fn descriptor(
+ group: CommonMetricGroup,
+ metric_name: &str,
+ kind: MetricKind,
+ labels: &[MetricLabelKey],
+ ) -> MetricDescriptor {
+ MetricDescriptor::new(
+ group,
+ name(metric_name),
+ "safe help",
+ kind,
+ labels.iter().copied(),
+ )
+ .unwrap()
+ }
+
+ #[test]
+ fn arbitrary_and_cross_group_labels_are_rejected() {
+ assert_eq!(
+ MetricDescriptor::new(
+ CommonMetricGroup::Build,
+ name("radroots_build_info"),
+ "build information",
+ MetricKind::Gauge,
+ [MetricLabelKey::RelayId],
+ ),
+ Err(MetricsContractError::ForbiddenLabelKey)
+ );
+ assert_eq!(
+ MetricDescriptor::new(
+ CommonMetricGroup::Transport,
+ name("radroots_transport_state"),
+ "transport state",
+ MetricKind::Gauge,
+ [MetricLabelKey::RelayId, MetricLabelKey::RelayId],
+ ),
+ Err(MetricsContractError::DuplicateLabelKey)
+ );
+ assert_eq!(
+ MetricName::new("bad metric"),
+ Err(MetricsContractError::InvalidMetricName)
+ );
+ }
+
+ #[test]
+ fn relay_labels_require_stable_ids_and_reject_raw_urls() {
+ let stable = StableRelayId::new("relay-west-01").unwrap();
+ let label = MetricLabel::relay_id(stable.clone());
+ assert_eq!(label.value(), stable.as_str());
+ for forbidden in [
+ "wss://relay.example.com",
+ "Relay-West",
+ "relay west",
+ "-relay",
+ "",
+ ] {
+ assert_eq!(
+ StableRelayId::new(forbidden),
+ Err(MetricsContractError::InvalidStableRelayId)
+ );
+ }
+ }
+
+ #[test]
+ fn rendering_is_sorted_and_escapes_help() {
+ let descriptor = MetricDescriptor::new(
+ CommonMetricGroup::Task,
+ name("radroots_task_outcomes_total"),
+ "task\\outcomes\nby state",
+ MetricKind::Counter,
+ [MetricLabelKey::Task, MetricLabelKey::Outcome],
+ )
+ .unwrap();
+ let sample = MetricSample::new(
+ name("radroots_task_outcomes_total"),
+ MetricValue::Counter(7),
+ [
+ MetricLabel::task_outcome(MetricTaskOutcome::OptionalFailure),
+ MetricLabel::task(&TaskName::new("worker_one").unwrap()).unwrap(),
+ ],
+ )
+ .unwrap();
+ let snapshot = BoundedMetricsSnapshot::new([descriptor], [sample]).unwrap();
+ assert_eq!(
+ String::from_utf8(snapshot.render(1024).unwrap()).unwrap(),
+ concat!(
+ "# HELP radroots_task_outcomes_total task\\\\outcomes\\nby state\n",
+ "# TYPE radroots_task_outcomes_total counter\n",
+ "radroots_task_outcomes_total{task=\"worker_one\",outcome=\"optional_failure\"} 7\n",
+ )
+ );
+
+ let mut defensive = BoundedOutput::new(64);
+ defensive
+ .push_escaped_label_value("worker\\\"one\n")
+ .unwrap();
+ assert_eq!(defensive.finish(), b"worker\\\\\\\"one\\n");
+ }
+
+ #[test]
+ fn component_ids_reject_free_text_urls_paths_and_control_characters() {
+ for forbidden in [
+ "raw error text",
+ "wss://relay.example.com",
+ "/private/path",
+ "line\nfeed",
+ "quote\"value",
+ "back\\slash",
+ "",
+ ] {
+ assert_eq!(
+ MetricComponentId::new(forbidden),
+ Err(MetricsContractError::InvalidComponentId)
+ );
+ }
+ assert!(MetricComponentId::new("sqlite_writer").is_ok());
+ }
+
+ #[test]
+ fn duplicates_unknowns_kind_and_label_mismatches_fail_closed() {
+ let phase = descriptor(
+ CommonMetricGroup::Phase,
+ "radroots_phase",
+ MetricKind::Gauge,
+ &[MetricLabelKey::Phase],
+ );
+ assert_eq!(
+ BoundedMetricsSnapshot::new([phase.clone(), phase.clone()], []),
+ Err(MetricsContractError::DuplicateDescriptor)
+ );
+ let unknown =
+ MetricSample::new(name("radroots_unknown"), MetricValue::Gauge(1), []).unwrap();
+ assert_eq!(
+ BoundedMetricsSnapshot::new([phase.clone()], [unknown]),
+ Err(MetricsContractError::UnknownDescriptor)
+ );
+ let wrong_kind = MetricSample::new(
+ name("radroots_phase"),
+ MetricValue::Counter(1),
+ [MetricLabel::phase(ServicePhase::Ready)],
+ )
+ .unwrap();
+ assert_eq!(
+ BoundedMetricsSnapshot::new([phase.clone()], [wrong_kind]),
+ Err(MetricsContractError::ValueKindMismatch)
+ );
+ let wrong_labels =
+ MetricSample::new(name("radroots_phase"), MetricValue::Gauge(1), []).unwrap();
+ assert_eq!(
+ BoundedMetricsSnapshot::new([phase], [wrong_labels]),
+ Err(MetricsContractError::LabelSetMismatch)
+ );
+ }
+
+ #[test]
+ fn render_and_collection_bounds_fail_before_overgrowth() {
+ let descriptor = descriptor(
+ CommonMetricGroup::Phase,
+ "radroots_phase",
+ MetricKind::Gauge,
+ &[MetricLabelKey::Phase],
+ );
+ let sample = MetricSample::new(
+ name("radroots_phase"),
+ MetricValue::Gauge(1),
+ [MetricLabel::phase(ServicePhase::Ready)],
+ )
+ .unwrap();
+ let snapshot = BoundedMetricsSnapshot::new([descriptor.clone()], [sample]).unwrap();
+ let exact = snapshot.render(METRICS_MAX_RENDER_UTF8_BYTES).unwrap();
+ assert_eq!(snapshot.render(exact.len()).unwrap(), exact);
+ assert_eq!(
+ snapshot.render(exact.len() - 1),
+ Err(MetricsRenderError::ResponseTooLarge)
+ );
+ assert_eq!(snapshot.render(0), Err(MetricsRenderError::InvalidMaximum));
+ assert_eq!(
+ snapshot.render(METRICS_MAX_RENDER_UTF8_BYTES + 1),
+ Err(MetricsRenderError::InvalidMaximum)
+ );
+
+ assert_eq!(
+ BoundedMetricsSnapshot::new(
+ std::iter::repeat_n(descriptor, METRICS_MAX_DESCRIPTORS + 1),
+ [],
+ ),
+ Err(MetricsContractError::TooManyDescriptors)
+ );
+ }
+
+ #[test]
+ fn every_common_group_and_label_key_has_a_stable_inventory() {
+ assert_eq!(METRICS_MAX_DESCRIPTORS, 64);
+ assert_eq!(METRICS_MAX_SAMPLES, 512);
+ assert_eq!(METRICS_MAX_LABELS_PER_SAMPLE, 8);
+ assert_eq!(METRICS_MAX_RENDER_UTF8_BYTES, 1_048_576);
+ assert_eq!(METRIC_NAME_MAX_BYTES, 128);
+ assert_eq!(METRIC_HELP_MAX_BYTES, 512);
+ assert_eq!(METRIC_LABEL_VALUE_MAX_BYTES, 128);
+ assert_eq!(STABLE_RELAY_ID_MAX_BYTES, 64);
+
+ let inventory = [
+ (
+ CommonMetricGroup::Build,
+ &[MetricLabelKey::Version, MetricLabelKey::Revision][..],
+ ),
+ (CommonMetricGroup::Phase, &[MetricLabelKey::Phase][..]),
+ (
+ CommonMetricGroup::Task,
+ &[
+ MetricLabelKey::Task,
+ MetricLabelKey::Classification,
+ MetricLabelKey::Outcome,
+ ][..],
+ ),
+ (
+ CommonMetricGroup::Storage,
+ &[MetricLabelKey::Storage, MetricLabelKey::State][..],
+ ),
+ (
+ CommonMetricGroup::Transport,
+ &[
+ MetricLabelKey::Transport,
+ MetricLabelKey::RelayId,
+ MetricLabelKey::State,
+ ][..],
+ ),
+ ];
+ let all_keys: BTreeSet<_> = [
+ MetricLabelKey::Version,
+ MetricLabelKey::Revision,
+ MetricLabelKey::Phase,
+ MetricLabelKey::Task,
+ MetricLabelKey::Classification,
+ MetricLabelKey::Storage,
+ MetricLabelKey::Transport,
+ MetricLabelKey::RelayId,
+ MetricLabelKey::State,
+ MetricLabelKey::Outcome,
+ ]
+ .into_iter()
+ .collect();
+ assert_eq!(
+ inventory
+ .iter()
+ .flat_map(|(_, keys)| keys.iter().copied())
+ .collect::<BTreeSet<_>>(),
+ all_keys
+ );
+ for (group, allowed) in inventory {
+ for key in all_keys.iter().copied() {
+ assert_eq!(key.allowed_for(group), allowed.contains(&key));
+ }
+ }
+ }
+
+ #[test]
+ fn typed_label_vocabularies_are_exact_and_non_bypassable() {
+ let phases = [
+ ServicePhase::Starting,
+ ServicePhase::Ready,
+ ServicePhase::Degraded,
+ ServicePhase::Unready,
+ ServicePhase::Stopping,
+ ServicePhase::Failed,
+ ]
+ .map(MetricLabel::phase);
+ assert_eq!(
+ phases.map(|label| label.value().to_owned()),
+ [
+ "starting", "ready", "degraded", "unready", "stopping", "failed"
+ ]
+ .map(str::to_owned)
+ );
+ assert_eq!(
+ [
+ TaskClassification::Critical,
+ TaskClassification::Optional,
+ TaskClassification::OneShot,
+ ]
+ .map(MetricLabel::classification)
+ .map(|label| label.value().to_owned()),
+ ["critical", "optional", "one_shot"].map(str::to_owned)
+ );
+ assert_eq!(
+ [
+ MetricHealthState::Ready,
+ MetricHealthState::Degraded,
+ MetricHealthState::Unready,
+ MetricHealthState::ReadOnly,
+ MetricHealthState::RepairRequired,
+ MetricHealthState::Unavailable,
+ ]
+ .map(MetricLabel::health_state)
+ .map(|label| label.value().to_owned()),
+ [
+ "ready",
+ "degraded",
+ "unready",
+ "read_only",
+ "repair_required",
+ "unavailable",
+ ]
+ .map(str::to_owned)
+ );
+ assert_eq!(
+ [
+ MetricTaskOutcome::ExpectedCompletion,
+ MetricTaskOutcome::OptionalFailure,
+ MetricTaskOutcome::TaskReturnedError,
+ MetricTaskOutcome::TaskPanicked,
+ MetricTaskOutcome::UnexpectedCompletion,
+ MetricTaskOutcome::UnexpectedCancellation,
+ MetricTaskOutcome::JoinFailed,
+ ]
+ .map(MetricLabel::task_outcome)
+ .map(|label| label.value().to_owned()),
+ [
+ "expected_completion",
+ "optional_failure",
+ "task_returned_error",
+ "task_panicked",
+ "unexpected_completion",
+ "unexpected_cancellation",
+ "join_failed",
+ ]
+ .map(str::to_owned)
+ );
+ }
+
+ #[test]
+ fn infinite_iterators_are_bounded_during_ingestion() {
+ assert_eq!(
+ MetricDescriptor::new(
+ CommonMetricGroup::Task,
+ name("radroots_task"),
+ "task",
+ MetricKind::Gauge,
+ std::iter::repeat(MetricLabelKey::Task),
+ ),
+ Err(MetricsContractError::TooManyLabels)
+ );
+ assert_eq!(
+ MetricSample::new(
+ name("radroots_phase"),
+ MetricValue::Gauge(1),
+ std::iter::repeat(MetricLabel::phase(ServicePhase::Ready)),
+ ),
+ Err(MetricsContractError::TooManyLabels)
+ );
+
+ let descriptor = descriptor(
+ CommonMetricGroup::Phase,
+ "radroots_phase",
+ MetricKind::Gauge,
+ &[MetricLabelKey::Phase],
+ );
+ let sample = MetricSample::new(
+ name("radroots_phase"),
+ MetricValue::Gauge(1),
+ [MetricLabel::phase(ServicePhase::Ready)],
+ )
+ .unwrap();
+ assert_eq!(
+ BoundedMetricsSnapshot::new(std::iter::repeat(descriptor.clone()), []),
+ Err(MetricsContractError::TooManyDescriptors)
+ );
+ assert_eq!(
+ BoundedMetricsSnapshot::new([descriptor], std::iter::repeat(sample)),
+ Err(MetricsContractError::TooManySamples)
+ );
+ }
+}
diff --git a/crates/service_host/src/operations/mod.rs b/crates/service_host/src/operations/mod.rs
@@ -2,6 +2,7 @@
mod config;
mod health;
+mod metrics;
pub use config::{
OperationsBindPolicy, OperationsConfigError, OperationsConfigField, OperationsListenAddress,
@@ -12,3 +13,10 @@ pub use health::{
LIVEZ_PATH, OPERATIONS_HEALTH_CONTENT_TYPE, OperationsHealthResponse, READYZ_PATH, livez,
readyz,
};
+pub use metrics::{
+ BoundedMetricsSnapshot, CommonMetricGroup, METRICS_CONTENT_TYPE, METRICS_MAX_DESCRIPTORS,
+ METRICS_MAX_LABELS_PER_SAMPLE, METRICS_MAX_RENDER_UTF8_BYTES, METRICS_MAX_SAMPLES,
+ MetricComponentId, MetricDescriptor, MetricHealthState, MetricKind, MetricLabel,
+ MetricLabelKey, MetricName, MetricSample, MetricTaskOutcome, MetricValue, MetricsContractError,
+ MetricsRenderError, StableRelayId,
+};
diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs
@@ -29,6 +29,7 @@ const OPERATIONS_SOURCE: &str = concat!(
include_str!("../src/operations/mod.rs"),
include_str!("../src/operations/config.rs"),
include_str!("../src/operations/health.rs"),
+ include_str!("../src/operations/metrics.rs"),
);
#[test]