faucet_core/observability/
install.rs1use thiserror::Error;
6
7#[derive(Debug, Clone, Default)]
10pub struct ObservabilityConfig {
11 pub prometheus: Option<PrometheusConfig>,
12 pub tracing: Option<TracingConfig>,
13}
14
15#[derive(Debug, Clone)]
16pub struct PrometheusConfig {
17 pub listen: String,
20 pub buckets: Option<Vec<f64>>,
23}
24
25#[derive(Debug, Clone)]
26pub struct TracingConfig {
27 pub level: String,
29}
30
31#[derive(Debug, Clone, Default)]
34pub struct InstallReport {
35 pub prometheus_listen: Option<String>,
36 pub prometheus_already_installed: bool,
37 pub tracing_already_installed: bool,
38}
39
40#[derive(Debug, Error)]
41pub enum InstallError {
42 #[error("failed to bind Prometheus listener at {listen}: {source}")]
43 PrometheusBind {
44 listen: String,
45 #[source]
46 source: std::io::Error,
47 },
48 #[error("failed to install Prometheus recorder: {0}")]
49 PrometheusInstall(String),
50}
51
52#[cfg(feature = "observability-install")]
65pub fn install_observability(cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
66 let mut report = InstallReport::default();
67
68 if let Some(p) = cfg.prometheus.as_ref() {
69 use metrics_exporter_prometheus::{BuildError, PrometheusBuilder};
70
71 let listen: std::net::SocketAddr =
72 p.listen.parse().map_err(|e: std::net::AddrParseError| {
73 InstallError::PrometheusBind {
74 listen: p.listen.clone(),
75 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
76 }
77 })?;
78
79 const DEFAULT_BUCKETS: &[f64] = &[
80 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0,
81 ];
82 let buckets = p.buckets.as_deref().unwrap_or(DEFAULT_BUCKETS);
83
84 let builder = PrometheusBuilder::new()
85 .with_http_listener(listen)
86 .set_buckets(buckets)
87 .map_err(|e| InstallError::PrometheusInstall(e.to_string()))?;
88
89 match builder.install() {
90 Ok(()) => report.prometheus_listen = Some(p.listen.clone()),
91 Err(e) => match e {
95 BuildError::FailedToSetGlobalRecorder(_) => {
98 tracing::warn!("Prometheus recorder already installed; continuing");
99 report.prometheus_already_installed = true;
100 }
101 BuildError::FailedToCreateHTTPListener(msg) => {
107 return Err(InstallError::PrometheusBind {
108 listen: p.listen.clone(),
109 source: std::io::Error::other(msg),
110 });
111 }
112 other => return Err(InstallError::PrometheusInstall(other.to_string())),
113 },
114 }
115 }
116
117 if let Some(t) = cfg.tracing.as_ref() {
118 use tracing_subscriber::EnvFilter;
119 use tracing_subscriber::layer::SubscriberExt;
120 use tracing_subscriber::util::SubscriberInitExt;
121
122 let filter = EnvFilter::try_new(&t.level).unwrap_or_else(|_| EnvFilter::new("info"));
123 let registry = tracing_subscriber::registry()
124 .with(filter)
125 .with(tracing_subscriber::fmt::layer());
126 if registry.try_init().is_err() {
127 tracing::warn!("tracing subscriber already installed; continuing");
131 report.tracing_already_installed = true;
132 }
133 }
134
135 register_build_info();
138
139 Ok(report)
140}
141
142#[cfg(not(feature = "observability-install"))]
144pub fn install_observability(_cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
145 register_build_info();
146 Ok(InstallReport::default())
147}
148
149pub fn register_build_info() {
159 metrics::gauge!(
160 "faucet_build_info",
161 "version" => env!("CARGO_PKG_VERSION"),
162 )
163 .set(1.0);
164}
165
166#[cfg(all(test, feature = "observability-install"))]
167mod tests {
168 use super::*;
169 use std::sync::Mutex;
170
171 static LOCK: Mutex<()> = Mutex::new(());
172
173 #[test]
174 fn no_config_returns_empty_report() {
175 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
176 let r = install_observability(&ObservabilityConfig::default()).unwrap();
177 assert!(r.prometheus_listen.is_none());
178 assert!(!r.prometheus_already_installed);
179 assert!(!r.tracing_already_installed);
180 }
181
182 #[test]
183 fn malformed_listen_returns_bind_error() {
184 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
185 let cfg = ObservabilityConfig {
186 prometheus: Some(PrometheusConfig {
187 listen: "not-a-socket".into(),
188 buckets: None,
189 }),
190 tracing: None,
191 };
192 match install_observability(&cfg) {
193 Err(InstallError::PrometheusBind { .. }) => {}
194 other => panic!("expected PrometheusBind error, got {other:?}"),
195 }
196 }
197}