1pub mod labels;
2pub mod labels_builder;
3pub mod macros;
4pub mod metric_def;
5pub mod store;
6
7pub use labels::Labels;
8pub use labels_builder::LabelsBuilder;
9pub use metric_def::{MetricDef, ToMetricDef};
10pub use store::MetricStore;
11
12pub trait RenderIntoMetrics {
15 fn render_into_metrics(&self, namespace: Option<&str>) -> String;
16}
17
18#[derive(Debug, Clone)]
19pub enum MetricValue {
20 I32(i32),
21 I64(i64),
22 I128(i128),
23 U32(u32),
24 U64(u64),
25 U128(u128),
26 F64(f64),
27 Bool(bool),
28}
29
30impl MetricValue {
31 pub fn render(&self) -> String {
32 match self {
33 MetricValue::I32(v) => v.to_string(),
34 MetricValue::I64(v) => v.to_string(),
35 MetricValue::I128(v) => v.to_string(),
36 MetricValue::U32(v) => v.to_string(),
37 MetricValue::U64(v) => v.to_string(),
38 MetricValue::U128(v) => v.to_string(),
39 MetricValue::F64(v) => format!("{}", v),
40 MetricValue::Bool(v) => {
41 if *v {
42 1_i64.to_string()
43 } else {
44 0_i64.to_string()
45 }
46 }
47 }
48 }
49}
50
51impl From<i32> for MetricValue {
52 fn from(v: i32) -> Self {
53 MetricValue::I32(v)
54 }
55}
56
57impl From<i64> for MetricValue {
58 fn from(v: i64) -> Self {
59 MetricValue::I64(v)
60 }
61}
62
63impl From<i128> for MetricValue {
64 fn from(v: i128) -> Self {
65 MetricValue::I128(v)
66 }
67}
68
69impl From<u32> for MetricValue {
70 fn from(v: u32) -> Self {
71 MetricValue::U32(v)
72 }
73}
74
75impl From<u64> for MetricValue {
76 fn from(v: u64) -> Self {
77 MetricValue::U64(v)
78 }
79}
80
81impl From<u128> for MetricValue {
82 fn from(v: u128) -> Self {
83 MetricValue::U128(v)
84 }
85}
86
87impl From<f64> for MetricValue {
88 fn from(v: f64) -> Self {
89 MetricValue::F64(v)
90 }
91}
92
93impl From<bool> for MetricValue {
94 fn from(v: bool) -> Self {
95 MetricValue::Bool(v)
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct Sample {
102 labels: Labels,
103 value: MetricValue,
104}
105
106impl Sample {
107 pub fn new<T: Into<MetricValue>>(labels: &Labels, value: T) -> Self {
108 Self {
109 labels: labels.clone(),
110 value: value.into(),
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq)]
116pub enum Error {
117 InvalidMetricName(String),
123
124 InvalidLabelName(String),
130}
131
132#[derive(Clone, Debug)]
138pub enum MetricType {
139 Counter,
144
145 Gauge,
148
149 Histogram,
153}
154
155impl std::fmt::Display for MetricType {
156 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157 match self {
158 MetricType::Counter => write!(f, "counter"),
159 MetricType::Gauge => write!(f, "gauge"),
160 MetricType::Histogram => write!(f, "histogram"),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use crate::metric_def::{MetricDef, ToMetricDef};
168 use crate::{labels_builder::LabelsBuilder, store::MetricStore};
169
170 use super::*;
171
172 pub struct State {
173 name: String,
174 client: String,
175 health: bool,
176 height: i64,
177 delta: f64,
178 maybe: Option<i64>,
179 }
180
181 #[derive(Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
182 pub enum ServiceMetric {
183 WorkerHealth,
184 ServiceHeight,
185 ServiceDelta,
186 Maybe,
187 Maybe2,
188 }
189
190 impl ToMetricDef for ServiceMetric {
191 fn to_metric_def(&self) -> MetricDef {
192 match self {
193 ServiceMetric::WorkerHealth => {
194 MetricDef::new("worker_health", "worker health", MetricType::Gauge).unwrap()
195 }
196 ServiceMetric::ServiceHeight => {
197 MetricDef::new("service_height", "service height", MetricType::Gauge).unwrap()
198 }
199 ServiceMetric::ServiceDelta => {
200 MetricDef::gauge("service_delta", "service delta").unwrap()
201 }
202 ServiceMetric::Maybe => MetricDef::gauge("service_maybe", "service maybe").unwrap(),
203 ServiceMetric::Maybe2 => {
204 MetricDef::gauge("service_maybe2", "service maybe2").unwrap()
205 }
206 }
207 }
208 }
209
210 #[test]
211 fn complex() {
212 let states = vec![
213 State {
214 name: "a".into(),
215 client: "woot".into(),
216 health: true,
217 height: 100,
218 delta: 1.0,
219 maybe: Some(100),
220 },
221 State {
222 name: "b".into(),
223 client: "woot".into(),
224 health: true,
225 height: 200,
226 delta: 2.2,
227 maybe: Some(100),
228 },
229 State {
230 name: "c".into(),
231 client: "meh".into(),
232 health: true,
233 height: 300,
234 delta: 3.0,
235 maybe: Some(100),
236 },
237 State {
238 name: "d".into(),
239 client: "meh".into(),
240 health: false,
241 height: 0,
242 delta: 291283791287391287391.123,
243 maybe: None,
244 },
245 ];
246
247 let static_labels_builder = LabelsBuilder::from([("process", "simple-metrics")]);
248 let static_labels = static_labels_builder.build().unwrap();
249
250 let namespace = String::from("test_exporter");
251 let mut store: MetricStore<ServiceMetric> =
252 MetricStore::new().with_static_labels(static_labels);
253
254 for s in states {
255 let common_builder = LabelsBuilder::from([("name", s.name)]);
256 let common = common_builder.build().unwrap();
257
258 store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
259
260 let lbs = common.builder().with("client", s.client).build().unwrap();
261
262 store.add_value(ServiceMetric::ServiceHeight, &lbs, s.height);
263
264 if let Some(maybe) = s.maybe {
265 store.add_value(ServiceMetric::Maybe, &lbs, maybe)
266 }
267
268 store.maybe_add_value(ServiceMetric::Maybe2, &lbs, s.maybe);
269
270 let lbs_p = lbs.builder().with("type", "pos").build().unwrap();
271 store.add_value(ServiceMetric::ServiceDelta, &lbs_p, s.delta);
272
273 let lbs_n = lbs.builder().with("type", "neg").build().unwrap();
274 store.add_value(ServiceMetric::ServiceDelta, &lbs_n, -s.delta);
275 }
276
277 let actual1 = store.render_into_metrics(Some(&namespace));
278 let actual2 = store
279 .to_rich_samples()
280 .render_into_metrics(Some(&namespace));
281
282 let expected = r#"# HELP test_exporter_worker_health worker health
283# TYPE test_exporter_worker_health gauge
284test_exporter_worker_health{name="a",process="simple-metrics"} 1
285test_exporter_worker_health{name="b",process="simple-metrics"} 1
286test_exporter_worker_health{name="c",process="simple-metrics"} 1
287test_exporter_worker_health{name="d",process="simple-metrics"} 0
288
289# HELP test_exporter_service_height service height
290# TYPE test_exporter_service_height gauge
291test_exporter_service_height{client="woot",name="a",process="simple-metrics"} 100
292test_exporter_service_height{client="woot",name="b",process="simple-metrics"} 200
293test_exporter_service_height{client="meh",name="c",process="simple-metrics"} 300
294test_exporter_service_height{client="meh",name="d",process="simple-metrics"} 0
295
296# HELP test_exporter_service_delta service delta
297# TYPE test_exporter_service_delta gauge
298test_exporter_service_delta{client="woot",name="a",process="simple-metrics",type="pos"} 1
299test_exporter_service_delta{client="woot",name="a",process="simple-metrics",type="neg"} -1
300test_exporter_service_delta{client="woot",name="b",process="simple-metrics",type="pos"} 2.2
301test_exporter_service_delta{client="woot",name="b",process="simple-metrics",type="neg"} -2.2
302test_exporter_service_delta{client="meh",name="c",process="simple-metrics",type="pos"} 3
303test_exporter_service_delta{client="meh",name="c",process="simple-metrics",type="neg"} -3
304test_exporter_service_delta{client="meh",name="d",process="simple-metrics",type="pos"} 291283791287391300000
305test_exporter_service_delta{client="meh",name="d",process="simple-metrics",type="neg"} -291283791287391300000
306
307# HELP test_exporter_service_maybe service maybe
308# TYPE test_exporter_service_maybe gauge
309test_exporter_service_maybe{client="woot",name="a",process="simple-metrics"} 100
310test_exporter_service_maybe{client="woot",name="b",process="simple-metrics"} 100
311test_exporter_service_maybe{client="meh",name="c",process="simple-metrics"} 100
312
313# HELP test_exporter_service_maybe2 service maybe2
314# TYPE test_exporter_service_maybe2 gauge
315test_exporter_service_maybe2{client="woot",name="a",process="simple-metrics"} 100
316test_exporter_service_maybe2{client="woot",name="b",process="simple-metrics"} 100
317test_exporter_service_maybe2{client="meh",name="c",process="simple-metrics"} 100
318"#;
319 assert_eq!(actual1, expected);
320 assert_eq!(actual2, expected);
321 }
322
323 pub struct SimpleState {
324 pub name: String,
325 pub health: bool,
326 pub height: i64,
327 }
328
329 #[test]
330 fn simple() {
331 let states = vec![
332 SimpleState {
333 name: "a".into(),
334 health: true,
335 height: 100,
336 },
337 SimpleState {
338 name: "b".into(),
339 health: false,
340 height: 200,
341 },
342 ];
343
344 let static_labels = LabelsBuilder::new()
345 .with("process", "simple-metrics")
346 .build()
347 .unwrap();
348
349 let mut store: MetricStore<ServiceMetric> =
350 MetricStore::new().with_static_labels(static_labels);
351
352 for s in states {
353 let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
354
355 store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
356 store.add_value(ServiceMetric::ServiceHeight, &common, s.height)
357 }
358
359 let _cloned_store = store.clone();
360
361 let actual = store.render_into_metrics(None);
362 println!("{}", actual);
363
364 let expected = r#"# HELP worker_health worker health
365# TYPE worker_health gauge
366worker_health{name="a",process="simple-metrics"} 1
367worker_health{name="b",process="simple-metrics"} 0
368
369# HELP service_height service height
370# TYPE service_height gauge
371service_height{name="a",process="simple-metrics"} 100
372service_height{name="b",process="simple-metrics"} 200
373"#;
374 assert_eq!(actual, expected);
375 }
376
377 #[test]
378 fn simple_with_labels_chain() {
379 let states = vec![
380 SimpleState {
381 name: "a".into(),
382 health: true,
383 height: 100,
384 },
385 SimpleState {
386 name: "b".into(),
387 health: false,
388 height: 200,
389 },
390 ];
391
392 let static_labels = LabelsBuilder::new()
393 .with("process", "simple-metrics")
394 .build()
395 .unwrap();
396
397 let mut store: MetricStore<ServiceMetric> =
398 MetricStore::new().with_static_labels(static_labels);
399
400 let common_builder =
401 LabelsBuilder::from([("common_a", "some_value"), ("common_b", "other_value")]);
402
403 for s in states {
404 let state_labels = common_builder.clone().with("name", s.name).build().unwrap();
405
406 store.add_sample(
407 ServiceMetric::WorkerHealth,
408 Sample::new(&state_labels, s.health),
409 );
410 store.add_value(ServiceMetric::ServiceHeight, &state_labels, s.height)
411 }
412
413 let actual = store.render_into_metrics(None);
414
415 let expected = r#"# HELP worker_health worker health
416# TYPE worker_health gauge
417worker_health{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 1
418worker_health{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 0
419
420# HELP service_height service height
421# TYPE service_height gauge
422service_height{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 100
423service_height{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 200
424"#;
425 assert_eq!(actual, expected);
426 }
427
428 #[test]
429 fn simple_with_label_group() {
430 let states = vec![
431 SimpleState {
432 name: "a".into(),
433 health: true,
434 height: 100,
435 },
436 SimpleState {
437 name: "b".into(),
438 health: false,
439 height: 200,
440 },
441 ];
442
443 let static_labels = LabelsBuilder::new()
444 .with("process", "simple-metrics")
445 .build()
446 .unwrap();
447
448 let mut store: MetricStore<ServiceMetric> =
449 MetricStore::new().with_static_labels(static_labels);
450
451 let common_builder =
452 LabelsBuilder::from([("common_a", "some_value"), ("common_b", "other_value")]);
453
454 for s in states {
455 let state_labels = common_builder.clone().with("name", s.name).build().unwrap();
456
457 store.add_with_common_labels(
458 &state_labels,
459 &[
460 (ServiceMetric::WorkerHealth, s.health.into()),
461 (ServiceMetric::ServiceHeight, s.height.into()),
462 ],
463 );
464 }
465
466 let actual = store.render_into_metrics(None);
467
468 let expected = r#"# HELP worker_health worker health
469# TYPE worker_health gauge
470worker_health{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 1
471worker_health{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 0
472
473# HELP service_height service height
474# TYPE service_height gauge
475service_height{common_a="some_value",common_b="other_value",name="a",process="simple-metrics"} 100
476service_height{common_a="some_value",common_b="other_value",name="b",process="simple-metrics"} 200
477"#;
478 assert_eq!(actual, expected);
479 }
480
481 #[test]
482 fn simple_with_namespace() {
483 let states = vec![
484 SimpleState {
485 name: "a".into(),
486 health: true,
487 height: 100,
488 },
489 SimpleState {
490 name: "b".into(),
491 health: false,
492 height: 200,
493 },
494 ];
495
496 let static_labels = LabelsBuilder::new()
497 .with("process", "simple-metrics")
498 .build()
499 .unwrap();
500
501 let mut store: MetricStore<ServiceMetric> =
502 MetricStore::new().with_static_labels(static_labels);
503
504 for s in states {
505 let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
506
507 store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
508 store.add_value(ServiceMetric::ServiceHeight, &common, s.height)
509 }
510
511 let _cloned_store = store.clone();
512
513 let actual = store.render_into_metrics(Some("namespace"));
514 println!("{}", actual);
515
516 let expected = r#"# HELP namespace_worker_health worker health
517# TYPE namespace_worker_health gauge
518namespace_worker_health{name="a",process="simple-metrics"} 1
519namespace_worker_health{name="b",process="simple-metrics"} 0
520
521# HELP namespace_service_height service height
522# TYPE namespace_service_height gauge
523namespace_service_height{name="a",process="simple-metrics"} 100
524namespace_service_height{name="b",process="simple-metrics"} 200
525"#;
526 assert_eq!(actual, expected);
527 }
528
529 #[test]
530 fn simple_escape_label_values() {
531 let states = vec![
532 SimpleState {
533 name: r#""a""#.into(),
534 health: true,
535 height: 100,
536 },
537 SimpleState {
538 name: r#""b""#.into(),
539 health: false,
540 height: 200,
541 },
542 ];
543
544 let static_labels = LabelsBuilder::new()
545 .with("process", "simple-metrics")
546 .build()
547 .unwrap();
548
549 let mut store: MetricStore<ServiceMetric> =
550 MetricStore::new().with_static_labels(static_labels);
551
552 for s in states {
553 let common = LabelsBuilder::from([("name", s.name)]).build().unwrap();
554
555 store.add_sample(ServiceMetric::WorkerHealth, Sample::new(&common, s.health));
556 }
557
558 let _cloned_store = store.clone();
559
560 let actual = store.render_into_metrics(Some("namespace"));
561 println!("{}", actual);
562
563 let expected = r#"# HELP namespace_worker_health worker health
564# TYPE namespace_worker_health gauge
565namespace_worker_health{name="\"a\"",process="simple-metrics"} 1
566namespace_worker_health{name="\"b\"",process="simple-metrics"} 0
567"#;
568 assert_eq!(actual, expected);
569 }
570
571 #[test]
572 fn invalid_metric_name() {
573 let starting_with_a_digit = MetricDef::new(
574 "1starting_with_a_digit",
575 "starting with a digit",
576 MetricType::Gauge,
577 );
578 assert!(starting_with_a_digit.is_err());
579
580 let has_spaces = MetricDef::new(
581 "service health",
582 "has spaces in the metric name",
583 MetricType::Gauge,
584 );
585 assert!(has_spaces.is_err());
586
587 let has_weird_chars =
588 MetricDef::new("dobrĂ½_den", "has non ascii characters", MetricType::Gauge);
589 assert!(has_weird_chars.is_err());
590 }
591}