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, IntCounter, IntCounterVec, IntGauge, 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    /// `cache_bytes` - total size of the on-disk mirror cache, maintained
26    /// incrementally as mirrors are added, refreshed, and evicted. Populated only
27    /// when a cap is configured (`--cache-max-mb`); with no cap it stays `0`.
28    cache_bytes: IntGauge,
29    /// `cache_mirrors` - number of cached mirrors (same caveat as `cache_bytes`).
30    cache_mirrors: IntGauge,
31    /// `evictions_total` - idle mirrors evicted to keep the cache under the cap.
32    evictions: IntCounter,
33}
34
35impl Metrics {
36    pub fn new() -> Self {
37        let registry = Registry::new();
38        let requests = IntCounterVec::new(
39            Opts::new("gitcacheproxy_requests_total", "Git requests served"),
40            &["kind", "result", "repo"],
41        )
42        .expect("valid metric");
43        let upstream = IntCounterVec::new(
44            Opts::new(
45                "gitcacheproxy_upstream_ops_total",
46                "Upstream clone/fetch operations",
47            ),
48            &["op", "result", "repo"],
49        )
50        .expect("valid metric");
51        let cache_bytes = IntGauge::new(
52            "gitcacheproxy_cache_bytes",
53            "Total size of the on-disk mirror cache in bytes",
54        )
55        .expect("valid metric");
56        let cache_mirrors = IntGauge::new(
57            "gitcacheproxy_cache_mirrors",
58            "Number of cached mirrors on disk",
59        )
60        .expect("valid metric");
61        let evictions = IntCounter::new(
62            "gitcacheproxy_evictions_total",
63            "Idle mirrors evicted to keep the cache under the configured cap",
64        )
65        .expect("valid metric");
66        registry
67            .register(Box::new(requests.clone()))
68            .expect("register requests");
69        registry
70            .register(Box::new(upstream.clone()))
71            .expect("register upstream");
72        registry
73            .register(Box::new(cache_bytes.clone()))
74            .expect("register cache_bytes");
75        registry
76            .register(Box::new(cache_mirrors.clone()))
77            .expect("register cache_mirrors");
78        registry
79            .register(Box::new(evictions.clone()))
80            .expect("register evictions");
81        Self {
82            registry,
83            requests,
84            upstream,
85            cache_bytes,
86            cache_mirrors,
87            evictions,
88        }
89    }
90
91    /// Record the outcome of a client request. `repo` is the served repo path on
92    /// success and `-` on any failure, so unbounded client-supplied paths cannot
93    /// inflate label cardinality. `kind` is `info_refs`, `upload_pack`, `auth` or
94    /// `receive_pack`; `result` is `ok`, `error`, `upstream_error`,
95    /// `unauthorized` or `rejected`.
96    pub fn record_request(&self, kind: &str, result: &str, repo: &str) {
97        self.requests.with_label_values(&[kind, result, repo]).inc();
98    }
99
100    /// Record an upstream clone/fetch. Errors are recorded too (`result =
101    /// "error"`); pass the repo path on success and `-` on error, matching
102    /// `record_request`.
103    pub fn record_upstream(&self, op: &str, result: &str, repo: &str) {
104        self.upstream.with_label_values(&[op, result, repo]).inc();
105    }
106
107    /// Refresh the cache-size gauges from the eviction index.
108    pub fn set_cache_size(&self, bytes: u64, mirrors: usize) {
109        self.cache_bytes.set(bytes as i64);
110        self.cache_mirrors.set(mirrors as i64);
111    }
112
113    /// Record one evicted mirror.
114    pub fn record_eviction(&self) {
115        self.evictions.inc();
116    }
117
118    pub fn gather(&self) -> String {
119        let mut buf = Vec::new();
120        let enc = TextEncoder::new();
121        // encode never fails for the text format into a Vec.
122        let _ = enc.encode(&self.registry.gather(), &mut buf);
123        String::from_utf8_lossy(&buf).into_owned()
124    }
125}
126
127impl Default for Metrics {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn gather_renders_recorded_series() {
139        let m = Metrics::new();
140        m.record_request("info_refs", "ok", "group/foo.git");
141        m.record_request("upload_pack", "error", "group/bar.git");
142        m.record_upstream("fetch", "ok", "group/foo.git");
143        m.record_upstream("clone", "error", "group/bar.git");
144        m.set_cache_size(2048, 3);
145        m.record_eviction();
146        m.record_eviction();
147
148        let out = m.gather();
149        assert!(out.contains("gitcacheproxy_cache_bytes 2048"));
150        assert!(out.contains("gitcacheproxy_cache_mirrors 3"));
151        assert!(out.contains("gitcacheproxy_evictions_total 2"));
152        assert!(out.contains(
153            r#"gitcacheproxy_requests_total{kind="info_refs",repo="group/foo.git",result="ok"} 1"#
154        ));
155        assert!(out.contains(
156            r#"gitcacheproxy_requests_total{kind="upload_pack",repo="group/bar.git",result="error"} 1"#
157        ));
158        assert!(out.contains(r#"op="fetch",repo="group/foo.git",result="ok"#));
159        assert!(out.contains(r#"op="clone",repo="group/bar.git",result="error"#));
160    }
161}