Skip to main content

rust_web_server/metrics/
exposition.rs

1//! The Prometheus protobuf exposition format (`io.prometheus.client.MetricFamily`,
2//! length-delimited), served by `GET /metrics` to a scraper that asks for it.
3//! It is the only format that carries native histograms.
4//!
5//! Families come from the text output of every other producer
6//! ([`parse_text`]: counters, gauges and classic histograms) plus
7//! `http_server_request_duration_seconds` built directly, with its classic
8//! buckets and its native (schema 3) ones side by side. Field numbers are
9//! those of prometheus/client_model `metrics.proto`.
10
11use super::http;
12
13/// The `Content-Type` of the protobuf format, also what a scraper's `Accept` names.
14pub const PROTOBUF_CONTENT_TYPE: &str = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited";
15
16/// Whether an `Accept` header asks for the protobuf format (with a `q` above 0).
17pub fn accepts_protobuf(accept: &str) -> bool {
18    accept.split(',').any(|range| {
19        let mut params = range.split(';').map(str::trim);
20        if !params.next().is_some_and(|t| t.eq_ignore_ascii_case("application/vnd.google.protobuf")) {
21            return false;
22        }
23        let mut message = false;
24        let mut q = 1.0;
25        for param in params {
26            match param.split_once('=') {
27                Some(("proto", v)) => message = v == "io.prometheus.client.MetricFamily",
28                Some(("q", v)) => q = v.parse().unwrap_or(0.0),
29                _ => {}
30            }
31        }
32        message && q > 0.0
33    })
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub(crate) enum Kind {
38    Counter = 0,
39    Gauge = 1,
40    Untyped = 3,
41    Histogram = 4,
42}
43
44#[derive(Clone, Debug, PartialEq, Default)]
45pub(crate) struct ClassicHistogram {
46    pub(crate) count: u64,
47    pub(crate) sum: f64,
48    /// `(upper bound, cumulative count)`, increasing.
49    pub(crate) buckets: Vec<(f64, u64)>,
50}
51
52#[derive(Clone, Debug, PartialEq)]
53pub(crate) enum Value {
54    Number(f64),
55    Histogram(ClassicHistogram, Option<http::Histogram>),
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub(crate) struct Sample {
60    pub(crate) labels: Vec<(String, String)>,
61    pub(crate) value: Value,
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub(crate) struct Family {
66    pub(crate) name: String,
67    pub(crate) help: String,
68    pub(crate) kind: Kind,
69    pub(crate) samples: Vec<Sample>,
70}
71
72/// Every metric, protobuf-encoded.
73pub fn protobuf() -> Vec<u8> {
74    let mut families = parse_text(&super::prometheus_text_without_http());
75    families.push(http_family());
76    families.retain(|f| !f.samples.is_empty());
77    encode(&families)
78}
79
80fn http_family() -> Family {
81    let samples = http::snapshot()
82        .into_iter()
83        .map(|(series, histogram)| {
84            let mut cumulative = 0;
85            let buckets = http::BOUNDS
86                .iter()
87                .enumerate()
88                .map(|(i, bound)| {
89                    cumulative += histogram.counts[i];
90                    (*bound, cumulative)
91                })
92                .collect();
93            let classic = ClassicHistogram { count: histogram.count, sum: histogram.sum, buckets };
94            Sample { labels: http::labels(&series), value: Value::Histogram(classic, Some(histogram)) }
95        })
96        .collect();
97    Family { name: http::NAME.to_string(), help: http::HELP.to_string(), kind: Kind::Histogram, samples }
98}
99
100// ── text → families ───────────────────────────────────────────────────────────
101
102/// Parse rws's own text output (one `# HELP`/`# TYPE` per family, samples
103/// after). Lines it doesn't understand are skipped.
104pub(crate) fn parse_text(text: &str) -> Vec<Family> {
105    let mut families: Vec<Family> = Vec::new();
106    for line in text.lines() {
107        let line = line.trim();
108        if line.is_empty() {
109            continue;
110        }
111        if let Some(rest) = line.strip_prefix("# HELP ") {
112            let (name, help) = rest.split_once(' ').unwrap_or((rest, ""));
113            family_named(&mut families, name).help = help.replace("\\n", "\n").replace("\\\\", "\\");
114            continue;
115        }
116        if let Some(rest) = line.strip_prefix("# TYPE ") {
117            let (name, kind) = rest.split_once(' ').unwrap_or((rest, ""));
118            family_named(&mut families, name).kind = match kind.trim() {
119                "counter" => Kind::Counter,
120                "gauge" => Kind::Gauge,
121                "histogram" => Kind::Histogram,
122                _ => Kind::Untyped,
123            };
124            continue;
125        }
126        if line.starts_with('#') {
127            continue;
128        }
129        let Some((name, labels, value)) = parse_sample(line) else { continue };
130        add_sample(&mut families, name, labels, value);
131    }
132    families
133}
134
135fn family_named<'a>(families: &'a mut Vec<Family>, name: &str) -> &'a mut Family {
136    match families.iter().position(|f| f.name == name) {
137        Some(i) => &mut families[i],
138        None => {
139            families.push(Family { name: name.to_string(), help: String::new(), kind: Kind::Untyped, samples: Vec::new() });
140            families.last_mut().unwrap()
141        }
142    }
143}
144
145fn add_sample(families: &mut Vec<Family>, name: &str, mut labels: Vec<(String, String)>, value: f64) {
146    // A histogram's `_bucket`/`_sum`/`_count` lines belong to its family.
147    for suffix in ["_bucket", "_sum", "_count"] {
148        let Some(base) = name.strip_suffix(suffix) else { continue };
149        let Some(family) = families.iter_mut().find(|f| f.name == base && f.kind == Kind::Histogram) else { continue };
150        let le = labels.iter().position(|(k, _)| k == "le").map(|i| labels.remove(i).1);
151        let sample = match family.samples.iter_mut().position(|s| s.labels == labels) {
152            Some(i) => &mut family.samples[i],
153            None => {
154                family.samples.push(Sample { labels, value: Value::Histogram(ClassicHistogram::default(), None) });
155                family.samples.last_mut().unwrap()
156            }
157        };
158        let Value::Histogram(classic, _) = &mut sample.value else { return };
159        match suffix {
160            "_sum" => classic.sum = value,
161            "_count" => classic.count = value as u64,
162            _ => {
163                let bound = match le.as_deref() {
164                    Some("+Inf") | None => return, // implied by the count
165                    Some(le) => le.parse().unwrap_or(f64::INFINITY),
166                };
167                classic.buckets.push((bound, value as u64));
168            }
169        }
170        return;
171    }
172    let family = family_named(families, name);
173    family.samples.push(Sample { labels, value: Value::Number(value) });
174}
175
176/// `name{a="x",b="y"} value` → its parts; label values unescaped.
177fn parse_sample(line: &str) -> Option<(&str, Vec<(String, String)>, f64)> {
178    let name_end = line.find(['{', ' '])?;
179    let name = &line[..name_end];
180    let mut labels = Vec::new();
181    let mut rest = &line[name_end..];
182    if let Some(after) = rest.strip_prefix('{') {
183        let mut chars = after.char_indices();
184        let mut key = String::new();
185        let close;
186        loop {
187            let (i, c) = chars.next()?;
188            match c {
189                '}' => {
190                    close = i;
191                    break;
192                }
193                ',' | ' ' => {}
194                '=' => {
195                    if chars.next()?.1 != '"' {
196                        return None;
197                    }
198                    let mut value = String::new();
199                    loop {
200                        match chars.next()?.1 {
201                            '"' => break,
202                            '\\' => match chars.next()?.1 {
203                                'n' => value.push('\n'),
204                                other => value.push(other),
205                            },
206                            other => value.push(other),
207                        }
208                    }
209                    labels.push((std::mem::take(&mut key), value));
210                }
211                other => key.push(other),
212            }
213        }
214        rest = &after[close + 1..];
215    }
216    let value = rest.split_whitespace().next()?;
217    let value = match value {
218        "+Inf" => f64::INFINITY,
219        "-Inf" => f64::NEG_INFINITY,
220        "NaN" => f64::NAN,
221        v => v.parse().ok()?,
222    };
223    Some((name, labels, value))
224}
225
226// ── protobuf encoding ─────────────────────────────────────────────────────────
227
228pub(crate) fn encode(families: &[Family]) -> Vec<u8> {
229    let mut out = Vec::new();
230    for family in families {
231        let mut message = Vec::new();
232        string_field(&mut message, 1, &family.name);
233        if !family.help.is_empty() {
234            string_field(&mut message, 2, &family.help);
235        }
236        varint_field(&mut message, 3, family.kind as u64);
237        for sample in &family.samples {
238            bytes_field(&mut message, 4, &metric(family.kind, sample));
239        }
240        varint(&mut out, message.len() as u64);
241        out.extend_from_slice(&message);
242    }
243    out
244}
245
246fn metric(kind: Kind, sample: &Sample) -> Vec<u8> {
247    let mut m = Vec::new();
248    for (name, value) in &sample.labels {
249        let mut pair = Vec::new();
250        string_field(&mut pair, 1, name);
251        string_field(&mut pair, 2, value);
252        bytes_field(&mut m, 1, &pair);
253    }
254    match &sample.value {
255        Value::Number(v) => {
256            let mut inner = Vec::new();
257            double_field(&mut inner, 1, *v);
258            let field = match kind {
259                Kind::Counter => 3,
260                Kind::Gauge => 2,
261                _ => 5, // untyped
262            };
263            bytes_field(&mut m, field, &inner);
264        }
265        Value::Histogram(classic, native) => bytes_field(&mut m, 7, &histogram(classic, native.as_ref())),
266    }
267    m
268}
269
270fn histogram(classic: &ClassicHistogram, native: Option<&http::Histogram>) -> Vec<u8> {
271    let mut h = Vec::new();
272    varint_field(&mut h, 1, classic.count);
273    double_field(&mut h, 2, classic.sum);
274    for (bound, cumulative) in &classic.buckets {
275        let mut bucket = Vec::new();
276        varint_field(&mut bucket, 1, *cumulative);
277        double_field(&mut bucket, 2, *bound);
278        bytes_field(&mut h, 3, &bucket);
279    }
280    if let Some(native) = native {
281        varint_field(&mut h, 5, zigzag(http::SCHEMA as i64));
282        double_field(&mut h, 6, http::ZERO_THRESHOLD);
283        varint_field(&mut h, 7, native.zero_count);
284        let (spans, deltas) = native.native_spans();
285        for (offset, length) in &spans {
286            let mut span = Vec::new();
287            varint_field(&mut span, 1, zigzag(*offset as i64));
288            varint_field(&mut span, 2, *length as u64);
289            bytes_field(&mut h, 12, &span);
290        }
291        if !deltas.is_empty() {
292            let mut packed = Vec::new();
293            for delta in &deltas {
294                varint(&mut packed, zigzag(*delta));
295            }
296            bytes_field(&mut h, 13, &packed);
297        }
298        if spans.is_empty() && native.zero_count == 0 {
299            // No observation yet: an empty span marks it as native anyway
300            // (client_golang does the same).
301            bytes_field(&mut h, 12, &[0x08, 0x00, 0x10, 0x00]);
302        }
303    }
304    h
305}
306
307fn zigzag(v: i64) -> u64 {
308    ((v << 1) ^ (v >> 63)) as u64
309}
310
311fn varint(out: &mut Vec<u8>, mut v: u64) {
312    while v >= 0x80 {
313        out.push((v as u8) | 0x80);
314        v >>= 7;
315    }
316    out.push(v as u8);
317}
318
319fn varint_field(out: &mut Vec<u8>, field: u32, v: u64) {
320    varint(out, (field as u64) << 3);
321    varint(out, v);
322}
323
324fn double_field(out: &mut Vec<u8>, field: u32, v: f64) {
325    varint(out, ((field as u64) << 3) | 1);
326    out.extend_from_slice(&v.to_le_bytes());
327}
328
329fn bytes_field(out: &mut Vec<u8>, field: u32, bytes: &[u8]) {
330    varint(out, ((field as u64) << 3) | 2);
331    varint(out, bytes.len() as u64);
332    out.extend_from_slice(bytes);
333}
334
335fn string_field(out: &mut Vec<u8>, field: u32, s: &str) {
336    bytes_field(out, field, s.as_bytes());
337}
338
339#[cfg(test)]
340mod tests;