rustic_rs/metrics/
prometheus.rs1use anyhow::{Context, Result, bail};
2use log::debug;
3use prometheus::{Registry, register_gauge_with_registry};
4use reqwest::Url;
5use std::collections::BTreeMap;
6
7use crate::metrics::MetricValue::*;
8
9use super::{Metric, MetricsExporter};
10
11pub struct PrometheusExporter {
12 pub endpoint: Url,
13 pub job_name: String,
14 pub grouping: BTreeMap<String, String>,
15 pub prometheus_user: Option<String>,
16 pub prometheus_pass: Option<String>,
17}
18
19impl MetricsExporter for PrometheusExporter {
20 fn push_metrics(&self, metrics: &[Metric]) -> Result<()> {
21 use prometheus::{Encoder, ProtobufEncoder};
22 use reqwest::{StatusCode, blocking::Client, header::CONTENT_TYPE};
23
24 let registry = Registry::new();
25
26 for metric in metrics {
27 let gauge = register_gauge_with_registry!(metric.name, metric.description, registry)
28 .context("registering prometheus gauge")?;
29
30 gauge.set(match metric.value {
31 Int(i) => i as f64,
32 Float(f) => f,
33 });
34 }
35
36 let (full_url, encoded_metrics) = self.make_url_and_encoded_metrics(®istry)?;
37
38 debug!("using url: {full_url}");
39
40 let mut builder = Client::new()
41 .post(full_url)
42 .header(CONTENT_TYPE, ProtobufEncoder::new().format_type())
43 .body(encoded_metrics);
44
45 if let Some(username) = &self.prometheus_user {
46 debug!(
47 "using auth {} {}",
48 username,
49 self.prometheus_pass.as_deref().unwrap_or("[NOT SET]")
50 );
51 builder = builder.basic_auth(username, self.prometheus_pass.as_ref());
52 }
53
54 let response = builder.send()?;
55
56 match response.status() {
57 StatusCode::ACCEPTED | StatusCode::OK => Ok(()),
58 _ => bail!(
59 "unexpected status code {} while pushing to {}",
60 response.status(),
61 self.endpoint
62 ),
63 }
64 }
65}
66
67impl PrometheusExporter {
68 fn make_url_and_encoded_metrics(&self, registry: &Registry) -> Result<(Url, Vec<u8>)> {
70 use base64::prelude::*;
71 use prometheus::{Encoder, ProtobufEncoder};
72
73 let mut url_components = vec![
74 "metrics".to_string(),
75 "job@base64".to_string(),
76 BASE64_URL_SAFE_NO_PAD.encode(&self.job_name),
77 ];
78
79 for (ln, lv) in &self.grouping {
80 if !lv.is_empty() {
82 let name = ln.to_string() + "@base64";
84 url_components.push(name);
85 url_components.push(BASE64_URL_SAFE_NO_PAD.encode(lv));
86 }
87 }
88 let url = self.endpoint.join(&url_components.join("/"))?;
89
90 let encoder = ProtobufEncoder::new();
91 let mut buf = Vec::new();
92 for mf in registry.gather() {
93 let _ = encoder.encode(&[mf], &mut buf);
97 }
98
99 Ok((url, buf))
100 }
101}
102
103#[cfg(feature = "prometheus")]
104#[test]
105fn test_make_url_and_encoded_metrics() -> Result<()> {
106 use std::str::FromStr;
107
108 let grouping = [
109 ("abc", "xyz"),
110 ("path", "/my/path"),
111 ("tags", "a,b,cde"),
112 ("nogroup", ""),
113 ]
114 .into_iter()
115 .map(|(a, b)| (a.to_string(), b.to_string()))
116 .collect();
117
118 let exporter = PrometheusExporter {
119 endpoint: Url::from_str("http://host")?,
120 job_name: "test_job".to_string(),
121 grouping,
122 prometheus_user: None,
123 prometheus_pass: None,
124 };
125
126 let (url, _) = exporter.make_url_and_encoded_metrics(&Registry::new())?;
127 assert_eq!(
128 url.to_string(),
129 "http://host/metrics/job@base64/dGVzdF9qb2I/abc@base64/eHl6/path@base64/L215L3BhdGg/tags@base64/YSxiLGNkZQ"
130 );
131 Ok(())
132}