use thiserror::Error;
#[derive(Debug, Clone, Default)]
pub struct ObservabilityConfig {
pub prometheus: Option<PrometheusConfig>,
pub tracing: Option<TracingConfig>,
}
#[derive(Debug, Clone)]
pub struct PrometheusConfig {
pub listen: String,
pub buckets: Option<Vec<f64>>,
}
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub level: String,
}
#[derive(Debug, Clone, Default)]
pub struct InstallReport {
pub prometheus_listen: Option<String>,
pub prometheus_already_installed: bool,
pub tracing_already_installed: bool,
}
#[derive(Debug, Error)]
pub enum InstallError {
#[error("failed to bind Prometheus listener at {listen}: {source}")]
PrometheusBind {
listen: String,
#[source]
source: std::io::Error,
},
#[error("failed to install Prometheus recorder: {0}")]
PrometheusInstall(String),
}
#[cfg(feature = "observability-install")]
pub fn install_observability(cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
let mut report = InstallReport::default();
if let Some(p) = cfg.prometheus.as_ref() {
use metrics_exporter_prometheus::{BuildError, PrometheusBuilder};
let listen: std::net::SocketAddr =
p.listen.parse().map_err(|e: std::net::AddrParseError| {
InstallError::PrometheusBind {
listen: p.listen.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
}
})?;
const DEFAULT_BUCKETS: &[f64] = &[
0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0,
];
let buckets = p.buckets.as_deref().unwrap_or(DEFAULT_BUCKETS);
let builder = PrometheusBuilder::new()
.with_http_listener(listen)
.set_buckets(buckets)
.map_err(|e| InstallError::PrometheusInstall(e.to_string()))?;
match builder.install() {
Ok(()) => report.prometheus_listen = Some(p.listen.clone()),
Err(e) => match e {
BuildError::FailedToSetGlobalRecorder(_) => {
tracing::warn!("Prometheus recorder already installed; continuing");
report.prometheus_already_installed = true;
}
BuildError::FailedToCreateHTTPListener(msg) => {
return Err(InstallError::PrometheusBind {
listen: p.listen.clone(),
source: std::io::Error::other(msg),
});
}
other => return Err(InstallError::PrometheusInstall(other.to_string())),
},
}
}
if let Some(t) = cfg.tracing.as_ref() {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let filter = EnvFilter::try_new(&t.level).unwrap_or_else(|_| EnvFilter::new("info"));
let registry = tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer());
if registry.try_init().is_err() {
tracing::warn!("tracing subscriber already installed; continuing");
report.tracing_already_installed = true;
}
}
register_build_info();
Ok(report)
}
#[cfg(not(feature = "observability-install"))]
pub fn install_observability(_cfg: &ObservabilityConfig) -> Result<InstallReport, InstallError> {
register_build_info();
Ok(InstallReport::default())
}
pub fn register_build_info() {
metrics::gauge!(
"faucet_build_info",
"version" => env!("CARGO_PKG_VERSION"),
)
.set(1.0);
}
#[cfg(all(test, feature = "observability-install"))]
mod tests {
use super::*;
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
#[test]
fn no_config_returns_empty_report() {
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
let r = install_observability(&ObservabilityConfig::default()).unwrap();
assert!(r.prometheus_listen.is_none());
assert!(!r.prometheus_already_installed);
assert!(!r.tracing_already_installed);
}
#[test]
fn malformed_listen_returns_bind_error() {
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
let cfg = ObservabilityConfig {
prometheus: Some(PrometheusConfig {
listen: "not-a-socket".into(),
buckets: None,
}),
tracing: None,
};
match install_observability(&cfg) {
Err(InstallError::PrometheusBind { .. }) => {}
other => panic!("expected PrometheusBind error, got {other:?}"),
}
}
}