halldyll_core/observe/
prometheus.rs1use crate::observe::metrics::{MetricsCollector, MetricsSnapshot};
20
21pub struct PrometheusExporter<'a> {
23 collector: &'a MetricsCollector,
24 prefix: String,
25}
26
27impl<'a> PrometheusExporter<'a> {
28 pub fn new(collector: &'a MetricsCollector) -> Self {
30 Self {
31 collector,
32 prefix: "halldyll".to_string(),
33 }
34 }
35
36 pub fn with_prefix(collector: &'a MetricsCollector, prefix: impl Into<String>) -> Self {
38 Self {
39 collector,
40 prefix: prefix.into(),
41 }
42 }
43
44 pub fn export(&self) -> String {
46 let snapshot = self.collector.global().snapshot();
47 self.format_metrics(&snapshot)
48 }
49
50 pub fn export_with_labels(&self, labels: &[(&str, &str)]) -> String {
52 let snapshot = self.collector.global().snapshot();
53 self.format_metrics_with_labels(&snapshot, labels)
54 }
55
56 fn format_metrics(&self, snapshot: &MetricsSnapshot) -> String {
57 let mut output = String::with_capacity(2048);
58 let p = &self.prefix;
59
60 output.push_str(&format!(
62 "# HELP {p}_requests_total Total number of HTTP requests\n\
63 # TYPE {p}_requests_total counter\n\
64 {p}_requests_total {}\n\n",
65 snapshot.requests_total
66 ));
67
68 output.push_str(&format!(
70 "# HELP {p}_requests_success_total Total number of successful requests\n\
71 # TYPE {p}_requests_success_total counter\n\
72 {p}_requests_success_total {}\n\n",
73 snapshot.requests_success
74 ));
75
76 output.push_str(&format!(
78 "# HELP {p}_requests_failed_total Total number of failed requests\n\
79 # TYPE {p}_requests_failed_total counter\n\
80 {p}_requests_failed_total {}\n\n",
81 snapshot.requests_failed
82 ));
83
84 output.push_str(&format!(
86 "# HELP {p}_requests_rate_limited_total Total number of rate-limited requests\n\
87 # TYPE {p}_requests_rate_limited_total counter\n\
88 {p}_requests_rate_limited_total {}\n\n",
89 snapshot.requests_rate_limited
90 ));
91
92 output.push_str(&format!(
94 "# HELP {p}_bytes_downloaded_total Total bytes downloaded\n\
95 # TYPE {p}_bytes_downloaded_total counter\n\
96 {p}_bytes_downloaded_total {}\n\n",
97 snapshot.bytes_downloaded
98 ));
99
100 output.push_str(&format!(
102 "# HELP {p}_retries_total Total number of retries performed\n\
103 # TYPE {p}_retries_total counter\n\
104 {p}_retries_total {}\n\n",
105 snapshot.retries_total
106 ));
107
108 output.push_str(&format!(
110 "# HELP {p}_documents_extracted_total Total number of documents extracted\n\
111 # TYPE {p}_documents_extracted_total counter\n\
112 {p}_documents_extracted_total {}\n\n",
113 snapshot.documents_extracted
114 ));
115
116 output.push_str(&format!(
118 "# HELP {p}_success_rate Current success rate (0.0-1.0)\n\
119 # TYPE {p}_success_rate gauge\n\
120 {p}_success_rate {:.4}\n\n",
121 snapshot.success_rate
122 ));
123
124 output.push_str(&format!(
126 "# HELP {p}_avg_latency_ms Average request latency in milliseconds\n\
127 # TYPE {p}_avg_latency_ms gauge\n\
128 {p}_avg_latency_ms {:.2}\n",
129 snapshot.avg_latency_ms
130 ));
131
132 output
133 }
134
135 fn format_metrics_with_labels(&self, snapshot: &MetricsSnapshot, labels: &[(&str, &str)]) -> String {
136 let label_str = if labels.is_empty() {
137 String::new()
138 } else {
139 let pairs: Vec<String> = labels
140 .iter()
141 .map(|(k, v)| format!("{}=\"{}\"", k, v))
142 .collect();
143 format!("{{{}}}", pairs.join(","))
144 };
145
146 let mut output = String::with_capacity(2048);
147 let p = &self.prefix;
148
149 output.push_str(&format!(
150 "# HELP {p}_requests_total Total number of HTTP requests\n\
151 # TYPE {p}_requests_total counter\n\
152 {p}_requests_total{label_str} {}\n\n",
153 snapshot.requests_total
154 ));
155
156 output.push_str(&format!(
157 "# HELP {p}_requests_success_total Total number of successful requests\n\
158 # TYPE {p}_requests_success_total counter\n\
159 {p}_requests_success_total{label_str} {}\n\n",
160 snapshot.requests_success
161 ));
162
163 output.push_str(&format!(
164 "# HELP {p}_requests_failed_total Total number of failed requests\n\
165 # TYPE {p}_requests_failed_total counter\n\
166 {p}_requests_failed_total{label_str} {}\n\n",
167 snapshot.requests_failed
168 ));
169
170 output.push_str(&format!(
171 "# HELP {p}_requests_rate_limited_total Total number of rate-limited requests\n\
172 # TYPE {p}_requests_rate_limited_total counter\n\
173 {p}_requests_rate_limited_total{label_str} {}\n\n",
174 snapshot.requests_rate_limited
175 ));
176
177 output.push_str(&format!(
178 "# HELP {p}_bytes_downloaded_total Total bytes downloaded\n\
179 # TYPE {p}_bytes_downloaded_total counter\n\
180 {p}_bytes_downloaded_total{label_str} {}\n\n",
181 snapshot.bytes_downloaded
182 ));
183
184 output.push_str(&format!(
185 "# HELP {p}_retries_total Total number of retries performed\n\
186 # TYPE {p}_retries_total counter\n\
187 {p}_retries_total{label_str} {}\n\n",
188 snapshot.retries_total
189 ));
190
191 output.push_str(&format!(
192 "# HELP {p}_documents_extracted_total Total number of documents extracted\n\
193 # TYPE {p}_documents_extracted_total counter\n\
194 {p}_documents_extracted_total{label_str} {}\n\n",
195 snapshot.documents_extracted
196 ));
197
198 output.push_str(&format!(
199 "# HELP {p}_success_rate Current success rate (0.0-1.0)\n\
200 # TYPE {p}_success_rate gauge\n\
201 {p}_success_rate{label_str} {:.4}\n\n",
202 snapshot.success_rate
203 ));
204
205 output.push_str(&format!(
206 "# HELP {p}_avg_latency_ms Average request latency in milliseconds\n\
207 # TYPE {p}_avg_latency_ms gauge\n\
208 {p}_avg_latency_ms{label_str} {:.2}\n",
209 snapshot.avg_latency_ms
210 ));
211
212 output
213 }
214}
215
216pub fn export_prometheus(collector: &MetricsCollector) -> String {
218 PrometheusExporter::new(collector).export()
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_prometheus_export() {
227 let collector = MetricsCollector::new();
228 collector.global().inc_requests();
229 collector.global().inc_success();
230 collector.global().add_bytes(1024);
231
232 let exporter = PrometheusExporter::new(&collector);
233 let output = exporter.export();
234
235 assert!(output.contains("halldyll_requests_total 1"));
236 assert!(output.contains("halldyll_requests_success_total 1"));
237 assert!(output.contains("halldyll_bytes_downloaded_total 1024"));
238 assert!(output.contains("# TYPE halldyll_requests_total counter"));
239 }
240
241 #[test]
242 fn test_prometheus_with_labels() {
243 let collector = MetricsCollector::new();
244 collector.global().inc_requests();
245
246 let exporter = PrometheusExporter::new(&collector);
247 let output = exporter.export_with_labels(&[("instance", "scraper-1"), ("env", "prod")]);
248
249 assert!(output.contains("halldyll_requests_total{instance=\"scraper-1\",env=\"prod\"} 1"));
250 }
251
252 #[test]
253 fn test_custom_prefix() {
254 let collector = MetricsCollector::new();
255 collector.global().inc_requests();
256
257 let exporter = PrometheusExporter::with_prefix(&collector, "myapp_scraper");
258 let output = exporter.export();
259
260 assert!(output.contains("myapp_scraper_requests_total 1"));
261 }
262}