1use serde_json::Value;
36use std::collections::{BTreeMap, BTreeSet};
37
38pub const MAX_SAMPLES: usize = 50_000;
44
45pub const MAX_NAME_LEN: usize = 256;
47
48pub const MAX_TEXT_LEN: usize = 16 * 1024;
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum Format {
54 Text,
56 OpenMetrics,
58}
59
60impl Format {
61 pub fn content_type(self) -> &'static str {
63 match self {
64 Format::Text => "text/plain; version=0.0.4; charset=utf-8",
65 Format::OpenMetrics => "application/openmetrics-text; version=1.0.0; charset=utf-8",
66 }
67 }
68
69 pub fn as_str(self) -> &'static str {
71 match self {
72 Format::Text => "text",
73 Format::OpenMetrics => "openmetrics",
74 }
75 }
76}
77
78pub fn negotiate(accept: Option<&str>) -> Format {
85 let Some(accept) = accept else {
86 return Format::Text;
87 };
88 let mut best_openmetrics: f32 = -1.0;
89 let mut best_other: f32 = -1.0;
90 for entry in accept.split(',') {
91 let mut parts = entry.split(';');
92 let media = parts.next().unwrap_or("").trim().to_ascii_lowercase();
93 let mut q: f32 = 1.0;
94 for param in parts {
95 if let Some((k, v)) = param.split_once('=') {
96 if k.trim().eq_ignore_ascii_case("q") {
97 q = v.trim().parse().unwrap_or(0.0);
98 }
99 }
100 }
101 if !q.is_finite() || q <= 0.0 {
102 continue;
103 }
104 if media == "application/openmetrics-text" {
105 best_openmetrics = best_openmetrics.max(q);
106 } else if media == "text/plain" || media == "*/*" || media == "text/*" {
107 best_other = best_other.max(q);
108 }
109 }
110 if best_openmetrics > best_other {
111 Format::OpenMetrics
112 } else {
113 Format::Text
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum MetricType {
120 Counter,
121 Gauge,
122 Histogram,
123 Summary,
124 Untyped,
125}
126
127impl MetricType {
128 fn parse(s: &str) -> Option<Self> {
129 match s.to_ascii_lowercase().as_str() {
130 "counter" => Some(Self::Counter),
131 "gauge" => Some(Self::Gauge),
132 "histogram" => Some(Self::Histogram),
133 "summary" => Some(Self::Summary),
134 "untyped" | "unknown" => Some(Self::Untyped),
135 _ => None,
136 }
137 }
138
139 fn type_word(self, format: Format) -> &'static str {
140 match (self, format) {
141 (Self::Counter, _) => "counter",
142 (Self::Gauge, _) => "gauge",
143 (Self::Histogram, _) => "histogram",
144 (Self::Summary, _) => "summary",
145 (Self::Untyped, Format::Text) => "untyped",
146 (Self::Untyped, Format::OpenMetrics) => "unknown",
147 }
148 }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
153enum Suffix {
154 None,
156 Bucket,
157 Sum,
158 Count,
159}
160
161impl Suffix {
162 fn text(self, mtype: MetricType) -> &'static str {
163 match (self, mtype) {
164 (Suffix::None, MetricType::Counter) => "_total",
165 (Suffix::None, _) => "",
166 (Suffix::Bucket, _) => "_bucket",
167 (Suffix::Sum, _) => "_sum",
168 (Suffix::Count, _) => "_count",
169 }
170 }
171}
172
173#[derive(Clone, Debug)]
174struct Sample {
175 suffix: Suffix,
176 labels: BTreeMap<String, String>,
178 bound: Option<f64>,
180 value: f64,
181 timestamp_ms: Option<i64>,
182}
183
184#[derive(Clone, Debug)]
185struct Family {
186 base: String,
188 mtype: MetricType,
189 help: Option<String>,
190 samples: Vec<Sample>,
191}
192
193#[derive(Clone, Debug)]
195pub struct MetricFamilies {
196 families: Vec<Family>,
197}
198
199fn valid_metric_name(name: &str) -> bool {
200 let mut chars = name.chars();
201 match chars.next() {
202 Some(c) if c.is_ascii_alphabetic() || c == '_' || c == ':' => {}
203 _ => return false,
204 }
205 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')
206}
207
208fn valid_label_name(name: &str) -> bool {
209 let mut chars = name.chars();
210 match chars.next() {
211 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
212 _ => return false,
213 }
214 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
215}
216
217fn parse_value(v: &Value) -> Option<f64> {
219 match v {
220 Value::Number(n) => n.as_f64(),
221 Value::String(s) => match s.trim() {
222 "NaN" | "nan" => Some(f64::NAN),
223 "+Inf" | "Inf" | "inf" | "+inf" => Some(f64::INFINITY),
224 "-Inf" | "-inf" => Some(f64::NEG_INFINITY),
225 other => other.parse::<f64>().ok(),
226 },
227 _ => None,
228 }
229}
230
231fn format_value(v: f64) -> String {
233 if v.is_nan() {
234 "NaN".to_string()
235 } else if v == f64::INFINITY {
236 "+Inf".to_string()
237 } else if v == f64::NEG_INFINITY {
238 "-Inf".to_string()
239 } else {
240 format!("{v}")
243 }
244}
245
246fn format_bound(v: f64) -> String {
249 if v == f64::INFINITY {
250 "+Inf".to_string()
251 } else if v == f64::NEG_INFINITY {
252 "-Inf".to_string()
253 } else if v.fract() == 0.0 && v.abs() < 1e15 {
254 format!("{v:.1}")
255 } else {
256 format!("{v}")
257 }
258}
259
260fn escape_label_value(s: &str) -> String {
261 let mut out = String::with_capacity(s.len());
262 for c in s.chars() {
263 match c {
264 '\\' => out.push_str("\\\\"),
265 '"' => out.push_str("\\\""),
266 '\n' => out.push_str("\\n"),
267 other => out.push(other),
268 }
269 }
270 out
271}
272
273fn escape_help(s: &str, format: Format) -> String {
274 let mut out = String::with_capacity(s.len());
275 for c in s.chars() {
276 match c {
277 '\\' => out.push_str("\\\\"),
278 '\n' => out.push_str("\\n"),
279 '"' if format == Format::OpenMetrics => out.push_str("\\\""),
280 other => out.push(other),
281 }
282 }
283 out
284}
285
286impl MetricFamilies {
287 pub fn parse(metrics: &Value) -> Result<Self, String> {
292 let list = metrics
293 .as_array()
294 .ok_or("'metrics' must be an array of metric families")?;
295
296 let mut families = Vec::with_capacity(list.len());
297 let mut seen_bases: BTreeSet<String> = BTreeSet::new();
298 let mut total_samples = 0usize;
299
300 for (i, raw) in list.iter().enumerate() {
301 let name = raw
302 .get("name")
303 .and_then(Value::as_str)
304 .ok_or_else(|| format!("metrics[{i}] is missing a string 'name'"))?;
305 if name.len() > MAX_NAME_LEN {
306 return Err(format!(
307 "metric name '{}…' is longer than {MAX_NAME_LEN} characters",
308 &name[..name
309 .char_indices()
310 .nth(32)
311 .map(|(b, _)| b)
312 .unwrap_or(name.len())]
313 ));
314 }
315 if !valid_metric_name(name) {
316 return Err(format!(
317 "metric name '{name}' is invalid: it must match [a-zA-Z_:][a-zA-Z0-9_:]*"
318 ));
319 }
320 let type_str = raw.get("type").and_then(Value::as_str).unwrap_or("untyped");
321 let mtype = MetricType::parse(type_str).ok_or_else(|| {
322 format!(
323 "metric '{name}' has type '{type_str}'; it must be one of counter, gauge, \
324 histogram, summary, untyped"
325 )
326 })?;
327 let help = match raw.get("help") {
328 None | Some(Value::Null) => None,
329 Some(Value::String(s)) if s.len() > MAX_TEXT_LEN => {
330 return Err(format!(
331 "metric '{name}' help text is longer than {MAX_TEXT_LEN} bytes"
332 ))
333 }
334 Some(Value::String(s)) => Some(s.clone()),
335 Some(_) => return Err(format!("metric '{name}' help must be a string")),
336 };
337
338 let base = match mtype {
339 MetricType::Counter => name.strip_suffix("_total").unwrap_or(name).to_string(),
340 _ => name.to_string(),
341 };
342 if base.is_empty() {
343 return Err(format!("metric name '{name}' is only a suffix"));
344 }
345 if !seen_bases.insert(base.clone()) {
346 return Err(format!(
347 "metric family '{name}' appears twice; put all of its samples in one entry"
348 ));
349 }
350
351 let raw_samples = match raw.get("samples") {
352 None | Some(Value::Null) => Vec::new(),
353 Some(Value::Array(a)) => a.clone(),
354 Some(_) => return Err(format!("metric '{name}' samples must be an array")),
355 };
356 total_samples = total_samples.saturating_add(raw_samples.len());
357 if total_samples > MAX_SAMPLES {
358 return Err(format!(
359 "more than {MAX_SAMPLES} samples in one answer; send fewer series"
360 ));
361 }
362
363 let mut samples = Vec::with_capacity(raw_samples.len());
364 for (j, s) in raw_samples.iter().enumerate() {
365 samples.push(parse_sample(name, mtype, j, s)?);
366 }
367
368 let mut family = Family {
369 base,
370 mtype,
371 help,
372 samples,
373 };
374 normalise(&mut family, name)?;
375 families.push(family);
376 }
377
378 let mut owner: BTreeMap<String, String> = BTreeMap::new();
380 for f in &families {
381 for series in series_names(f) {
382 if let Some(other) = owner.insert(series.clone(), f.base.clone()) {
383 if other != f.base {
384 return Err(format!(
385 "sample name '{series}' is produced by both '{other}' and '{}'; \
386 rename one of them",
387 f.base
388 ));
389 }
390 }
391 }
392 }
393
394 Ok(Self { families })
395 }
396
397 pub fn family_count(&self) -> usize {
399 self.families.len()
400 }
401
402 pub fn sample_count(&self) -> usize {
404 self.families.iter().map(|f| f.samples.len()).sum()
405 }
406
407 pub fn render(&self, format: Format) -> String {
409 let mut out = String::new();
410 for f in &self.families {
411 let family_name = match (f.mtype, format) {
412 (MetricType::Counter, Format::Text) => format!("{}_total", f.base),
413 _ => f.base.clone(),
414 };
415 if let Some(help) = &f.help {
416 out.push_str("# HELP ");
417 out.push_str(&family_name);
418 out.push(' ');
419 out.push_str(&escape_help(help, format));
420 out.push('\n');
421 }
422 out.push_str("# TYPE ");
423 out.push_str(&family_name);
424 out.push(' ');
425 out.push_str(f.mtype.type_word(format));
426 out.push('\n');
427
428 for s in &f.samples {
429 out.push_str(&f.base);
430 out.push_str(s.suffix.text(f.mtype));
431 let mut labels: Vec<(String, String)> = s
432 .labels
433 .iter()
434 .map(|(k, v)| (k.clone(), escape_label_value(v)))
435 .collect();
436 if let Some(bound) = s.bound {
437 let key = if f.mtype == MetricType::Histogram {
438 "le"
439 } else {
440 "quantile"
441 };
442 labels.push((key.to_string(), format_bound(bound)));
443 }
444 if !labels.is_empty() {
445 out.push('{');
446 for (idx, (k, v)) in labels.iter().enumerate() {
447 if idx > 0 {
448 out.push(',');
449 }
450 out.push_str(k);
451 out.push_str("=\"");
452 out.push_str(v);
453 out.push('"');
454 }
455 out.push('}');
456 }
457 out.push(' ');
458 out.push_str(&format_value(s.value));
459 if let Some(ts) = s.timestamp_ms {
460 out.push(' ');
461 match format {
462 Format::Text => out.push_str(&ts.to_string()),
463 Format::OpenMetrics => {
465 let secs = ts.div_euclid(1000);
466 let millis = ts.rem_euclid(1000);
467 out.push_str(&format!("{secs}.{millis:03}"));
468 }
469 }
470 }
471 out.push('\n');
472 }
473 }
474 if format == Format::OpenMetrics {
475 out.push_str("# EOF\n");
476 }
477 out
478 }
479}
480
481fn series_names(f: &Family) -> Vec<String> {
483 let suffixes: &[&str] = match f.mtype {
484 MetricType::Counter => &["_total"],
485 MetricType::Gauge | MetricType::Untyped => &[""],
486 MetricType::Histogram => &["_bucket", "_sum", "_count"],
487 MetricType::Summary => &["", "_sum", "_count"],
488 };
489 suffixes
490 .iter()
491 .map(|s| format!("{}{}", f.base, s))
492 .collect()
493}
494
495fn parse_sample(name: &str, mtype: MetricType, j: usize, s: &Value) -> Result<Sample, String> {
496 let where_ = || format!("metric '{name}' sample {j}");
497
498 let value = s
499 .get("value")
500 .and_then(parse_value)
501 .ok_or_else(|| format!("{} has no numeric 'value'", where_()))?;
502
503 let suffix_str = s.get("suffix").and_then(Value::as_str).unwrap_or("");
504 let suffix = match (mtype, suffix_str) {
505 (MetricType::Counter, "" | "_total") => Suffix::None,
506 (MetricType::Gauge | MetricType::Untyped, "") => Suffix::None,
507 (MetricType::Histogram, "_bucket") => Suffix::Bucket,
508 (MetricType::Histogram | MetricType::Summary, "_sum") => Suffix::Sum,
509 (MetricType::Histogram | MetricType::Summary, "_count") => Suffix::Count,
510 (MetricType::Summary, "") => Suffix::None,
511 (MetricType::Histogram, "") => {
512 return Err(format!(
513 "{} needs a suffix: a histogram sample is _bucket (with an 'le' label), _sum or \
514 _count",
515 where_()
516 ))
517 }
518 (_, other) => {
519 return Err(format!(
520 "{} has suffix '{other}', which a {} does not have",
521 where_(),
522 mtype.type_word(Format::Text)
523 ))
524 }
525 };
526
527 let mut labels = BTreeMap::new();
528 let mut bound: Option<f64> = None;
529 match s.get("labels") {
530 None | Some(Value::Null) => {}
531 Some(Value::Object(map)) => {
532 for (k, v) in map {
533 if k.len() > MAX_NAME_LEN || !valid_label_name(k) {
534 return Err(format!(
535 "{}: label name '{k}' is invalid: it must match [a-zA-Z_][a-zA-Z0-9_]*",
536 where_()
537 ));
538 }
539 if k.starts_with("__") {
540 return Err(format!(
541 "{}: label name '{k}' begins with '__', which Prometheus reserves",
542 where_()
543 ));
544 }
545 let text = match v {
546 Value::String(s) => s.clone(),
547 Value::Number(n) => n.to_string(),
548 Value::Bool(b) => b.to_string(),
549 _ => {
550 return Err(format!(
551 "{}: label '{k}' must be a string, number or boolean",
552 where_()
553 ))
554 }
555 };
556 if text.len() > MAX_TEXT_LEN {
557 return Err(format!(
558 "{}: label '{k}' is longer than {MAX_TEXT_LEN} bytes",
559 where_()
560 ));
561 }
562 let is_le = k == "le" && mtype == MetricType::Histogram;
563 let is_quantile = k == "quantile" && mtype == MetricType::Summary;
564 if is_le || is_quantile {
565 let b = parse_value(v).filter(|b| !b.is_nan()).ok_or_else(|| {
566 format!("{}: '{k}' must be a number or \"+Inf\"", where_())
567 })?;
568 bound = Some(b);
569 } else if k == "le" || k == "quantile" {
570 return Err(format!(
574 "{}: '{k}' is reserved for {} samples",
575 where_(),
576 if k == "le" {
577 "histogram _bucket"
578 } else {
579 "summary quantile"
580 }
581 ));
582 } else {
583 labels.insert(k.clone(), text);
584 }
585 }
586 }
587 Some(_) => return Err(format!("{}: 'labels' must be an object", where_())),
588 }
589
590 match (mtype, suffix) {
591 (MetricType::Histogram, Suffix::Bucket) if bound.is_none() => {
592 return Err(format!("{} is a _bucket without an 'le' label", where_()))
593 }
594 (MetricType::Histogram, Suffix::Sum | Suffix::Count) if bound.is_some() => {
595 return Err(format!(
596 "{}: 'le' belongs on _bucket samples only",
597 where_()
598 ))
599 }
600 (MetricType::Summary, Suffix::None) => match bound {
601 None => {
602 return Err(format!(
603 "{} is a summary quantile without a 'quantile' label",
604 where_()
605 ))
606 }
607 Some(q) if !(0.0..=1.0).contains(&q) => {
608 return Err(format!("{}: quantile {q} is outside 0..1", where_()))
609 }
610 _ => {}
611 },
612 (MetricType::Summary, Suffix::Sum | Suffix::Count) if bound.is_some() => {
613 return Err(format!(
614 "{}: 'quantile' belongs on quantile samples only",
615 where_()
616 ))
617 }
618 _ => {}
619 }
620
621 let counts_things = matches!(
622 (mtype, suffix),
623 (MetricType::Counter, _) | (_, Suffix::Bucket) | (_, Suffix::Count)
624 );
625 if counts_things && (value.is_nan() || value < 0.0) {
626 return Err(format!(
627 "{}: value {} is not a valid count; counters, buckets and _count are non-negative",
628 where_(),
629 format_value(value)
630 ));
631 }
632
633 let timestamp_ms = match s.get("timestamp_ms") {
634 None | Some(Value::Null) => None,
635 Some(v) => Some(v.as_i64().ok_or_else(|| {
636 format!(
637 "{}: timestamp_ms must be an integer (milliseconds since the epoch)",
638 where_()
639 )
640 })?),
641 };
642
643 Ok(Sample {
644 suffix,
645 labels,
646 bound,
647 value,
648 timestamp_ms,
649 })
650}
651
652fn normalise(f: &mut Family, name: &str) -> Result<(), String> {
654 let mut seen = BTreeSet::new();
656 for s in &f.samples {
657 let key = (s.suffix, s.labels.clone(), s.bound.map(|b| b.to_bits()));
658 if !seen.insert(key) {
659 return Err(format!(
660 "metric '{name}' has two samples with the same labels{}; each series may \
661 appear once",
662 match s.bound {
663 Some(b) => format!(" and bound {}", format_bound(b)),
664 None => String::new(),
665 }
666 ));
667 }
668 }
669
670 if f.mtype != MetricType::Histogram && f.mtype != MetricType::Summary {
671 return Ok(());
672 }
673
674 let mut groups: BTreeMap<BTreeMap<String, String>, Vec<Sample>> = BTreeMap::new();
677 let mut order: Vec<BTreeMap<String, String>> = Vec::new();
678 for s in f.samples.drain(..) {
679 if !groups.contains_key(&s.labels) {
680 order.push(s.labels.clone());
681 }
682 groups.entry(s.labels.clone()).or_default().push(s);
683 }
684
685 let mut out = Vec::new();
686 for labels in order {
687 let mut group = groups.remove(&labels).unwrap_or_default();
688 group.sort_by(|a, b| {
689 a.suffix.cmp(&b.suffix).then(
690 a.bound
691 .unwrap_or(0.0)
692 .partial_cmp(&b.bound.unwrap_or(0.0))
693 .unwrap_or(std::cmp::Ordering::Equal),
694 )
695 });
696
697 if f.mtype == MetricType::Histogram {
698 let buckets: Vec<&Sample> = group
699 .iter()
700 .filter(|s| s.suffix == Suffix::Bucket)
701 .collect();
702 let count = group.iter().find(|s| s.suffix == Suffix::Count).cloned();
703 let mut previous = 0.0f64;
704 for b in &buckets {
705 if b.value < previous {
706 return Err(format!(
707 "histogram '{name}' bucket le={} holds {} but a smaller bucket holds \
708 {}; buckets are cumulative, so each must be at least the one before",
709 format_bound(b.bound.unwrap_or(0.0)),
710 format_value(b.value),
711 format_value(previous)
712 ));
713 }
714 previous = b.value;
715 }
716 let has_inf = buckets
717 .last()
718 .is_some_and(|b| b.bound == Some(f64::INFINITY));
719 let inf_value = if has_inf {
720 previous
721 } else {
722 let v = count.as_ref().map(|c| c.value).unwrap_or(previous);
723 if v < previous {
724 return Err(format!(
725 "histogram '{name}' _count {} is smaller than its largest bucket {}",
726 format_value(v),
727 format_value(previous)
728 ));
729 }
730 let template = group.iter().find(|s| s.suffix == Suffix::Bucket).cloned();
731 if let Some(mut inf) = template.or_else(|| count.clone()) {
732 inf.suffix = Suffix::Bucket;
733 inf.bound = Some(f64::INFINITY);
734 inf.value = v;
735 let insert_at = group
736 .iter()
737 .position(|s| s.suffix != Suffix::Bucket)
738 .unwrap_or(group.len());
739 group.insert(insert_at, inf);
740 v
741 } else {
742 return Err(format!(
745 "histogram '{name}' has a _sum but no buckets and no _count"
746 ));
747 }
748 };
749 match count {
750 Some(c) if c.value != inf_value => {
751 return Err(format!(
752 "histogram '{name}' _count is {} but its +Inf bucket is {}; they are \
753 the same number by definition",
754 format_value(c.value),
755 format_value(inf_value)
756 ))
757 }
758 Some(_) => {}
759 None => {
760 let mut c = group
761 .iter()
762 .find(|s| s.suffix == Suffix::Bucket)
763 .cloned()
764 .expect("a +Inf bucket was just ensured");
765 c.suffix = Suffix::Count;
766 c.bound = None;
767 c.value = inf_value;
768 group.push(c);
769 }
770 }
771 }
772 out.extend(group);
773 }
774 f.samples = out;
775 Ok(())
776}