metrics.rs (44835B)
1 //! Bounded, deterministic Prometheus-compatible metrics snapshots. 2 3 use core::fmt; 4 use std::collections::BTreeMap; 5 use std::error::Error; 6 7 use crate::{BuildInfo, ServicePhase, TaskClassification, TaskName}; 8 9 pub const METRICS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8"; 10 pub const METRICS_MAX_DESCRIPTORS: usize = 64; 11 pub const METRICS_MAX_SAMPLES: usize = 512; 12 pub const METRICS_MAX_LABELS_PER_SAMPLE: usize = 8; 13 pub const METRICS_MAX_RENDER_UTF8_BYTES: usize = 1_048_576; 14 15 const METRIC_NAME_MAX_BYTES: usize = 128; 16 const METRIC_HELP_MAX_BYTES: usize = 512; 17 const METRIC_LABEL_VALUE_MAX_BYTES: usize = 128; 18 const STABLE_RELAY_ID_MAX_BYTES: usize = 64; 19 20 /// One of the five service-neutral metric interfaces owned by the host. 21 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 22 pub enum CommonMetricGroup { 23 Build, 24 Phase, 25 Task, 26 Storage, 27 Transport, 28 } 29 30 /// Prometheus metric type supported by the bounded snapshot. 31 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 32 pub enum MetricKind { 33 Counter, 34 Gauge, 35 } 36 37 impl MetricKind { 38 const fn as_str(self) -> &'static str { 39 match self { 40 Self::Counter => "counter", 41 Self::Gauge => "gauge", 42 } 43 } 44 } 45 46 /// Closed label-key inventory. Services cannot add arbitrary label dimensions. 47 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 48 pub enum MetricLabelKey { 49 Version, 50 Revision, 51 Phase, 52 Task, 53 Classification, 54 Storage, 55 Transport, 56 RelayId, 57 State, 58 Outcome, 59 } 60 61 /// A bounded canonical name for a storage or transport component. 62 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 63 pub struct MetricComponentId(String); 64 65 impl MetricComponentId { 66 pub fn new(value: impl AsRef<str>) -> Result<Self, MetricsContractError> { 67 let value = value.as_ref(); 68 if value.is_empty() 69 || value.len() > METRIC_LABEL_VALUE_MAX_BYTES 70 || !value.bytes().enumerate().all(|(index, byte)| { 71 if index == 0 { 72 byte.is_ascii_lowercase() 73 } else { 74 byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' 75 } 76 }) 77 { 78 return Err(MetricsContractError::InvalidComponentId); 79 } 80 Ok(Self(value.to_owned())) 81 } 82 83 #[must_use] 84 pub fn as_str(&self) -> &str { 85 &self.0 86 } 87 } 88 89 /// Closed storage and transport health vocabulary. 90 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 91 pub enum MetricHealthState { 92 Ready, 93 Degraded, 94 Unready, 95 ReadOnly, 96 RepairRequired, 97 Unavailable, 98 } 99 100 impl MetricHealthState { 101 const fn as_str(self) -> &'static str { 102 match self { 103 Self::Ready => "ready", 104 Self::Degraded => "degraded", 105 Self::Unready => "unready", 106 Self::ReadOnly => "read_only", 107 Self::RepairRequired => "repair_required", 108 Self::Unavailable => "unavailable", 109 } 110 } 111 } 112 113 /// Closed supervisor outcome vocabulary for task metrics. 114 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 115 pub enum MetricTaskOutcome { 116 ExpectedCompletion, 117 OptionalFailure, 118 TaskReturnedError, 119 TaskPanicked, 120 UnexpectedCompletion, 121 UnexpectedCancellation, 122 JoinFailed, 123 } 124 125 impl MetricTaskOutcome { 126 const fn as_str(self) -> &'static str { 127 match self { 128 Self::ExpectedCompletion => "expected_completion", 129 Self::OptionalFailure => "optional_failure", 130 Self::TaskReturnedError => "task_returned_error", 131 Self::TaskPanicked => "task_panicked", 132 Self::UnexpectedCompletion => "unexpected_completion", 133 Self::UnexpectedCancellation => "unexpected_cancellation", 134 Self::JoinFailed => "join_failed", 135 } 136 } 137 } 138 139 impl MetricLabelKey { 140 #[must_use] 141 pub const fn as_str(self) -> &'static str { 142 match self { 143 Self::Version => "version", 144 Self::Revision => "revision", 145 Self::Phase => "phase", 146 Self::Task => "task", 147 Self::Classification => "classification", 148 Self::Storage => "storage", 149 Self::Transport => "transport", 150 Self::RelayId => "relay_id", 151 Self::State => "state", 152 Self::Outcome => "outcome", 153 } 154 } 155 156 const fn allowed_for(self, group: CommonMetricGroup) -> bool { 157 match group { 158 CommonMetricGroup::Build => matches!(self, Self::Version | Self::Revision), 159 CommonMetricGroup::Phase => matches!(self, Self::Phase), 160 CommonMetricGroup::Task => { 161 matches!(self, Self::Task | Self::Classification | Self::Outcome) 162 } 163 CommonMetricGroup::Storage => matches!(self, Self::Storage | Self::State), 164 CommonMetricGroup::Transport => { 165 matches!(self, Self::Transport | Self::RelayId | Self::State) 166 } 167 } 168 } 169 } 170 171 /// A validated Prometheus metric name. 172 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 173 pub struct MetricName(String); 174 175 impl MetricName { 176 pub fn new(value: impl AsRef<str>) -> Result<Self, MetricsContractError> { 177 let value = value.as_ref(); 178 if value.is_empty() || value.len() > METRIC_NAME_MAX_BYTES || !valid_metric_name(value) { 179 return Err(MetricsContractError::InvalidMetricName); 180 } 181 Ok(Self(value.to_owned())) 182 } 183 184 #[must_use] 185 pub fn as_str(&self) -> &str { 186 &self.0 187 } 188 } 189 190 /// A canonical bounded relay identity suitable for a label value. 191 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 192 pub struct StableRelayId(String); 193 194 impl StableRelayId { 195 pub fn new(value: impl AsRef<str>) -> Result<Self, MetricsContractError> { 196 let value = value.as_ref(); 197 if value.is_empty() 198 || value.len() > STABLE_RELAY_ID_MAX_BYTES 199 || !value.bytes().enumerate().all(|(index, byte)| { 200 if index == 0 { 201 byte.is_ascii_lowercase() || byte.is_ascii_digit() 202 } else { 203 byte.is_ascii_lowercase() 204 || byte.is_ascii_digit() 205 || matches!(byte, b'.' | b'_' | b'-') 206 } 207 }) 208 { 209 return Err(MetricsContractError::InvalidStableRelayId); 210 } 211 Ok(Self(value.to_owned())) 212 } 213 214 #[must_use] 215 pub fn as_str(&self) -> &str { 216 &self.0 217 } 218 } 219 220 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] 221 struct MetricLabelValue(String); 222 223 impl MetricLabelValue { 224 fn new(value: impl AsRef<str>) -> Result<Self, MetricsContractError> { 225 let value = value.as_ref(); 226 if value.is_empty() 227 || value.len() > METRIC_LABEL_VALUE_MAX_BYTES 228 || !value.bytes().enumerate().all(|(index, byte)| { 229 if index == 0 { 230 byte.is_ascii_alphanumeric() 231 } else { 232 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-') 233 } 234 }) 235 { 236 return Err(MetricsContractError::InvalidLabelValue); 237 } 238 Ok(Self(value.to_owned())) 239 } 240 241 fn as_str(&self) -> &str { 242 &self.0 243 } 244 } 245 246 /// One validated label with a closed key and bounded value. 247 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] 248 pub struct MetricLabel { 249 key: MetricLabelKey, 250 value: MetricLabelValue, 251 } 252 253 impl MetricLabel { 254 pub fn build_version(build: &BuildInfo) -> Result<Self, MetricsContractError> { 255 Self::validated(MetricLabelKey::Version, build.service_version()) 256 } 257 258 pub fn build_revision(build: &BuildInfo) -> Result<Self, MetricsContractError> { 259 Self::validated(MetricLabelKey::Revision, build.service_commit()) 260 } 261 262 #[must_use] 263 pub fn phase(phase: ServicePhase) -> Self { 264 Self::fixed( 265 MetricLabelKey::Phase, 266 match phase { 267 ServicePhase::Starting => "starting", 268 ServicePhase::Ready => "ready", 269 ServicePhase::Degraded => "degraded", 270 ServicePhase::Unready => "unready", 271 ServicePhase::Stopping => "stopping", 272 ServicePhase::Failed => "failed", 273 }, 274 ) 275 } 276 277 pub fn task(task: &TaskName) -> Result<Self, MetricsContractError> { 278 Self::validated(MetricLabelKey::Task, task.as_str()) 279 } 280 281 #[must_use] 282 pub fn classification(classification: TaskClassification) -> Self { 283 Self::fixed( 284 MetricLabelKey::Classification, 285 match classification { 286 TaskClassification::Critical => "critical", 287 TaskClassification::Optional => "optional", 288 TaskClassification::OneShot => "one_shot", 289 }, 290 ) 291 } 292 293 #[must_use] 294 pub fn storage(storage: MetricComponentId) -> Self { 295 Self::fixed_owned(MetricLabelKey::Storage, storage.0) 296 } 297 298 #[must_use] 299 pub fn transport(transport: MetricComponentId) -> Self { 300 Self::fixed_owned(MetricLabelKey::Transport, transport.0) 301 } 302 303 #[must_use] 304 pub fn relay_id(value: StableRelayId) -> Self { 305 Self::fixed_owned(MetricLabelKey::RelayId, value.0) 306 } 307 308 #[must_use] 309 pub fn health_state(state: MetricHealthState) -> Self { 310 Self::fixed(MetricLabelKey::State, state.as_str()) 311 } 312 313 #[must_use] 314 pub fn task_outcome(outcome: MetricTaskOutcome) -> Self { 315 Self::fixed(MetricLabelKey::Outcome, outcome.as_str()) 316 } 317 318 #[must_use] 319 pub const fn key(&self) -> MetricLabelKey { 320 self.key 321 } 322 323 #[must_use] 324 pub fn value(&self) -> &str { 325 self.value.as_str() 326 } 327 328 fn validated(key: MetricLabelKey, value: &str) -> Result<Self, MetricsContractError> { 329 Ok(Self { 330 key, 331 value: MetricLabelValue::new(value)?, 332 }) 333 } 334 335 fn fixed(key: MetricLabelKey, value: &'static str) -> Self { 336 Self { 337 key, 338 value: MetricLabelValue(value.to_owned()), 339 } 340 } 341 342 fn fixed_owned(key: MetricLabelKey, value: String) -> Self { 343 Self { 344 key, 345 value: MetricLabelValue(value), 346 } 347 } 348 } 349 350 impl fmt::Debug for MetricLabel { 351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 352 formatter 353 .debug_struct("MetricLabel") 354 .field("key", &self.key) 355 .field("value", &"[redacted]") 356 .finish() 357 } 358 } 359 360 /// One validated metric family descriptor. 361 #[derive(Clone, PartialEq, Eq)] 362 pub struct MetricDescriptor { 363 group: CommonMetricGroup, 364 name: MetricName, 365 help: String, 366 kind: MetricKind, 367 label_keys: Vec<MetricLabelKey>, 368 } 369 370 impl MetricDescriptor { 371 pub fn new( 372 group: CommonMetricGroup, 373 name: MetricName, 374 help: impl AsRef<str>, 375 kind: MetricKind, 376 label_keys: impl IntoIterator<Item = MetricLabelKey>, 377 ) -> Result<Self, MetricsContractError> { 378 let help = help.as_ref(); 379 if help.is_empty() 380 || help.len() > METRIC_HELP_MAX_BYTES 381 || help 382 .chars() 383 .any(|character| character.is_control() && character != '\n') 384 { 385 return Err(MetricsContractError::InvalidHelp); 386 } 387 let mut label_keys: Vec<_> = label_keys 388 .into_iter() 389 .take(METRICS_MAX_LABELS_PER_SAMPLE + 1) 390 .collect(); 391 if label_keys.len() > METRICS_MAX_LABELS_PER_SAMPLE { 392 return Err(MetricsContractError::TooManyLabels); 393 } 394 label_keys.sort_unstable(); 395 if label_keys.windows(2).any(|pair| pair[0] == pair[1]) { 396 return Err(MetricsContractError::DuplicateLabelKey); 397 } 398 if label_keys.iter().any(|key| !key.allowed_for(group)) { 399 return Err(MetricsContractError::ForbiddenLabelKey); 400 } 401 Ok(Self { 402 group, 403 name, 404 help: help.to_owned(), 405 kind, 406 label_keys, 407 }) 408 } 409 410 #[must_use] 411 pub const fn group(&self) -> CommonMetricGroup { 412 self.group 413 } 414 415 #[must_use] 416 pub fn name(&self) -> &MetricName { 417 &self.name 418 } 419 420 #[must_use] 421 pub const fn kind(&self) -> MetricKind { 422 self.kind 423 } 424 425 #[must_use] 426 pub fn label_keys(&self) -> &[MetricLabelKey] { 427 &self.label_keys 428 } 429 } 430 431 impl fmt::Debug for MetricDescriptor { 432 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 433 formatter 434 .debug_struct("MetricDescriptor") 435 .field("group", &self.group) 436 .field("name", &self.name) 437 .field("help", &"[redacted]") 438 .field("kind", &self.kind) 439 .field("label_keys", &self.label_keys) 440 .finish() 441 } 442 } 443 444 /// Exact integer sample value; floating-point ambiguity is intentionally absent. 445 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 446 pub enum MetricValue { 447 Counter(u64), 448 Gauge(i64), 449 } 450 451 impl MetricValue { 452 const fn kind(self) -> MetricKind { 453 match self { 454 Self::Counter(_) => MetricKind::Counter, 455 Self::Gauge(_) => MetricKind::Gauge, 456 } 457 } 458 } 459 460 /// One bounded sample associated with a descriptor by exact name. 461 #[derive(Clone, PartialEq, Eq)] 462 pub struct MetricSample { 463 name: MetricName, 464 value: MetricValue, 465 labels: Vec<MetricLabel>, 466 } 467 468 impl MetricSample { 469 pub fn new( 470 name: MetricName, 471 value: MetricValue, 472 labels: impl IntoIterator<Item = MetricLabel>, 473 ) -> Result<Self, MetricsContractError> { 474 let mut labels: Vec<_> = labels 475 .into_iter() 476 .take(METRICS_MAX_LABELS_PER_SAMPLE + 1) 477 .collect(); 478 if labels.len() > METRICS_MAX_LABELS_PER_SAMPLE { 479 return Err(MetricsContractError::TooManyLabels); 480 } 481 labels.sort_unstable(); 482 if labels.windows(2).any(|pair| pair[0].key == pair[1].key) { 483 return Err(MetricsContractError::DuplicateLabelKey); 484 } 485 Ok(Self { 486 name, 487 value, 488 labels, 489 }) 490 } 491 492 #[must_use] 493 pub fn name(&self) -> &MetricName { 494 &self.name 495 } 496 497 #[must_use] 498 pub const fn value(&self) -> MetricValue { 499 self.value 500 } 501 502 #[must_use] 503 pub fn labels(&self) -> &[MetricLabel] { 504 &self.labels 505 } 506 } 507 508 impl fmt::Debug for MetricSample { 509 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 510 formatter 511 .debug_struct("MetricSample") 512 .field("name", &self.name) 513 .field("value", &self.value) 514 .field("label_count", &self.labels.len()) 515 .finish() 516 } 517 } 518 519 /// An immutable validated metrics snapshot with deterministic ordering. 520 #[derive(Clone, PartialEq, Eq)] 521 pub struct BoundedMetricsSnapshot { 522 descriptors: Vec<MetricDescriptor>, 523 samples: Vec<MetricSample>, 524 } 525 526 impl BoundedMetricsSnapshot { 527 pub fn new( 528 descriptors: impl IntoIterator<Item = MetricDescriptor>, 529 samples: impl IntoIterator<Item = MetricSample>, 530 ) -> Result<Self, MetricsContractError> { 531 let mut descriptors: Vec<_> = descriptors 532 .into_iter() 533 .take(METRICS_MAX_DESCRIPTORS + 1) 534 .collect(); 535 let mut samples: Vec<_> = samples.into_iter().take(METRICS_MAX_SAMPLES + 1).collect(); 536 if descriptors.len() > METRICS_MAX_DESCRIPTORS { 537 return Err(MetricsContractError::TooManyDescriptors); 538 } 539 if samples.len() > METRICS_MAX_SAMPLES { 540 return Err(MetricsContractError::TooManySamples); 541 } 542 descriptors.sort_by(|left, right| left.name.cmp(&right.name)); 543 if descriptors 544 .windows(2) 545 .any(|pair| pair[0].name == pair[1].name) 546 { 547 return Err(MetricsContractError::DuplicateDescriptor); 548 } 549 let descriptor_by_name: BTreeMap<_, _> = descriptors 550 .iter() 551 .map(|descriptor| (&descriptor.name, descriptor)) 552 .collect(); 553 for sample in &samples { 554 let descriptor = descriptor_by_name 555 .get(&sample.name) 556 .ok_or(MetricsContractError::UnknownDescriptor)?; 557 if sample.value.kind() != descriptor.kind { 558 return Err(MetricsContractError::ValueKindMismatch); 559 } 560 let actual_keys: Vec<_> = sample.labels.iter().map(|label| label.key).collect(); 561 if actual_keys != descriptor.label_keys { 562 return Err(MetricsContractError::LabelSetMismatch); 563 } 564 } 565 samples.sort_by(|left, right| { 566 left.name 567 .cmp(&right.name) 568 .then_with(|| left.labels.cmp(&right.labels)) 569 }); 570 if samples 571 .windows(2) 572 .any(|pair| pair[0].name == pair[1].name && pair[0].labels == pair[1].labels) 573 { 574 return Err(MetricsContractError::DuplicateSample); 575 } 576 Ok(Self { 577 descriptors, 578 samples, 579 }) 580 } 581 582 #[must_use] 583 pub fn descriptors(&self) -> &[MetricDescriptor] { 584 &self.descriptors 585 } 586 587 #[must_use] 588 pub fn samples(&self) -> &[MetricSample] { 589 &self.samples 590 } 591 592 /// Renders deterministic Prometheus text without exceeding `maximum_bytes`. 593 pub fn render(&self, maximum_bytes: usize) -> Result<Vec<u8>, MetricsRenderError> { 594 if maximum_bytes == 0 || maximum_bytes > METRICS_MAX_RENDER_UTF8_BYTES { 595 return Err(MetricsRenderError::InvalidMaximum); 596 } 597 let mut output = BoundedOutput::new(maximum_bytes); 598 for descriptor in &self.descriptors { 599 output.push_str("# HELP ")?; 600 output.push_str(descriptor.name.as_str())?; 601 output.push_byte(b' ')?; 602 output.push_escaped_help(&descriptor.help)?; 603 output.push_byte(b'\n')?; 604 output.push_str("# TYPE ")?; 605 output.push_str(descriptor.name.as_str())?; 606 output.push_byte(b' ')?; 607 output.push_str(descriptor.kind.as_str())?; 608 output.push_byte(b'\n')?; 609 610 for sample in self 611 .samples 612 .iter() 613 .filter(|sample| sample.name == descriptor.name) 614 { 615 output.push_str(sample.name.as_str())?; 616 if !sample.labels.is_empty() { 617 output.push_byte(b'{')?; 618 for (index, label) in sample.labels.iter().enumerate() { 619 if index != 0 { 620 output.push_byte(b',')?; 621 } 622 output.push_str(label.key.as_str())?; 623 output.push_str("=\"")?; 624 output.push_escaped_label_value(label.value.as_str())?; 625 output.push_byte(b'\"')?; 626 } 627 output.push_byte(b'}')?; 628 } 629 output.push_byte(b' ')?; 630 match sample.value { 631 MetricValue::Counter(value) => output.push_str(&value.to_string())?, 632 MetricValue::Gauge(value) => output.push_str(&value.to_string())?, 633 } 634 output.push_byte(b'\n')?; 635 } 636 } 637 Ok(output.finish()) 638 } 639 } 640 641 impl fmt::Debug for BoundedMetricsSnapshot { 642 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 643 formatter 644 .debug_struct("BoundedMetricsSnapshot") 645 .field("descriptor_count", &self.descriptors.len()) 646 .field("sample_count", &self.samples.len()) 647 .finish() 648 } 649 } 650 651 /// Safe construction failure for the metrics contract. 652 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 653 pub enum MetricsContractError { 654 InvalidMetricName, 655 InvalidHelp, 656 InvalidLabelValue, 657 InvalidStableRelayId, 658 InvalidComponentId, 659 ForbiddenLabelKey, 660 DuplicateLabelKey, 661 TooManyLabels, 662 TooManyDescriptors, 663 TooManySamples, 664 DuplicateDescriptor, 665 UnknownDescriptor, 666 ValueKindMismatch, 667 LabelSetMismatch, 668 DuplicateSample, 669 } 670 671 impl fmt::Display for MetricsContractError { 672 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 673 formatter.write_str("metrics snapshot violates the bounded host contract") 674 } 675 } 676 677 impl Error for MetricsContractError {} 678 679 /// Safe deterministic-rendering failure. 680 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 681 pub enum MetricsRenderError { 682 InvalidMaximum, 683 ResponseTooLarge, 684 } 685 686 impl fmt::Display for MetricsRenderError { 687 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 688 formatter.write_str("metrics snapshot cannot be rendered within the response limit") 689 } 690 } 691 692 impl Error for MetricsRenderError {} 693 694 struct BoundedOutput { 695 bytes: Vec<u8>, 696 maximum: usize, 697 } 698 699 impl BoundedOutput { 700 fn new(maximum: usize) -> Self { 701 Self { 702 bytes: Vec::with_capacity(maximum.min(4096)), 703 maximum, 704 } 705 } 706 707 fn push_byte(&mut self, byte: u8) -> Result<(), MetricsRenderError> { 708 if self.bytes.len() == self.maximum { 709 return Err(MetricsRenderError::ResponseTooLarge); 710 } 711 self.bytes.push(byte); 712 Ok(()) 713 } 714 715 fn push_str(&mut self, value: &str) -> Result<(), MetricsRenderError> { 716 let remaining = self.maximum.saturating_sub(self.bytes.len()); 717 if value.len() > remaining { 718 return Err(MetricsRenderError::ResponseTooLarge); 719 } 720 self.bytes.extend_from_slice(value.as_bytes()); 721 Ok(()) 722 } 723 724 fn push_escaped_help(&mut self, value: &str) -> Result<(), MetricsRenderError> { 725 for character in value.chars() { 726 match character { 727 '\\' => self.push_str("\\\\")?, 728 '\n' => self.push_str("\\n")?, 729 _ => { 730 let mut bytes = [0; 4]; 731 self.push_str(character.encode_utf8(&mut bytes))?; 732 } 733 } 734 } 735 Ok(()) 736 } 737 738 fn push_escaped_label_value(&mut self, value: &str) -> Result<(), MetricsRenderError> { 739 for character in value.chars() { 740 match character { 741 '\\' => self.push_str("\\\\")?, 742 '\"' => self.push_str("\\\"")?, 743 '\n' => self.push_str("\\n")?, 744 _ => { 745 let mut bytes = [0; 4]; 746 self.push_str(character.encode_utf8(&mut bytes))?; 747 } 748 } 749 } 750 Ok(()) 751 } 752 753 fn finish(self) -> Vec<u8> { 754 self.bytes 755 } 756 } 757 758 fn valid_metric_name(value: &str) -> bool { 759 value.bytes().enumerate().all(|(index, byte)| { 760 if index == 0 { 761 byte.is_ascii_alphabetic() || matches!(byte, b'_' | b':') 762 } else { 763 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':') 764 } 765 }) 766 } 767 768 #[cfg(test)] 769 mod tests { 770 use std::collections::BTreeSet; 771 use std::error::Error; 772 773 use super::*; 774 use crate::{BuildInfoEnvironment, BuildMode, ContractVersions}; 775 776 fn name(value: &str) -> MetricName { 777 MetricName::new(value).unwrap() 778 } 779 780 fn descriptor( 781 group: CommonMetricGroup, 782 metric_name: &str, 783 kind: MetricKind, 784 labels: &[MetricLabelKey], 785 ) -> MetricDescriptor { 786 MetricDescriptor::new( 787 group, 788 name(metric_name), 789 "safe help", 790 kind, 791 labels.iter().copied(), 792 ) 793 .unwrap() 794 } 795 796 fn build_info() -> BuildInfo { 797 BuildInfo::from_compile_time( 798 BuildMode::Release, 799 BuildInfoEnvironment { 800 service_version: Some("1.2.3"), 801 service_commit: Some("1111111111111111111111111111111111111111"), 802 lib_revision: Some("2222222222222222222222222222222222222222"), 803 rust_version: Some("1.97.0"), 804 target: Some("x86_64-unknown-linux-gnu"), 805 feature_profile: Some("release"), 806 contract_versions: ContractVersions::new(1, 1, 1, 1, 1).unwrap(), 807 }, 808 ) 809 .unwrap() 810 } 811 812 #[test] 813 fn arbitrary_and_cross_group_labels_are_rejected() { 814 assert_eq!( 815 MetricDescriptor::new( 816 CommonMetricGroup::Build, 817 name("radroots_build_info"), 818 "build information", 819 MetricKind::Gauge, 820 [MetricLabelKey::RelayId], 821 ), 822 Err(MetricsContractError::ForbiddenLabelKey) 823 ); 824 assert_eq!( 825 MetricDescriptor::new( 826 CommonMetricGroup::Transport, 827 name("radroots_transport_state"), 828 "transport state", 829 MetricKind::Gauge, 830 [MetricLabelKey::RelayId, MetricLabelKey::RelayId], 831 ), 832 Err(MetricsContractError::DuplicateLabelKey) 833 ); 834 assert_eq!( 835 MetricName::new("bad metric"), 836 Err(MetricsContractError::InvalidMetricName) 837 ); 838 } 839 840 #[test] 841 fn relay_labels_require_stable_ids_and_reject_raw_urls() { 842 let stable = StableRelayId::new("relay-west-01").unwrap(); 843 let label = MetricLabel::relay_id(stable.clone()); 844 assert_eq!(label.value(), stable.as_str()); 845 for forbidden in [ 846 "wss://relay.example.com", 847 "Relay-West", 848 "relay west", 849 "-relay", 850 "", 851 ] { 852 assert_eq!( 853 StableRelayId::new(forbidden), 854 Err(MetricsContractError::InvalidStableRelayId) 855 ); 856 } 857 } 858 859 #[test] 860 fn rendering_is_sorted_and_escapes_help() { 861 let descriptor = MetricDescriptor::new( 862 CommonMetricGroup::Task, 863 name("radroots_task_outcomes_total"), 864 "task\\outcomes\nby state", 865 MetricKind::Counter, 866 [MetricLabelKey::Task, MetricLabelKey::Outcome], 867 ) 868 .unwrap(); 869 let sample = MetricSample::new( 870 name("radroots_task_outcomes_total"), 871 MetricValue::Counter(7), 872 [ 873 MetricLabel::task_outcome(MetricTaskOutcome::OptionalFailure), 874 MetricLabel::task(&TaskName::new("worker_one").unwrap()).unwrap(), 875 ], 876 ) 877 .unwrap(); 878 let snapshot = BoundedMetricsSnapshot::new([descriptor], [sample]).unwrap(); 879 assert_eq!( 880 String::from_utf8(snapshot.render(1024).unwrap()).unwrap(), 881 concat!( 882 "# HELP radroots_task_outcomes_total task\\\\outcomes\\nby state\n", 883 "# TYPE radroots_task_outcomes_total counter\n", 884 "radroots_task_outcomes_total{task=\"worker_one\",outcome=\"optional_failure\"} 7\n", 885 ) 886 ); 887 888 let mut defensive = BoundedOutput::new(64); 889 defensive 890 .push_escaped_label_value("worker\\\"one\n") 891 .unwrap(); 892 assert_eq!(defensive.finish(), b"worker\\\\\\\"one\\n"); 893 } 894 895 #[test] 896 fn component_ids_reject_free_text_urls_paths_and_control_characters() { 897 for forbidden in [ 898 "raw error text", 899 "wss://relay.example.com", 900 "/private/path", 901 "line\nfeed", 902 "quote\"value", 903 "back\\slash", 904 "", 905 ] { 906 assert_eq!( 907 MetricComponentId::new(forbidden), 908 Err(MetricsContractError::InvalidComponentId) 909 ); 910 } 911 assert!(MetricComponentId::new("sqlite_writer").is_ok()); 912 } 913 914 #[test] 915 fn duplicates_unknowns_kind_and_label_mismatches_fail_closed() { 916 let phase = descriptor( 917 CommonMetricGroup::Phase, 918 "radroots_phase", 919 MetricKind::Gauge, 920 &[MetricLabelKey::Phase], 921 ); 922 assert_eq!( 923 BoundedMetricsSnapshot::new([phase.clone(), phase.clone()], []), 924 Err(MetricsContractError::DuplicateDescriptor) 925 ); 926 let unknown = 927 MetricSample::new(name("radroots_unknown"), MetricValue::Gauge(1), []).unwrap(); 928 assert_eq!( 929 BoundedMetricsSnapshot::new([phase.clone()], [unknown]), 930 Err(MetricsContractError::UnknownDescriptor) 931 ); 932 let wrong_kind = MetricSample::new( 933 name("radroots_phase"), 934 MetricValue::Counter(1), 935 [MetricLabel::phase(ServicePhase::Ready)], 936 ) 937 .unwrap(); 938 assert_eq!( 939 BoundedMetricsSnapshot::new([phase.clone()], [wrong_kind]), 940 Err(MetricsContractError::ValueKindMismatch) 941 ); 942 let wrong_labels = 943 MetricSample::new(name("radroots_phase"), MetricValue::Gauge(1), []).unwrap(); 944 assert_eq!( 945 BoundedMetricsSnapshot::new([phase], [wrong_labels]), 946 Err(MetricsContractError::LabelSetMismatch) 947 ); 948 } 949 950 #[test] 951 fn render_and_collection_bounds_fail_before_overgrowth() { 952 let descriptor = descriptor( 953 CommonMetricGroup::Phase, 954 "radroots_phase", 955 MetricKind::Gauge, 956 &[MetricLabelKey::Phase], 957 ); 958 let sample = MetricSample::new( 959 name("radroots_phase"), 960 MetricValue::Gauge(1), 961 [MetricLabel::phase(ServicePhase::Ready)], 962 ) 963 .unwrap(); 964 let snapshot = BoundedMetricsSnapshot::new([descriptor.clone()], [sample]).unwrap(); 965 let exact = snapshot.render(METRICS_MAX_RENDER_UTF8_BYTES).unwrap(); 966 assert_eq!(snapshot.render(exact.len()).unwrap(), exact); 967 assert_eq!( 968 snapshot.render(exact.len() - 1), 969 Err(MetricsRenderError::ResponseTooLarge) 970 ); 971 assert_eq!(snapshot.render(0), Err(MetricsRenderError::InvalidMaximum)); 972 assert_eq!( 973 snapshot.render(METRICS_MAX_RENDER_UTF8_BYTES + 1), 974 Err(MetricsRenderError::InvalidMaximum) 975 ); 976 977 assert_eq!( 978 BoundedMetricsSnapshot::new( 979 std::iter::repeat_n(descriptor, METRICS_MAX_DESCRIPTORS + 1), 980 [], 981 ), 982 Err(MetricsContractError::TooManyDescriptors) 983 ); 984 } 985 986 #[test] 987 fn every_common_group_and_label_key_has_a_stable_inventory() { 988 assert_eq!(METRICS_MAX_DESCRIPTORS, 64); 989 assert_eq!(METRICS_MAX_SAMPLES, 512); 990 assert_eq!(METRICS_MAX_LABELS_PER_SAMPLE, 8); 991 assert_eq!(METRICS_MAX_RENDER_UTF8_BYTES, 1_048_576); 992 assert_eq!(METRIC_NAME_MAX_BYTES, 128); 993 assert_eq!(METRIC_HELP_MAX_BYTES, 512); 994 assert_eq!(METRIC_LABEL_VALUE_MAX_BYTES, 128); 995 assert_eq!(STABLE_RELAY_ID_MAX_BYTES, 64); 996 997 let inventory = [ 998 ( 999 CommonMetricGroup::Build, 1000 &[MetricLabelKey::Version, MetricLabelKey::Revision][..], 1001 ), 1002 (CommonMetricGroup::Phase, &[MetricLabelKey::Phase][..]), 1003 ( 1004 CommonMetricGroup::Task, 1005 &[ 1006 MetricLabelKey::Task, 1007 MetricLabelKey::Classification, 1008 MetricLabelKey::Outcome, 1009 ][..], 1010 ), 1011 ( 1012 CommonMetricGroup::Storage, 1013 &[MetricLabelKey::Storage, MetricLabelKey::State][..], 1014 ), 1015 ( 1016 CommonMetricGroup::Transport, 1017 &[ 1018 MetricLabelKey::Transport, 1019 MetricLabelKey::RelayId, 1020 MetricLabelKey::State, 1021 ][..], 1022 ), 1023 ]; 1024 let all_keys: BTreeSet<_> = [ 1025 MetricLabelKey::Version, 1026 MetricLabelKey::Revision, 1027 MetricLabelKey::Phase, 1028 MetricLabelKey::Task, 1029 MetricLabelKey::Classification, 1030 MetricLabelKey::Storage, 1031 MetricLabelKey::Transport, 1032 MetricLabelKey::RelayId, 1033 MetricLabelKey::State, 1034 MetricLabelKey::Outcome, 1035 ] 1036 .into_iter() 1037 .collect(); 1038 assert_eq!( 1039 inventory 1040 .iter() 1041 .flat_map(|(_, keys)| keys.iter().copied()) 1042 .collect::<BTreeSet<_>>(), 1043 all_keys 1044 ); 1045 for (group, allowed) in inventory { 1046 for key in all_keys.iter().copied() { 1047 assert_eq!(key.allowed_for(group), allowed.contains(&key)); 1048 } 1049 } 1050 } 1051 1052 #[test] 1053 fn typed_label_vocabularies_are_exact_and_non_bypassable() { 1054 let phases = [ 1055 ServicePhase::Starting, 1056 ServicePhase::Ready, 1057 ServicePhase::Degraded, 1058 ServicePhase::Unready, 1059 ServicePhase::Stopping, 1060 ServicePhase::Failed, 1061 ] 1062 .map(MetricLabel::phase); 1063 assert_eq!( 1064 phases.map(|label| label.value().to_owned()), 1065 [ 1066 "starting", "ready", "degraded", "unready", "stopping", "failed" 1067 ] 1068 .map(str::to_owned) 1069 ); 1070 assert_eq!( 1071 [ 1072 TaskClassification::Critical, 1073 TaskClassification::Optional, 1074 TaskClassification::OneShot, 1075 ] 1076 .map(MetricLabel::classification) 1077 .map(|label| label.value().to_owned()), 1078 ["critical", "optional", "one_shot"].map(str::to_owned) 1079 ); 1080 assert_eq!( 1081 [ 1082 MetricHealthState::Ready, 1083 MetricHealthState::Degraded, 1084 MetricHealthState::Unready, 1085 MetricHealthState::ReadOnly, 1086 MetricHealthState::RepairRequired, 1087 MetricHealthState::Unavailable, 1088 ] 1089 .map(MetricLabel::health_state) 1090 .map(|label| label.value().to_owned()), 1091 [ 1092 "ready", 1093 "degraded", 1094 "unready", 1095 "read_only", 1096 "repair_required", 1097 "unavailable", 1098 ] 1099 .map(str::to_owned) 1100 ); 1101 assert_eq!( 1102 [ 1103 MetricTaskOutcome::ExpectedCompletion, 1104 MetricTaskOutcome::OptionalFailure, 1105 MetricTaskOutcome::TaskReturnedError, 1106 MetricTaskOutcome::TaskPanicked, 1107 MetricTaskOutcome::UnexpectedCompletion, 1108 MetricTaskOutcome::UnexpectedCancellation, 1109 MetricTaskOutcome::JoinFailed, 1110 ] 1111 .map(MetricLabel::task_outcome) 1112 .map(|label| label.value().to_owned()), 1113 [ 1114 "expected_completion", 1115 "optional_failure", 1116 "task_returned_error", 1117 "task_panicked", 1118 "unexpected_completion", 1119 "unexpected_cancellation", 1120 "join_failed", 1121 ] 1122 .map(str::to_owned) 1123 ); 1124 } 1125 1126 #[test] 1127 fn infinite_iterators_are_bounded_during_ingestion() { 1128 assert_eq!( 1129 MetricDescriptor::new( 1130 CommonMetricGroup::Task, 1131 name("radroots_task"), 1132 "task", 1133 MetricKind::Gauge, 1134 std::iter::repeat(MetricLabelKey::Task), 1135 ), 1136 Err(MetricsContractError::TooManyLabels) 1137 ); 1138 assert_eq!( 1139 MetricSample::new( 1140 name("radroots_phase"), 1141 MetricValue::Gauge(1), 1142 std::iter::repeat(MetricLabel::phase(ServicePhase::Ready)), 1143 ), 1144 Err(MetricsContractError::TooManyLabels) 1145 ); 1146 1147 let descriptor = descriptor( 1148 CommonMetricGroup::Phase, 1149 "radroots_phase", 1150 MetricKind::Gauge, 1151 &[MetricLabelKey::Phase], 1152 ); 1153 let sample = MetricSample::new( 1154 name("radroots_phase"), 1155 MetricValue::Gauge(1), 1156 [MetricLabel::phase(ServicePhase::Ready)], 1157 ) 1158 .unwrap(); 1159 assert_eq!( 1160 BoundedMetricsSnapshot::new(std::iter::repeat(descriptor.clone()), []), 1161 Err(MetricsContractError::TooManyDescriptors) 1162 ); 1163 assert_eq!( 1164 BoundedMetricsSnapshot::new([descriptor], std::iter::repeat(sample)), 1165 Err(MetricsContractError::TooManySamples) 1166 ); 1167 } 1168 1169 #[test] 1170 fn accessors_debug_errors_and_remaining_bounds_are_exact() { 1171 let build = build_info(); 1172 let version = MetricLabel::build_version(&build).unwrap(); 1173 let revision = MetricLabel::build_revision(&build).unwrap(); 1174 assert_eq!(version.key(), MetricLabelKey::Version); 1175 assert_eq!(version.value(), "1.2.3"); 1176 assert_eq!(revision.key(), MetricLabelKey::Revision); 1177 assert_eq!(revision.value(), "1111111111111111111111111111111111111111"); 1178 assert_eq!( 1179 format!("{version:?}"), 1180 "MetricLabel { key: Version, value: \"[redacted]\" }" 1181 ); 1182 1183 let storage_id = MetricComponentId::new("sqlite_writer").unwrap(); 1184 assert_eq!(storage_id.as_str(), "sqlite_writer"); 1185 let transport_id = MetricComponentId::new("nostr_relay").unwrap(); 1186 assert_eq!(transport_id.as_str(), "nostr_relay"); 1187 assert_eq!(MetricLabel::storage(storage_id).value(), "sqlite_writer"); 1188 assert_eq!(MetricLabel::transport(transport_id).value(), "nostr_relay"); 1189 assert!(MetricComponentId::new("a0").is_ok()); 1190 assert!(MetricComponentId::new("a_").is_ok()); 1191 assert_eq!( 1192 MetricComponentId::new("a-").unwrap_err(), 1193 MetricsContractError::InvalidComponentId 1194 ); 1195 1196 let metric_name = name("radroots_build_info"); 1197 assert_eq!(metric_name.as_str(), "radroots_build_info"); 1198 let build_descriptor = MetricDescriptor::new( 1199 CommonMetricGroup::Build, 1200 metric_name.clone(), 1201 "build identity", 1202 MetricKind::Gauge, 1203 [MetricLabelKey::Revision, MetricLabelKey::Version], 1204 ) 1205 .unwrap(); 1206 assert_eq!(build_descriptor.group(), CommonMetricGroup::Build); 1207 assert_eq!(build_descriptor.name(), &metric_name); 1208 assert_eq!(build_descriptor.kind(), MetricKind::Gauge); 1209 assert_eq!( 1210 build_descriptor.label_keys(), 1211 &[MetricLabelKey::Version, MetricLabelKey::Revision] 1212 ); 1213 assert!(format!("{build_descriptor:?}").contains("help: \"[redacted]\"")); 1214 1215 let sample = 1216 MetricSample::new(metric_name, MetricValue::Gauge(1), [revision, version]).unwrap(); 1217 assert_eq!(sample.name().as_str(), "radroots_build_info"); 1218 assert_eq!(sample.value(), MetricValue::Gauge(1)); 1219 assert_eq!(sample.labels().len(), 2); 1220 assert!(format!("{sample:?}").contains("label_count: 2")); 1221 assert_eq!( 1222 MetricSample::new( 1223 name("radroots_duplicate_labels"), 1224 MetricValue::Gauge(1), 1225 [ 1226 MetricLabel::phase(ServicePhase::Ready), 1227 MetricLabel::phase(ServicePhase::Degraded), 1228 ], 1229 ), 1230 Err(MetricsContractError::DuplicateLabelKey) 1231 ); 1232 1233 let snapshot = BoundedMetricsSnapshot::new([build_descriptor], [sample.clone()]).unwrap(); 1234 assert_eq!(snapshot.descriptors().len(), 1); 1235 assert_eq!(snapshot.samples(), std::slice::from_ref(&sample)); 1236 assert_eq!( 1237 format!("{snapshot:?}"), 1238 "BoundedMetricsSnapshot { descriptor_count: 1, sample_count: 1 }" 1239 ); 1240 let rendered = String::from_utf8(snapshot.render(4096).unwrap()).unwrap(); 1241 assert!(rendered.contains("version=\"1.2.3\",revision=")); 1242 assert_eq!( 1243 BoundedMetricsSnapshot::new(snapshot.descriptors().to_vec(), [sample.clone(), sample]), 1244 Err(MetricsContractError::DuplicateSample) 1245 ); 1246 1247 let phase_descriptor = descriptor( 1248 CommonMetricGroup::Phase, 1249 "radroots_phase_limit", 1250 MetricKind::Gauge, 1251 &[MetricLabelKey::Phase], 1252 ); 1253 let phase_sample = MetricSample::new( 1254 name("radroots_phase_limit"), 1255 MetricValue::Gauge(1), 1256 [MetricLabel::phase(ServicePhase::Ready)], 1257 ) 1258 .unwrap(); 1259 assert_eq!( 1260 BoundedMetricsSnapshot::new( 1261 [phase_descriptor], 1262 std::iter::repeat_n(phase_sample, METRICS_MAX_SAMPLES + 1), 1263 ), 1264 Err(MetricsContractError::TooManySamples) 1265 ); 1266 1267 for invalid_help in [ 1268 String::new(), 1269 "x".repeat(METRIC_HELP_MAX_BYTES + 1), 1270 "unsafe\u{0000}help".to_owned(), 1271 ] { 1272 assert_eq!( 1273 MetricDescriptor::new( 1274 CommonMetricGroup::Phase, 1275 name("radroots_invalid_help"), 1276 invalid_help, 1277 MetricKind::Gauge, 1278 [MetricLabelKey::Phase], 1279 ), 1280 Err(MetricsContractError::InvalidHelp) 1281 ); 1282 } 1283 for invalid_value in ["", "-leading", "contains space"] { 1284 assert!(matches!( 1285 MetricLabelValue::new(invalid_value), 1286 Err(MetricsContractError::InvalidLabelValue) 1287 )); 1288 } 1289 assert!(MetricLabelValue::new("A0._:-").is_ok()); 1290 assert!(matches!( 1291 MetricLabelValue::new("A/"), 1292 Err(MetricsContractError::InvalidLabelValue) 1293 )); 1294 assert!(MetricName::new("_metric").is_ok()); 1295 assert!(MetricName::new(":metric9").is_ok()); 1296 assert!(MetricName::new("a".repeat(METRIC_NAME_MAX_BYTES)).is_ok()); 1297 assert_eq!( 1298 MetricName::new("").unwrap_err(), 1299 MetricsContractError::InvalidMetricName 1300 ); 1301 assert_eq!( 1302 MetricName::new("9metric").unwrap_err(), 1303 MetricsContractError::InvalidMetricName 1304 ); 1305 assert_eq!( 1306 MetricName::new("x".repeat(METRIC_NAME_MAX_BYTES + 1)), 1307 Err(MetricsContractError::InvalidMetricName) 1308 ); 1309 assert_eq!( 1310 StableRelayId::new("x".repeat(STABLE_RELAY_ID_MAX_BYTES + 1)), 1311 Err(MetricsContractError::InvalidStableRelayId) 1312 ); 1313 assert!(StableRelayId::new("x".repeat(STABLE_RELAY_ID_MAX_BYTES)).is_ok()); 1314 assert_eq!( 1315 MetricComponentId::new("x".repeat(METRIC_LABEL_VALUE_MAX_BYTES + 1)), 1316 Err(MetricsContractError::InvalidComponentId) 1317 ); 1318 assert!(MetricComponentId::new("x".repeat(METRIC_LABEL_VALUE_MAX_BYTES)).is_ok()); 1319 assert!(MetricLabelValue::new("x".repeat(METRIC_LABEL_VALUE_MAX_BYTES)).is_ok()); 1320 assert!(matches!( 1321 MetricLabelValue::new("x".repeat(METRIC_LABEL_VALUE_MAX_BYTES + 1)), 1322 Err(MetricsContractError::InvalidLabelValue) 1323 )); 1324 1325 let exact_help = "x".repeat(METRIC_HELP_MAX_BYTES); 1326 assert!( 1327 MetricDescriptor::new( 1328 CommonMetricGroup::Phase, 1329 name("radroots_exact_help"), 1330 &exact_help, 1331 MetricKind::Gauge, 1332 [MetricLabelKey::Phase], 1333 ) 1334 .is_ok() 1335 ); 1336 1337 let very_large = "x".repeat(4 * 1024 * 1024); 1338 assert_eq!( 1339 MetricName::new(&very_large), 1340 Err(MetricsContractError::InvalidMetricName) 1341 ); 1342 assert_eq!( 1343 StableRelayId::new(&very_large), 1344 Err(MetricsContractError::InvalidStableRelayId) 1345 ); 1346 assert_eq!( 1347 MetricComponentId::new(&very_large), 1348 Err(MetricsContractError::InvalidComponentId) 1349 ); 1350 assert!(matches!( 1351 MetricLabelValue::new(&very_large), 1352 Err(MetricsContractError::InvalidLabelValue) 1353 )); 1354 assert_eq!( 1355 MetricDescriptor::new( 1356 CommonMetricGroup::Phase, 1357 name("radroots_very_large_help"), 1358 &very_large, 1359 MetricKind::Gauge, 1360 [MetricLabelKey::Phase], 1361 ), 1362 Err(MetricsContractError::InvalidHelp) 1363 ); 1364 1365 for error in [ 1366 MetricsContractError::InvalidMetricName.to_string(), 1367 MetricsRenderError::InvalidMaximum.to_string(), 1368 ] { 1369 assert!(!error.is_empty()); 1370 } 1371 assert!(MetricsContractError::InvalidMetricName.source().is_none()); 1372 assert!(MetricsRenderError::ResponseTooLarge.source().is_none()); 1373 } 1374 }