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