1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Instant;
3
4pub struct MethodCounters {
6 pub get: AtomicU64,
7 pub post: AtomicU64,
8 pub patch: AtomicU64,
9 pub delete: AtomicU64,
10 pub options: AtomicU64,
11}
12
13impl MethodCounters {
14 fn new() -> Self {
15 Self {
16 get: AtomicU64::new(0),
17 post: AtomicU64::new(0),
18 patch: AtomicU64::new(0),
19 delete: AtomicU64::new(0),
20 options: AtomicU64::new(0),
21 }
22 }
23
24 fn increment(&self, method: &str) {
25 match method {
26 "GET" => self.get.fetch_add(1, Ordering::Relaxed),
27 "POST" => self.post.fetch_add(1, Ordering::Relaxed),
28 "PATCH" => self.patch.fetch_add(1, Ordering::Relaxed),
29 "DELETE" => self.delete.fetch_add(1, Ordering::Relaxed),
30 "OPTIONS" => self.options.fetch_add(1, Ordering::Relaxed),
31 _ => 0,
32 };
33 }
34}
35
36pub struct Metrics {
41 pub requests_total: AtomicU64,
42 pub requests_ok: AtomicU64,
43 pub requests_err: AtomicU64,
44 pub requests_by_method: MethodCounters,
45 start_time: Instant,
46}
47
48impl Metrics {
49 pub fn new() -> Self {
51 Self {
52 requests_total: AtomicU64::new(0),
53 requests_ok: AtomicU64::new(0),
54 requests_err: AtomicU64::new(0),
55 requests_by_method: MethodCounters::new(),
56 start_time: Instant::now(),
57 }
58 }
59
60 pub fn record_request(&self, method: &str, status: u16) {
63 self.requests_total.fetch_add(1, Ordering::Relaxed);
64 if (200..400).contains(&status) {
65 self.requests_ok.fetch_add(1, Ordering::Relaxed);
66 } else {
67 self.requests_err.fetch_add(1, Ordering::Relaxed);
68 }
69 self.requests_by_method.increment(method);
70 }
71
72 pub fn uptime_secs(&self) -> u64 {
74 self.start_time.elapsed().as_secs()
75 }
76
77 pub fn snapshot(&self) -> serde_json::Value {
79 serde_json::json!({
80 "uptime_secs": self.uptime_secs(),
81 "requests": {
82 "total": self.requests_total.load(Ordering::Relaxed),
83 "ok": self.requests_ok.load(Ordering::Relaxed),
84 "error": self.requests_err.load(Ordering::Relaxed),
85 },
86 "methods": {
87 "GET": self.requests_by_method.get.load(Ordering::Relaxed),
88 "POST": self.requests_by_method.post.load(Ordering::Relaxed),
89 "PATCH": self.requests_by_method.patch.load(Ordering::Relaxed),
90 "DELETE": self.requests_by_method.delete.load(Ordering::Relaxed),
91 }
92 })
93 }
94
95 pub fn prometheus(&self) -> String {
99 let total = self.requests_total.load(Ordering::Relaxed);
100 let ok = self.requests_ok.load(Ordering::Relaxed);
101 let err = self.requests_err.load(Ordering::Relaxed);
102 let uptime = self.uptime_secs();
103 let get = self.requests_by_method.get.load(Ordering::Relaxed);
104 let post = self.requests_by_method.post.load(Ordering::Relaxed);
105 let patch = self.requests_by_method.patch.load(Ordering::Relaxed);
106 let delete = self.requests_by_method.delete.load(Ordering::Relaxed);
107 let options = self.requests_by_method.options.load(Ordering::Relaxed);
108
109 format!(
110 "# HELP pylon_uptime_seconds Server uptime in seconds.\n\
111 # TYPE pylon_uptime_seconds gauge\n\
112 pylon_uptime_seconds {uptime}\n\
113 # HELP pylon_http_requests_total HTTP requests total.\n\
114 # TYPE pylon_http_requests_total counter\n\
115 pylon_http_requests_total {total}\n\
116 # HELP pylon_http_requests_ok_total HTTP requests with 2xx/3xx status.\n\
117 # TYPE pylon_http_requests_ok_total counter\n\
118 pylon_http_requests_ok_total {ok}\n\
119 # HELP pylon_http_requests_errors_total HTTP requests with 4xx/5xx status.\n\
120 # TYPE pylon_http_requests_errors_total counter\n\
121 pylon_http_requests_errors_total {err}\n\
122 # HELP pylon_http_requests_by_method HTTP requests by method.\n\
123 # TYPE pylon_http_requests_by_method counter\n\
124 pylon_http_requests_by_method{{method=\"GET\"}} {get}\n\
125 pylon_http_requests_by_method{{method=\"POST\"}} {post}\n\
126 pylon_http_requests_by_method{{method=\"PATCH\"}} {patch}\n\
127 pylon_http_requests_by_method{{method=\"DELETE\"}} {delete}\n\
128 pylon_http_requests_by_method{{method=\"OPTIONS\"}} {options}\n"
129 )
130 }
131}
132
133impl Default for Metrics {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn new_metrics_are_zero() {
145 let m = Metrics::new();
146 assert_eq!(m.requests_total.load(Ordering::Relaxed), 0);
147 assert_eq!(m.requests_ok.load(Ordering::Relaxed), 0);
148 assert_eq!(m.requests_err.load(Ordering::Relaxed), 0);
149 }
150
151 #[test]
152 fn record_ok_request() {
153 let m = Metrics::new();
154 m.record_request("GET", 200);
155 assert_eq!(m.requests_total.load(Ordering::Relaxed), 1);
156 assert_eq!(m.requests_ok.load(Ordering::Relaxed), 1);
157 assert_eq!(m.requests_err.load(Ordering::Relaxed), 0);
158 assert_eq!(m.requests_by_method.get.load(Ordering::Relaxed), 1);
159 }
160
161 #[test]
162 fn record_error_request() {
163 let m = Metrics::new();
164 m.record_request("POST", 500);
165 assert_eq!(m.requests_total.load(Ordering::Relaxed), 1);
166 assert_eq!(m.requests_ok.load(Ordering::Relaxed), 0);
167 assert_eq!(m.requests_err.load(Ordering::Relaxed), 1);
168 assert_eq!(m.requests_by_method.post.load(Ordering::Relaxed), 1);
169 }
170
171 #[test]
172 fn method_counters_increment_independently() {
173 let m = Metrics::new();
174 m.record_request("GET", 200);
175 m.record_request("GET", 200);
176 m.record_request("POST", 201);
177 m.record_request("DELETE", 204);
178 m.record_request("PATCH", 200);
179 m.record_request("OPTIONS", 204);
180
181 assert_eq!(m.requests_by_method.get.load(Ordering::Relaxed), 2);
182 assert_eq!(m.requests_by_method.post.load(Ordering::Relaxed), 1);
183 assert_eq!(m.requests_by_method.delete.load(Ordering::Relaxed), 1);
184 assert_eq!(m.requests_by_method.patch.load(Ordering::Relaxed), 1);
185 assert_eq!(m.requests_by_method.options.load(Ordering::Relaxed), 1);
186 assert_eq!(m.requests_total.load(Ordering::Relaxed), 6);
187 }
188
189 #[test]
190 fn snapshot_returns_valid_json() {
191 let m = Metrics::new();
192 m.record_request("GET", 200);
193 m.record_request("POST", 400);
194
195 let snap = m.snapshot();
196 assert_eq!(snap["requests"]["total"], 2);
197 assert_eq!(snap["requests"]["ok"], 1);
198 assert_eq!(snap["requests"]["error"], 1);
199 assert_eq!(snap["methods"]["GET"], 1);
200 assert_eq!(snap["methods"]["POST"], 1);
201 assert_eq!(snap["methods"]["PATCH"], 0);
202 assert_eq!(snap["methods"]["DELETE"], 0);
203 assert!(snap["uptime_secs"].as_u64().is_some());
204 }
205
206 #[test]
207 fn uptime_is_non_negative() {
208 let m = Metrics::new();
209 assert!(m.uptime_secs() < 2); }
211
212 #[test]
213 fn status_boundary_classification() {
214 let m = Metrics::new();
215 m.record_request("GET", 200);
217 m.record_request("GET", 204);
218 m.record_request("GET", 299);
219 m.record_request("GET", 301);
221 m.record_request("GET", 399);
222 m.record_request("GET", 400);
224 m.record_request("GET", 404);
225 m.record_request("GET", 500);
227
228 assert_eq!(m.requests_ok.load(Ordering::Relaxed), 5);
229 assert_eq!(m.requests_err.load(Ordering::Relaxed), 3);
230 }
231}