featherbit/acme/
metrics.rs1use std::sync::Arc;
5
6use prometheus::{IntCounterVec, IntGaugeVec, Opts, Registry};
7
8use super::CertState;
9
10pub struct AcmeMetrics {
11 pub not_after: IntGaugeVec,
13 pub state: IntGaugeVec,
15 pub renewals: IntCounterVec,
17 pub last_attempt: IntGaugeVec,
19}
20
21impl AcmeMetrics {
22 pub fn register(registry: &Registry) -> Result<Arc<Self>, prometheus::Error> {
26 let not_after = IntGaugeVec::new(
27 Opts::new(
28 "featherbit_acme_cert_not_after_timestamp_seconds",
29 "Expiry of the served managed certificate (unix seconds; 0 = placeholder)",
30 ),
31 &["cert_id"],
32 )
33 .unwrap();
34 let state = IntGaugeVec::new(
35 Opts::new(
36 "featherbit_acme_cert_state",
37 "Current state of a managed certificate (1 = current)",
38 ),
39 &["cert_id", "state"],
40 )
41 .unwrap();
42 let renewals = IntCounterVec::new(
43 Opts::new(
44 "featherbit_acme_renewals_total",
45 "ACME issuance/renewal attempts by outcome",
46 ),
47 &["cert_id", "result"],
48 )
49 .unwrap();
50 let last_attempt = IntGaugeVec::new(
51 Opts::new(
52 "featherbit_acme_last_renewal_attempt_timestamp_seconds",
53 "Unix time of the last issuance attempt",
54 ),
55 &["cert_id"],
56 )
57 .unwrap();
58 for c in [
59 Box::new(not_after.clone()) as Box<dyn prometheus::core::Collector>,
60 Box::new(state.clone()),
61 Box::new(renewals.clone()),
62 Box::new(last_attempt.clone()),
63 ] {
64 registry.register(c)?;
65 }
66 Ok(Arc::new(Self {
67 not_after,
68 state,
69 renewals,
70 last_attempt,
71 }))
72 }
73
74 pub fn observe(&self, cert_id: &str, state: CertState, not_after: i64) {
75 self.not_after.with_label_values(&[cert_id]).set(not_after);
76 for s in CertState::ALL {
77 self.state
78 .with_label_values(&[cert_id, s.as_str()])
79 .set(i64::from(s == state));
80 }
81 }
82
83 pub fn attempt(&self, cert_id: &str, success: bool, at: i64) {
84 self.renewals
85 .with_label_values(&[cert_id, if success { "success" } else { "failure" }])
86 .inc();
87 self.last_attempt.with_label_values(&[cert_id]).set(at);
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use prometheus::{Encoder, TextEncoder};
95
96 fn render(r: &prometheus::Registry) -> String {
97 let mut buf = Vec::new();
98 TextEncoder::new().encode(&r.gather(), &mut buf).unwrap();
99 String::from_utf8(buf).unwrap()
100 }
101
102 #[test]
103 fn series_render_with_expected_names_and_labels() {
104 let registry = prometheus::Registry::new();
105 let m = AcmeMetrics::register(®istry).unwrap();
106 m.observe("a.example.com", CertState::Issued, 1_800_000_000);
107 m.attempt("a.example.com", true, 1_700_000_000);
108 m.attempt("a.example.com", false, 1_700_000_100);
109 let out = render(®istry);
110 assert!(out.contains("featherbit_acme_cert_not_after_timestamp_seconds{cert_id=\"a.example.com\"} 1800000000"), "{out}");
111 assert!(out
112 .contains("featherbit_acme_cert_state{cert_id=\"a.example.com\",state=\"issued\"} 1"));
113 assert!(out.contains(
114 "featherbit_acme_cert_state{cert_id=\"a.example.com\",state=\"placeholder\"} 0"
115 ));
116 assert!(out.contains(
117 "featherbit_acme_cert_state{cert_id=\"a.example.com\",state=\"renewing\"} 0"
118 ));
119 assert!(out
120 .contains("featherbit_acme_cert_state{cert_id=\"a.example.com\",state=\"failed\"} 0"));
121 assert!(out.contains(
122 "featherbit_acme_renewals_total{cert_id=\"a.example.com\",result=\"success\"} 1"
123 ));
124 assert!(out.contains(
125 "featherbit_acme_renewals_total{cert_id=\"a.example.com\",result=\"failure\"} 1"
126 ));
127 assert!(out.contains("featherbit_acme_last_renewal_attempt_timestamp_seconds{cert_id=\"a.example.com\"} 1700000100"));
128 m.observe("b.example.com", CertState::Placeholder, 0);
130 assert!(render(®istry).contains(
131 "featherbit_acme_cert_not_after_timestamp_seconds{cert_id=\"b.example.com\"} 0"
132 ));
133 assert!(matches!(
135 AcmeMetrics::register(®istry),
136 Err(prometheus::Error::AlreadyReg)
137 ));
138 }
139}