upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/metrics/mod.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:17:04 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 15:24:19 +0000
commitfd0c87c787d0626b3546fa571541c9c809711821 (patch)
tree934f20d973127f380b807d2bd44b25c197cf349c /src/metrics/mod.rs
parent762cd8e815e797f173f541795de774fbbf978fc3 (diff)
add prometheus metrics
Diffstat (limited to 'src/metrics/mod.rs')
-rw-r--r--src/metrics/mod.rs469
1 files changed, 469 insertions, 0 deletions
diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs
new file mode 100644
index 0000000..4a4fe57
--- /dev/null
+++ b/src/metrics/mod.rs
@@ -0,0 +1,469 @@
1//! Prometheus metrics for ngit-grasp relay.
2//!
3//! This module provides comprehensive monitoring metrics including:
4//! - WebSocket connection tracking (with privacy-preserving IP aggregation)
5//! - Git operation metrics (clone, fetch, push)
6//! - Repository bandwidth tracking (top-N only for cardinality control)
7//! - Nostr event metrics
8//!
9//! # Privacy
10//! IP addresses are NEVER exposed in metrics. The `ConnectionTracker` maintains
11//! per-IP counts internally only for abuse detection. Only aggregate counts
12//! are exposed to Prometheus.
13
14pub mod bandwidth;
15pub mod connection;
16
17use std::sync::Arc;
18use std::time::Instant;
19
20use lazy_static::lazy_static;
21use prometheus::{
22 Counter, CounterVec, Encoder, Gauge, GaugeVec, Histogram, HistogramOpts, HistogramVec, Opts,
23 Registry, TextEncoder,
24};
25
26use bandwidth::BandwidthTracker;
27use connection::ConnectionTracker;
28
29lazy_static! {
30 /// Global Prometheus registry for ngit-grasp metrics
31 pub static ref REGISTRY: Registry = Registry::new();
32}
33
34/// Central metrics collection for ngit-grasp relay.
35///
36/// Thread-safe and designed for concurrent access from multiple tokio tasks.
37#[derive(Clone)]
38pub struct Metrics {
39 inner: Arc<MetricsInner>,
40}
41
42struct MetricsInner {
43 /// Connection tracking with abuse detection
44 pub connection_tracker: ConnectionTracker,
45
46 /// Repository bandwidth tracking (top-N only)
47 pub bandwidth_tracker: BandwidthTracker,
48
49 // === WebSocket Metrics ===
50 /// Total WebSocket connections since startup
51 pub websocket_connections_total: Counter,
52 /// Connection duration histogram
53 pub websocket_connection_duration: Histogram,
54 /// Messages received by type (REQ, EVENT, CLOSE)
55 pub websocket_messages_received: CounterVec,
56 /// Messages sent by type (EVENT, EOSE, OK, NOTICE)
57 pub websocket_messages_sent: CounterVec,
58
59 // === Git Operation Metrics ===
60 /// Git operations by type and status
61 pub git_operations_total: CounterVec,
62 /// Git operation duration histogram
63 pub git_operation_duration: HistogramVec,
64 /// Total bytes transferred
65 pub git_bytes_total: CounterVec,
66 /// Push authorization results
67 pub git_push_authorization: CounterVec,
68
69 // === Nostr Event Metrics ===
70 /// Events received by kind
71 pub events_received_total: CounterVec,
72 /// Events successfully stored by kind
73 pub events_stored_total: CounterVec,
74 /// Events rejected by kind and reason
75 pub events_rejected_total: CounterVec,
76
77 // === Repository Metrics ===
78 /// Total repositories hosted
79 pub repositories_total: Gauge,
80
81 // === System Health Metrics ===
82 /// Server start time for uptime calculation
83 pub start_time: Instant,
84 /// Build information gauge
85 pub build_info: GaugeVec,
86}
87
88impl Metrics {
89 /// Creates a new Metrics instance and registers all metrics with Prometheus.
90 ///
91 /// # Arguments
92 /// * `abuse_threshold` - Number of connections from a single IP before flagging as abuse
93 pub fn new(abuse_threshold: u32) -> Self {
94 let inner = MetricsInner::new(abuse_threshold);
95 Self {
96 inner: Arc::new(inner),
97 }
98 }
99
100 /// Returns the connection tracker for WebSocket connection management.
101 pub fn connection_tracker(&self) -> &ConnectionTracker {
102 &self.inner.connection_tracker
103 }
104
105 /// Returns the bandwidth tracker for repository bandwidth tracking.
106 pub fn bandwidth_tracker(&self) -> &BandwidthTracker {
107 &self.inner.bandwidth_tracker
108 }
109
110 // === WebSocket Recording Methods ===
111
112 /// Record a new WebSocket connection
113 pub fn record_websocket_connection(&self) {
114 self.inner.websocket_connections_total.inc();
115 }
116
117 /// Start timing a WebSocket connection, returns timer that records on drop
118 pub fn start_connection_timer(&self) -> HistogramTimer {
119 HistogramTimer::new(self.inner.websocket_connection_duration.clone())
120 }
121
122 /// Record a received WebSocket message
123 pub fn record_message_received(&self, msg_type: &str) {
124 self.inner
125 .websocket_messages_received
126 .with_label_values(&[msg_type])
127 .inc();
128 }
129
130 /// Record a sent WebSocket message
131 pub fn record_message_sent(&self, msg_type: &str) {
132 self.inner
133 .websocket_messages_sent
134 .with_label_values(&[msg_type])
135 .inc();
136 }
137
138 // === Git Operation Recording Methods ===
139
140 /// Record a git operation completion
141 pub fn record_git_operation(&self, operation: &str, status: &str) {
142 self.inner
143 .git_operations_total
144 .with_label_values(&[operation, status])
145 .inc();
146 }
147
148 /// Start timing a git operation, returns a timer
149 pub fn start_git_operation_timer(&self, operation: &str) -> GitOperationTimer {
150 GitOperationTimer::new(self.inner.git_operation_duration.clone(), operation.to_string())
151 }
152
153 /// Record bytes transferred for a git operation
154 pub fn record_git_bytes(&self, direction: &str, bytes: u64) {
155 self.inner
156 .git_bytes_total
157 .with_label_values(&[direction])
158 .inc_by(bytes as f64);
159 }
160
161 /// Record a push authorization result
162 pub fn record_push_authorization(&self, result: &str) {
163 self.inner
164 .git_push_authorization
165 .with_label_values(&[result])
166 .inc();
167 }
168
169 // === Nostr Event Recording Methods ===
170
171 /// Record a received Nostr event
172 pub fn record_event_received(&self, kind: u64) {
173 self.inner
174 .events_received_total
175 .with_label_values(&[&kind.to_string()])
176 .inc();
177 }
178
179 /// Record a stored Nostr event
180 pub fn record_event_stored(&self, kind: u64) {
181 self.inner
182 .events_stored_total
183 .with_label_values(&[&kind.to_string()])
184 .inc();
185 }
186
187 /// Record a rejected Nostr event
188 pub fn record_event_rejected(&self, kind: u64, reason: &str) {
189 self.inner
190 .events_rejected_total
191 .with_label_values(&[&kind.to_string(), reason])
192 .inc();
193 }
194
195 // === Repository Metrics ===
196
197 /// Set the total number of repositories
198 pub fn set_repositories_total(&self, count: u64) {
199 self.inner.repositories_total.set(count as f64);
200 }
201
202 /// Increment the repository count
203 pub fn inc_repositories_total(&self) {
204 self.inner.repositories_total.inc();
205 }
206
207 // === Rendering ===
208
209 /// Render all metrics in Prometheus text format.
210 ///
211 /// This method:
212 /// 1. Refreshes the top-N bandwidth metrics if needed
213 /// 2. Updates uptime
214 /// 3. Gathers all metrics from the registry
215 /// 4. Encodes them in Prometheus text format
216 pub fn render(&self) -> String {
217 // Refresh top-N bandwidth repos if needed
218 self.inner.bandwidth_tracker.maybe_refresh_top_n();
219
220 // Gather and encode metrics
221 let encoder = TextEncoder::new();
222 let metric_families = REGISTRY.gather();
223 let mut buffer = Vec::new();
224 encoder.encode(&metric_families, &mut buffer).unwrap();
225
226 // Add uptime as a comment (it's derived, not a registered metric)
227 let uptime = self.inner.start_time.elapsed().as_secs();
228 let mut output = String::from_utf8(buffer).unwrap();
229 output.push_str(&format!(
230 "\n# HELP ngit_uptime_seconds Seconds since server startup\n# TYPE ngit_uptime_seconds counter\nngit_uptime_seconds {}\n",
231 uptime
232 ));
233
234 output
235 }
236
237 /// Check if the system is under high load (for sync scheduling)
238 pub fn is_high_load(&self, threshold: u64) -> bool {
239 self.inner.connection_tracker.active_connections() > threshold
240 }
241}
242
243impl MetricsInner {
244 fn new(abuse_threshold: u32) -> Self {
245 // Create connection tracker
246 let connection_tracker = ConnectionTracker::new(abuse_threshold, &REGISTRY);
247
248 // Create bandwidth tracker
249 let bandwidth_tracker = BandwidthTracker::new(&REGISTRY);
250
251 // WebSocket metrics
252 let websocket_connections_total = Counter::with_opts(
253 Opts::new(
254 "ngit_websocket_connections_total",
255 "Total WebSocket connections since startup",
256 )
257 ).unwrap();
258 REGISTRY.register(Box::new(websocket_connections_total.clone())).unwrap();
259
260 let websocket_connection_duration = Histogram::with_opts(
261 HistogramOpts::new(
262 "ngit_websocket_connection_duration_seconds",
263 "Duration of WebSocket connections",
264 )
265 .buckets(vec![1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 3600.0]),
266 ).unwrap();
267 REGISTRY.register(Box::new(websocket_connection_duration.clone())).unwrap();
268
269 let websocket_messages_received = CounterVec::new(
270 Opts::new(
271 "ngit_websocket_messages_received_total",
272 "WebSocket messages received by type",
273 ),
274 &["type"],
275 ).unwrap();
276 REGISTRY.register(Box::new(websocket_messages_received.clone())).unwrap();
277
278 let websocket_messages_sent = CounterVec::new(
279 Opts::new(
280 "ngit_websocket_messages_sent_total",
281 "WebSocket messages sent by type",
282 ),
283 &["type"],
284 ).unwrap();
285 REGISTRY.register(Box::new(websocket_messages_sent.clone())).unwrap();
286
287 // Git operation metrics
288 let git_operations_total = CounterVec::new(
289 Opts::new(
290 "ngit_git_operations_total",
291 "Git operations by type and status",
292 ),
293 &["operation", "status"],
294 ).unwrap();
295 REGISTRY.register(Box::new(git_operations_total.clone())).unwrap();
296
297 let git_operation_duration = HistogramVec::new(
298 HistogramOpts::new(
299 "ngit_git_operation_duration_seconds",
300 "Duration of git operations",
301 )
302 .buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
303 &["operation"],
304 ).unwrap();
305 REGISTRY.register(Box::new(git_operation_duration.clone())).unwrap();
306
307 let git_bytes_total = CounterVec::new(
308 Opts::new(
309 "ngit_git_bytes_total",
310 "Total bytes transferred for git operations",
311 ),
312 &["direction"],
313 ).unwrap();
314 REGISTRY.register(Box::new(git_bytes_total.clone())).unwrap();
315
316 let git_push_authorization = CounterVec::new(
317 Opts::new(
318 "ngit_git_push_authorization_total",
319 "Push authorization results",
320 ),
321 &["result"],
322 ).unwrap();
323 REGISTRY.register(Box::new(git_push_authorization.clone())).unwrap();
324
325 // Nostr event metrics
326 let events_received_total = CounterVec::new(
327 Opts::new(
328 "ngit_events_received_total",
329 "Nostr events received by kind",
330 ),
331 &["kind"],
332 ).unwrap();
333 REGISTRY.register(Box::new(events_received_total.clone())).unwrap();
334
335 let events_stored_total = CounterVec::new(
336 Opts::new(
337 "ngit_events_stored_total",
338 "Nostr events successfully stored by kind",
339 ),
340 &["kind"],
341 ).unwrap();
342 REGISTRY.register(Box::new(events_stored_total.clone())).unwrap();
343
344 let events_rejected_total = CounterVec::new(
345 Opts::new(
346 "ngit_events_rejected_total",
347 "Nostr events rejected by kind and reason",
348 ),
349 &["kind", "reason"],
350 ).unwrap();
351 REGISTRY.register(Box::new(events_rejected_total.clone())).unwrap();
352
353 // Repository metrics
354 let repositories_total = Gauge::with_opts(
355 Opts::new(
356 "ngit_repositories_total",
357 "Total repositories hosted",
358 )
359 ).unwrap();
360 REGISTRY.register(Box::new(repositories_total.clone())).unwrap();
361
362 // Build info
363 let build_info = GaugeVec::new(
364 Opts::new(
365 "ngit_build_info",
366 "Build information",
367 ),
368 &["version", "commit"],
369 ).unwrap();
370 REGISTRY.register(Box::new(build_info.clone())).unwrap();
371
372 // Set build info gauge to 1 (it's just for labels)
373 build_info
374 .with_label_values(&[env!("CARGO_PKG_VERSION"), option_env!("GIT_HASH").unwrap_or("unknown")])
375 .set(1.0);
376
377 Self {
378 connection_tracker,
379 bandwidth_tracker,
380 websocket_connections_total,
381 websocket_connection_duration,
382 websocket_messages_received,
383 websocket_messages_sent,
384 git_operations_total,
385 git_operation_duration,
386 git_bytes_total,
387 git_push_authorization,
388 events_received_total,
389 events_stored_total,
390 events_rejected_total,
391 repositories_total,
392 start_time: Instant::now(),
393 build_info,
394 }
395 }
396}
397
398/// Timer for tracking WebSocket connection duration.
399/// Records the elapsed time when dropped.
400pub struct HistogramTimer {
401 histogram: Histogram,
402 start: Instant,
403}
404
405impl HistogramTimer {
406 fn new(histogram: Histogram) -> Self {
407 Self {
408 histogram,
409 start: Instant::now(),
410 }
411 }
412}
413
414impl Drop for HistogramTimer {
415 fn drop(&mut self) {
416 let elapsed = self.start.elapsed().as_secs_f64();
417 self.histogram.observe(elapsed);
418 }
419}
420
421/// Timer for tracking Git operation duration.
422/// Records the elapsed time when dropped.
423pub struct GitOperationTimer {
424 histogram_vec: HistogramVec,
425 operation: String,
426 start: Instant,
427}
428
429impl GitOperationTimer {
430 fn new(histogram_vec: HistogramVec, operation: String) -> Self {
431 Self {
432 histogram_vec,
433 operation,
434 start: Instant::now(),
435 }
436 }
437}
438
439impl Drop for GitOperationTimer {
440 fn drop(&mut self) {
441 let elapsed = self.start.elapsed().as_secs_f64();
442 self.histogram_vec
443 .with_label_values(&[&self.operation])
444 .observe(elapsed);
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_metrics_creation() {
454 // Note: This test may fail if run with other tests due to global registry
455 // In production, consider using a test-specific registry
456 let metrics = Metrics::new(10);
457
458 // Test that we can record metrics without panicking
459 metrics.record_websocket_connection();
460 metrics.record_message_received("REQ");
461 metrics.record_message_sent("EVENT");
462 metrics.record_git_operation("clone", "success");
463 metrics.record_git_bytes("in", 1024);
464 metrics.record_event_received(1);
465 metrics.record_event_stored(1);
466 metrics.record_event_rejected(1, "invalid_signature");
467 metrics.set_repositories_total(5);
468 }
469} \ No newline at end of file