rust_web_server/metrics/
tls.rs1use std::collections::{BTreeMap, HashMap};
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::{Mutex, OnceLock};
19
20type Negotiated = (&'static str, &'static str, &'static str);
21
22fn handshakes() -> &'static Mutex<HashMap<Negotiated, u64>> {
23 static HANDSHAKES: OnceLock<Mutex<HashMap<Negotiated, u64>>> = OnceLock::new();
24 HANDSHAKES.get_or_init(|| Mutex::new(HashMap::new()))
25}
26
27static FAILURES: AtomicU64 = AtomicU64::new(0);
28
29#[derive(Default)]
32struct Expiries {
33 configured: BTreeMap<String, i64>,
34 on_demand: BTreeMap<String, i64>,
35}
36
37fn expiries() -> &'static Mutex<Expiries> {
38 static EXPIRIES: OnceLock<Mutex<Expiries>> = OnceLock::new();
39 EXPIRIES.get_or_init(|| Mutex::new(Expiries::default()))
40}
41
42pub fn record_handshake(protocol: &'static str, cipher: &'static str, group: &'static str) {
44 let mut map = handshakes().lock().unwrap_or_else(|e| e.into_inner());
45 *map.entry((protocol, cipher, group)).or_insert(0) += 1;
46}
47
48pub fn record_handshake_failure() {
50 FAILURES.fetch_add(1, Ordering::Relaxed);
51}
52
53#[cfg(feature = "http2")]
55pub(crate) fn record_rustls(conn: &rustls::ServerConnection) {
56 let protocol = match conn.protocol_version() {
57 Some(rustls::ProtocolVersion::TLSv1_3) => "1.3",
58 Some(rustls::ProtocolVersion::TLSv1_2) => "1.2",
59 Some(other) => other.as_str().unwrap_or("unknown"),
60 None => "unknown",
61 };
62 let cipher = conn.negotiated_cipher_suite().and_then(|s| s.suite().as_str()).unwrap_or("unknown");
63 let group = conn.negotiated_key_exchange_group().and_then(|g| g.name().as_str()).unwrap_or("unknown");
64 record_handshake(protocol, cipher, group);
65}
66
67pub fn set_configured_certificates<'a>(certificates: impl IntoIterator<Item = (String, &'a [u8])>) {
70 let configured = certificates
71 .into_iter()
72 .filter_map(|(name, der)| Some((name, crate::core::x509::cert_info_from_der(der)?.not_after)))
73 .collect();
74 expiries().lock().unwrap_or_else(|e| e.into_inner()).configured = configured;
75}
76
77pub fn set_on_demand_certificate(name: &str, der: &[u8]) {
79 if let Some(info) = crate::core::x509::cert_info_from_der(der) {
80 expiries().lock().unwrap_or_else(|e| e.into_inner()).on_demand.insert(name.to_string(), info.not_after);
81 }
82}
83
84pub(crate) fn prometheus_text() -> String {
85 let mut out = String::new();
86 let mut map: Vec<(Negotiated, u64)> = handshakes().lock().unwrap_or_else(|e| e.into_inner()).iter().map(|(k, v)| (*k, *v)).collect();
87 let failures = FAILURES.load(Ordering::Relaxed);
88 if !map.is_empty() || failures > 0 {
89 map.sort();
90 out.push_str("# HELP rws_tls_handshakes_total Completed TLS handshakes by protocol version, cipher suite and key-exchange group\n");
91 out.push_str("# TYPE rws_tls_handshakes_total counter\n");
92 for ((protocol, cipher, group), n) in map {
93 out.push_str(&format!(
94 "rws_tls_handshakes_total{{tls_protocol_version=\"{protocol}\",tls_cipher=\"{cipher}\",tls_group=\"{group}\"}} {n}\n"
95 ));
96 }
97 out.push_str("# HELP rws_tls_handshake_failures_total TLS handshakes that failed\n");
98 out.push_str("# TYPE rws_tls_handshake_failures_total counter\n");
99 out.push_str(&format!("rws_tls_handshake_failures_total {failures}\n"));
100 }
101 let expiries = expiries().lock().unwrap_or_else(|e| e.into_inner());
102 let all: BTreeMap<&String, &i64> = expiries.on_demand.iter().chain(expiries.configured.iter()).collect();
103 if !all.is_empty() {
104 out.push_str("# HELP rws_tls_certificate_expiry_timestamp_seconds When each served certificate expires (notAfter), Unix seconds\n");
105 out.push_str("# TYPE rws_tls_certificate_expiry_timestamp_seconds gauge\n");
106 for (name, not_after) in all {
107 out.push_str(&format!(
108 "rws_tls_certificate_expiry_timestamp_seconds{{name=\"{}\"}} {not_after}\n",
109 name.replace('\\', "\\\\").replace('"', "\\\"")
110 ));
111 }
112 }
113 out
114}
115
116#[cfg(test)]
117mod tests;