1use std::collections::HashMap;
2use std::fmt::Write;
3
4use metriken::{MetricEntry, Value};
5
6pub struct PrometheusOptions {
8 pub help_text: bool,
10 pub percentiles: Vec<f64>,
13}
14
15impl Default for PrometheusOptions {
16 fn default() -> Self {
17 Self {
18 help_text: true,
19 percentiles: Vec::new(),
20 }
21 }
22}
23
24impl PrometheusOptions {
25 pub fn with_percentiles(mut self, percentiles: Vec<f64>) -> Self {
27 self.percentiles = percentiles;
28 self
29 }
30
31 pub fn without_help(mut self) -> Self {
33 self.help_text = false;
34 self
35 }
36}
37
38pub fn prometheus_text(options: &PrometheusOptions) -> String {
51 let mut output = String::new();
52
53 for metric in &metriken::metrics() {
54 let name = sanitize_name(metric.name());
55
56 match metric.value() {
57 Some(Value::Counter(value)) => {
58 write_type_help(&mut output, &name, "counter", metric, options);
59 write_metric_line(&mut output, &name, None, &value.to_string());
60 }
61 Some(Value::Gauge(value)) => {
62 write_type_help(&mut output, &name, "gauge", metric, options);
63 write_metric_line(&mut output, &name, None, &value.to_string());
64 }
65 Some(Value::Histogram(h)) => {
66 if let Some(snapshot) = h.load() {
67 write_histogram(&mut output, &name, None, &snapshot, metric, options);
68 }
69 }
70 Some(Value::CounterGroup(g)) => {
71 let base_metadata = entry_metadata(metric);
72 let active = g.metadata_snapshot();
73 if !active.is_empty() {
74 write_type_help(&mut output, &name, "counter", metric, options);
75 for (idx, entry_meta) in active {
76 if let Some(value) = g.counter_value(idx) {
77 let labels = merge_labels(&base_metadata, Some(entry_meta));
78 write_metric_line(
79 &mut output,
80 &name,
81 Some(&labels),
82 &value.to_string(),
83 );
84 }
85 }
86 }
87 }
88 Some(Value::GaugeGroup(g)) => {
89 let base_metadata = entry_metadata(metric);
90 let active = g.metadata_snapshot();
91 if !active.is_empty() {
92 write_type_help(&mut output, &name, "gauge", metric, options);
93 for (idx, entry_meta) in active {
94 if let Some(value) = g.gauge_value(idx) {
95 let labels = merge_labels(&base_metadata, Some(entry_meta));
96 write_metric_line(
97 &mut output,
98 &name,
99 Some(&labels),
100 &value.to_string(),
101 );
102 }
103 }
104 }
105 }
106 Some(Value::HistogramGroup(g)) => {
107 let base_metadata = entry_metadata(metric);
108 let active = g.metadata_snapshot();
109 for (idx, entry_meta) in active {
110 if let Some(snapshot) = g.load_histogram(idx) {
111 let labels = merge_labels(&base_metadata, Some(entry_meta));
112 write_histogram(
113 &mut output,
114 &name,
115 Some(&labels),
116 &snapshot,
117 metric,
118 options,
119 );
120 }
121 }
122 }
123 _ => {}
124 }
125 }
126
127 output
128}
129
130fn write_type_help(
131 output: &mut String,
132 name: &str,
133 kind: &str,
134 metric: &MetricEntry,
135 options: &PrometheusOptions,
136) {
137 if options.help_text {
138 if let Some(description) = metric.description() {
139 let _ = writeln!(output, "# HELP {name} {description}");
140 }
141 }
142 let _ = writeln!(output, "# TYPE {name} {kind}");
143}
144
145fn write_metric_line(output: &mut String, name: &str, labels: Option<&str>, value: &str) {
146 match labels {
147 Some(l) if !l.is_empty() => {
148 let _ = writeln!(output, "{name}{{{l}}} {value}");
149 }
150 _ => {
151 let _ = writeln!(output, "{name} {value}");
152 }
153 }
154}
155
156fn write_histogram(
157 output: &mut String,
158 name: &str,
159 labels: Option<&str>,
160 snapshot: &histogram::Histogram,
161 metric: &MetricEntry,
162 options: &PrometheusOptions,
163) {
164 if !options.percentiles.is_empty() {
165 write_type_help(output, name, "summary", metric, options);
167
168 if let Ok(Some(results)) = snapshot.percentiles(&options.percentiles) {
169 for (percentile, bucket) in results {
170 let value = bucket.end();
171 let quantile_label = format!("quantile=\"{percentile}\"");
172 let combined = match labels {
173 Some(l) if !l.is_empty() => format!("{l}, {quantile_label}"),
174 _ => quantile_label,
175 };
176 write_metric_line(output, name, Some(&combined), &value.to_string());
177 }
178 }
179
180 let mut count: u64 = 0;
182 let mut sum: u128 = 0;
183 for bucket in snapshot {
184 let c = bucket.count();
185 count += c;
186 sum += c as u128 * ((bucket.start() as u128 + bucket.end() as u128) / 2);
187 }
188 write_metric_line(output, &format!("{name}_count"), labels, &count.to_string());
189 write_metric_line(output, &format!("{name}_sum"), labels, &sum.to_string());
190 } else {
191 write_type_help(output, name, "histogram", metric, options);
193
194 let mut count: u64 = 0;
195 let mut sum: u128 = 0;
196 for bucket in snapshot {
197 let c = bucket.count();
198 sum += c as u128 * bucket.end() as u128;
199 count += c;
200 let le_label = format!("le=\"{}\"", bucket.end());
201 let combined = match labels {
202 Some(l) if !l.is_empty() => format!("{l}, {le_label}"),
203 _ => le_label,
204 };
205 write_metric_line(
206 output,
207 &format!("{name}_bucket"),
208 Some(&combined),
209 &count.to_string(),
210 );
211 }
212 let inf_label = "le=\"+Inf\"".to_string();
213 let combined = match labels {
214 Some(l) if !l.is_empty() => format!("{l}, {inf_label}"),
215 _ => inf_label,
216 };
217 write_metric_line(
218 output,
219 &format!("{name}_bucket"),
220 Some(&combined),
221 &count.to_string(),
222 );
223 write_metric_line(output, &format!("{name}_count"), labels, &count.to_string());
224 write_metric_line(output, &format!("{name}_sum"), labels, &sum.to_string());
225 }
226}
227
228fn entry_metadata(metric: &MetricEntry) -> HashMap<String, String> {
230 metric
231 .metadata()
232 .into_iter()
233 .map(|(k, v)| (k.to_string(), v.to_string()))
234 .collect()
235}
236
237fn merge_labels(
240 base: &HashMap<String, String>,
241 index_meta: Option<HashMap<String, String>>,
242) -> String {
243 let mut all = base.clone();
244 if let Some(meta) = index_meta {
245 all.extend(meta);
246 }
247 format_labels(&all)
248}
249
250fn format_labels(metadata: &HashMap<String, String>) -> String {
252 let mut pairs: Vec<String> = metadata
253 .iter()
254 .map(|(k, v)| format!("{k}=\"{v}\""))
255 .collect();
256 pairs.sort();
257 pairs.join(", ")
258}
259
260fn sanitize_name(name: &str) -> String {
264 let mut result = String::with_capacity(name.len());
265 for c in name.chars() {
266 if c.is_ascii_alphanumeric() || c == '_' || c == ':' {
267 result.push(c);
268 } else {
269 result.push('_');
270 }
271 }
272 result
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_sanitize_name() {
281 assert_eq!(sanitize_name("simple"), "simple");
282 assert_eq!(sanitize_name("with/slash"), "with_slash");
283 assert_eq!(sanitize_name("with.dots"), "with_dots");
284 assert_eq!(sanitize_name("ok_under_score"), "ok_under_score");
285 assert_eq!(sanitize_name("has:colon"), "has:colon");
286 }
287
288 #[test]
289 fn test_format_labels() {
290 let mut meta = HashMap::new();
291 meta.insert("b".into(), "2".into());
292 meta.insert("a".into(), "1".into());
293 assert_eq!(format_labels(&meta), "a=\"1\", b=\"2\"");
294 }
295
296 #[test]
297 fn test_format_labels_empty() {
298 let meta = HashMap::new();
299 assert_eq!(format_labels(&meta), "");
300 }
301
302 #[test]
303 fn test_write_metric_line_no_labels() {
304 let mut out = String::new();
305 write_metric_line(&mut out, "my_counter", None, "42");
306 assert_eq!(out, "my_counter 42\n");
307 }
308
309 #[test]
310 fn test_write_metric_line_with_labels() {
311 let mut out = String::new();
312 write_metric_line(&mut out, "my_counter", Some("cpu=\"0\""), "42");
313 assert_eq!(out, "my_counter{cpu=\"0\"} 42\n");
314 }
315
316 #[test]
317 fn test_write_metric_line_empty_labels() {
318 let mut out = String::new();
319 write_metric_line(&mut out, "my_counter", Some(""), "42");
320 assert_eq!(out, "my_counter 42\n");
321 }
322}