1use prometheus::{
15 Encoder, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge, Opts, Registry,
16 TextEncoder,
17};
18
19const DURATION_BUCKETS: &[f64] = &[
22 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0,
23];
24
25pub struct Metrics {
26 pub registry: Registry,
27 requests: IntCounterVec,
31 upstream: IntCounterVec,
34 cache_bytes: IntGauge,
38 cache_mirrors: IntGauge,
40 evictions: IntCounter,
42 upstream_duration: HistogramVec,
45 serve_duration: HistogramVec,
49}
50
51impl Metrics {
52 pub fn new() -> Self {
53 let registry = Registry::new();
54 let requests = IntCounterVec::new(
55 Opts::new("gitcacheproxy_requests_total", "Git requests served"),
56 &["kind", "result", "repo"],
57 )
58 .expect("valid metric");
59 let upstream = IntCounterVec::new(
60 Opts::new(
61 "gitcacheproxy_upstream_ops_total",
62 "Upstream clone/fetch operations",
63 ),
64 &["op", "result", "repo"],
65 )
66 .expect("valid metric");
67 let cache_bytes = IntGauge::new(
68 "gitcacheproxy_cache_bytes",
69 "Total size of the on-disk mirror cache in bytes",
70 )
71 .expect("valid metric");
72 let cache_mirrors = IntGauge::new(
73 "gitcacheproxy_cache_mirrors",
74 "Number of cached mirrors on disk",
75 )
76 .expect("valid metric");
77 let evictions = IntCounter::new(
78 "gitcacheproxy_evictions_total",
79 "Idle mirrors evicted to keep the cache under the configured cap",
80 )
81 .expect("valid metric");
82 let upstream_duration = HistogramVec::new(
83 HistogramOpts::new(
84 "gitcacheproxy_upstream_duration_seconds",
85 "Upstream clone/fetch duration in seconds",
86 )
87 .buckets(DURATION_BUCKETS.to_vec()),
88 &["op", "repo"],
89 )
90 .expect("valid metric");
91 let serve_duration = HistogramVec::new(
92 HistogramOpts::new(
93 "gitcacheproxy_serve_duration_seconds",
94 "Client serve duration in seconds (info/refs advertisement, upload-pack stream)",
95 )
96 .buckets(DURATION_BUCKETS.to_vec()),
97 &["kind", "repo"],
98 )
99 .expect("valid metric");
100 registry
101 .register(Box::new(requests.clone()))
102 .expect("register requests");
103 registry
104 .register(Box::new(upstream.clone()))
105 .expect("register upstream");
106 registry
107 .register(Box::new(cache_bytes.clone()))
108 .expect("register cache_bytes");
109 registry
110 .register(Box::new(cache_mirrors.clone()))
111 .expect("register cache_mirrors");
112 registry
113 .register(Box::new(evictions.clone()))
114 .expect("register evictions");
115 registry
116 .register(Box::new(upstream_duration.clone()))
117 .expect("register upstream_duration");
118 registry
119 .register(Box::new(serve_duration.clone()))
120 .expect("register serve_duration");
121 Self {
122 registry,
123 requests,
124 upstream,
125 cache_bytes,
126 cache_mirrors,
127 evictions,
128 upstream_duration,
129 serve_duration,
130 }
131 }
132
133 pub fn record_request(&self, kind: &str, result: &str, repo: &str) {
139 self.requests.with_label_values(&[kind, result, repo]).inc();
140 }
141
142 pub fn record_upstream(&self, op: &str, result: &str, repo: &str) {
146 self.upstream.with_label_values(&[op, result, repo]).inc();
147 }
148
149 pub fn set_cache_size(&self, bytes: u64, mirrors: usize) {
151 self.cache_bytes.set(bytes as i64);
152 self.cache_mirrors.set(mirrors as i64);
153 }
154
155 pub fn record_eviction(&self) {
157 self.evictions.inc();
158 }
159
160 pub fn observe_upstream(&self, op: &str, repo: &str, seconds: f64) {
163 self.upstream_duration
164 .with_label_values(&[op, repo])
165 .observe(seconds);
166 }
167
168 pub fn observe_serve(&self, kind: &str, repo: &str, seconds: f64) {
171 self.serve_duration
172 .with_label_values(&[kind, repo])
173 .observe(seconds);
174 }
175
176 pub fn gather(&self) -> String {
177 let mut buf = Vec::new();
178 let enc = TextEncoder::new();
179 let _ = enc.encode(&self.registry.gather(), &mut buf);
181 String::from_utf8_lossy(&buf).into_owned()
182 }
183}
184
185impl Default for Metrics {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn gather_renders_recorded_series() {
197 let m = Metrics::new();
198 m.record_request("info_refs", "ok", "group/foo.git");
199 m.record_request("upload_pack", "error", "group/bar.git");
200 m.record_upstream("fetch", "ok", "group/foo.git");
201 m.record_upstream("clone", "error", "group/bar.git");
202 m.set_cache_size(2048, 3);
203 m.record_eviction();
204 m.record_eviction();
205 m.observe_upstream("clone", "group/foo.git", 1.5);
206 m.observe_serve("upload_pack", "group/foo.git", 2.0);
207
208 let out = m.gather();
209 assert!(out.contains("gitcacheproxy_cache_bytes 2048"));
210 assert!(out.contains("gitcacheproxy_cache_mirrors 3"));
211 assert!(out.contains("gitcacheproxy_evictions_total 2"));
212 assert!(out.contains(
213 r#"gitcacheproxy_upstream_duration_seconds_count{op="clone",repo="group/foo.git"} 1"#
214 ));
215 assert!(out.contains(
216 r#"gitcacheproxy_serve_duration_seconds_count{kind="upload_pack",repo="group/foo.git"} 1"#
217 ));
218 assert!(out.contains(
219 r#"gitcacheproxy_requests_total{kind="info_refs",repo="group/foo.git",result="ok"} 1"#
220 ));
221 assert!(out.contains(
222 r#"gitcacheproxy_requests_total{kind="upload_pack",repo="group/bar.git",result="error"} 1"#
223 ));
224 assert!(out.contains(r#"op="fetch",repo="group/foo.git",result="ok"#));
225 assert!(out.contains(r#"op="clone",repo="group/bar.git",result="error"#));
226 }
227}