Skip to main content

git_cache_proxy/
metrics.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Prometheus metrics.
3//!
4//! Cardinality note: both `requests_total` and `upstream_ops_total` carry a
5//! `repo` label so operators can see per-repo traffic and totals
6//! (`sum without (repo) (...)`). To keep the label set bounded, the real repo
7//! name is emitted only for operations that *succeeded* (a served request, a
8//! completed clone/fetch); every failure - failed auth, malformed path,
9//! upstream error, a resolved-but-nonexistent repo - uses a `-` sentinel. The
10//! set of successfully served repos is bounded (the repos the fleet actually
11//! clones), so a flood of distinct but doomed repo paths cannot inflate the
12//! series count.
13
14use prometheus::{Encoder, IntCounterVec, Opts, Registry, TextEncoder};
15
16pub struct Metrics {
17    pub registry: Registry,
18    /// `requests_total{kind, result, repo}` - kind = info_refs | upload_pack |
19    /// auth | receive_pack; result = ok | error | upstream_error | unauthorized |
20    /// rejected; repo = the served repo path when result = ok, else `-`.
21    requests: IntCounterVec,
22    /// `upstream_ops_total{op, result, repo}` - op = clone | fetch; result = ok |
23    /// error; repo = the repo path when result = ok, else `-`.
24    upstream: IntCounterVec,
25}
26
27impl Metrics {
28    pub fn new() -> Self {
29        let registry = Registry::new();
30        let requests = IntCounterVec::new(
31            Opts::new("gitcacheproxy_requests_total", "Git requests served"),
32            &["kind", "result", "repo"],
33        )
34        .expect("valid metric");
35        let upstream = IntCounterVec::new(
36            Opts::new(
37                "gitcacheproxy_upstream_ops_total",
38                "Upstream clone/fetch operations",
39            ),
40            &["op", "result", "repo"],
41        )
42        .expect("valid metric");
43        registry
44            .register(Box::new(requests.clone()))
45            .expect("register requests");
46        registry
47            .register(Box::new(upstream.clone()))
48            .expect("register upstream");
49        Self {
50            registry,
51            requests,
52            upstream,
53        }
54    }
55
56    /// Record the outcome of a client request. `repo` is the served repo path on
57    /// success and `-` on any failure, so unbounded client-supplied paths cannot
58    /// inflate label cardinality. `kind` is `info_refs`, `upload_pack`, `auth` or
59    /// `receive_pack`; `result` is `ok`, `error`, `upstream_error`,
60    /// `unauthorized` or `rejected`.
61    pub fn record_request(&self, kind: &str, result: &str, repo: &str) {
62        self.requests.with_label_values(&[kind, result, repo]).inc();
63    }
64
65    /// Record an upstream clone/fetch. Errors are recorded too (`result =
66    /// "error"`); pass the repo path on success and `-` on error, matching
67    /// `record_request`.
68    pub fn record_upstream(&self, op: &str, result: &str, repo: &str) {
69        self.upstream.with_label_values(&[op, result, repo]).inc();
70    }
71
72    pub fn gather(&self) -> String {
73        let mut buf = Vec::new();
74        let enc = TextEncoder::new();
75        // encode never fails for the text format into a Vec.
76        let _ = enc.encode(&self.registry.gather(), &mut buf);
77        String::from_utf8_lossy(&buf).into_owned()
78    }
79}
80
81impl Default for Metrics {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn gather_renders_recorded_series() {
93        let m = Metrics::new();
94        m.record_request("info_refs", "ok", "group/foo.git");
95        m.record_request("upload_pack", "error", "group/bar.git");
96        m.record_upstream("fetch", "ok", "group/foo.git");
97        m.record_upstream("clone", "error", "group/bar.git");
98
99        let out = m.gather();
100        assert!(out.contains(
101            r#"gitcacheproxy_requests_total{kind="info_refs",repo="group/foo.git",result="ok"} 1"#
102        ));
103        assert!(out.contains(
104            r#"gitcacheproxy_requests_total{kind="upload_pack",repo="group/bar.git",result="error"} 1"#
105        ));
106        assert!(out.contains(r#"op="fetch",repo="group/foo.git",result="ok"#));
107        assert!(out.contains(r#"op="clone",repo="group/bar.git",result="error"#));
108    }
109}