1pub const DEFAULT_MAX_LABELS: usize = 10;
5
6pub const DEFAULT_BUCKETS: [f64; 11] = [
8 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
9];
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MetricType {
14 Gauge,
16 Counter,
18 Histogram,
20}
21
22impl MetricType {
23 pub fn name(&self) -> &'static str {
25 match self {
26 Self::Gauge => "gauge",
27 Self::Counter => "counter",
28 Self::Histogram => "histogram",
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct Labels {
36 pairs: Vec<(String, String)>,
37}
38
39impl Labels {
40 pub fn new() -> Self {
42 Self { pairs: Vec::new() }
43 }
44
45 pub fn add(mut self, key: &str, value: &str) -> Self {
47 self.pairs.push((key.to_string(), value.to_string()));
48 self
49 }
50
51 pub fn format(&self) -> String {
53 if self.pairs.is_empty() {
54 return String::new();
55 }
56
57 let parts: Vec<String> = self
58 .pairs
59 .iter()
60 .map(|(k, v)| format!("{}=\"{}\"", k, escape_label_value(v)))
61 .collect();
62
63 format!("{{{}}}", parts.join(","))
64 }
65
66 pub fn len(&self) -> usize {
68 self.pairs.len()
69 }
70
71 pub fn is_empty(&self) -> bool {
73 self.pairs.is_empty()
74 }
75}
76
77pub fn escape_label_value(s: &str) -> String {
79 s.replace('\\', "\\\\")
80 .replace('"', "\\\"")
81 .replace('\n', "\\n")
82}
83
84#[derive(Debug, Clone)]
86pub struct HistogramBuckets {
87 pub boundaries: Vec<f64>,
89 pub counts: Vec<u64>,
91 pub sum: f64,
93 pub count: u64,
95}
96
97impl Default for HistogramBuckets {
98 fn default() -> Self {
99 Self::with_buckets(&DEFAULT_BUCKETS)
100 }
101}
102
103impl HistogramBuckets {
104 pub fn with_buckets(boundaries: &[f64]) -> Self {
106 Self {
107 boundaries: boundaries.to_vec(),
108 counts: vec![0; boundaries.len()],
109 sum: 0.0,
110 count: 0,
111 }
112 }
113
114 pub fn observe(&mut self, value: f64) {
116 self.sum += value;
117 self.count += 1;
118
119 for (i, &boundary) in self.boundaries.iter().enumerate() {
120 if value <= boundary {
121 self.counts[i] += 1;
122 }
123 }
124 }
125
126 pub fn format(&self, name: &str, labels: &Labels) -> String {
128 let mut lines = Vec::new();
129 let label_str = labels.format();
130
131 let mut cumulative = 0u64;
133 for (i, &boundary) in self.boundaries.iter().enumerate() {
134 cumulative += self.counts[i];
135 let bucket_label = if label_str.is_empty() {
136 format!("{{le=\"{}\"}}", boundary)
137 } else {
138 format!(
139 "{{le=\"{}\",{}}}",
140 boundary,
141 &label_str[1..label_str.len() - 1]
142 )
143 };
144 lines.push(format!("{}_bucket{} {}", name, bucket_label, cumulative));
145 }
146
147 let inf_label = if label_str.is_empty() {
149 "{le=\"+Inf\"}".to_string()
150 } else {
151 format!("{{le=\"+Inf\",{}}}", &label_str[1..label_str.len() - 1])
152 };
153 lines.push(format!("{}_bucket{} {}", name, inf_label, self.count));
154
155 lines.push(format!("{}_sum{} {}", name, label_str, self.sum));
157 lines.push(format!("{}_count{} {}", name, label_str, self.count));
158
159 lines.join("\n")
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct MetricDef {
166 pub name: String,
168 pub help: String,
170 pub metric_type: MetricType,
172}
173
174impl MetricDef {
175 pub fn new(name: &str, help: &str, metric_type: MetricType) -> Self {
177 Self {
178 name: name.to_string(),
179 help: help.to_string(),
180 metric_type,
181 }
182 }
183
184 pub fn format_help(&self) -> String {
186 format!("# HELP {} {}", self.name, self.help)
187 }
188
189 pub fn format_type(&self) -> String {
191 format!("# TYPE {} {}", self.name, self.metric_type.name())
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct GaugeValue {
198 pub value: f64,
200 pub labels: Labels,
202 pub timestamp: Option<u64>,
204}
205
206impl GaugeValue {
207 pub fn new(value: f64) -> Self {
209 Self {
210 value,
211 labels: Labels::new(),
212 timestamp: None,
213 }
214 }
215
216 pub fn with_labels(mut self, labels: Labels) -> Self {
218 self.labels = labels;
219 self
220 }
221
222 pub fn with_timestamp(mut self, ts: u64) -> Self {
224 self.timestamp = Some(ts);
225 self
226 }
227
228 pub fn format(&self, name: &str) -> String {
230 let label_str = self.labels.format();
231 let ts_str = self
232 .timestamp
233 .map(|t| format!(" {}", t))
234 .unwrap_or_default();
235 format!("{}{} {}{}", name, label_str, self.value, ts_str)
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct CounterValue {
242 pub value: u64,
244 pub labels: Labels,
246}
247
248impl CounterValue {
249 pub fn new(value: u64) -> Self {
251 Self {
252 value,
253 labels: Labels::new(),
254 }
255 }
256
257 pub fn with_labels(mut self, labels: Labels) -> Self {
259 self.labels = labels;
260 self
261 }
262
263 pub fn format(&self, name: &str) -> String {
265 let label_str = self.labels.format();
266 format!("{}{} {}", name, label_str, self.value)
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct HistogramValue {
273 pub buckets: HistogramBuckets,
275 pub labels: Labels,
277}
278
279impl HistogramValue {
280 pub fn new() -> Self {
282 Self {
283 buckets: HistogramBuckets::default(),
284 labels: Labels::new(),
285 }
286 }
287
288 pub fn with_buckets(boundaries: &[f64]) -> Self {
290 Self {
291 buckets: HistogramBuckets::with_buckets(boundaries),
292 labels: Labels::new(),
293 }
294 }
295
296 pub fn with_labels(mut self, labels: Labels) -> Self {
298 self.labels = labels;
299 self
300 }
301
302 pub fn observe(&mut self, value: f64) {
304 self.buckets.observe(value);
305 }
306
307 pub fn format(&self, name: &str) -> String {
309 self.buckets.format(name, &self.labels)
310 }
311}
312
313impl Default for HistogramValue {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319pub fn validate_metric_name(name: &str) -> bool {
321 if name.is_empty() {
322 return false;
323 }
324
325 let first = name.chars().next().expect("non-empty string");
326 if !first.is_ascii_lowercase() && first != '_' {
327 return false;
328 }
329
330 name.chars()
331 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
332}