pub mod log;
pub mod multi;
pub mod noop;
#[cfg(feature = "observability-otel")]
pub mod otel;
#[cfg(feature = "observability-prometheus")]
pub mod prometheus;
pub mod runtime_trace;
pub mod traits;
pub mod verbose;
#[allow(unused_imports)]
pub use self::log::LogObserver;
pub use self::multi::MultiObserver;
pub use noop::NoopObserver;
#[cfg(feature = "observability-otel")]
pub use otel::OtelObserver;
#[cfg(feature = "observability-prometheus")]
pub use prometheus::PrometheusObserver;
pub use traits::{Observer, ObserverEvent};
#[allow(unused_imports)]
pub use verbose::VerboseObserver;
use crate::config::ObservabilityConfig;
use std::sync::{Arc, OnceLock};
pub fn create_observer(config: &ObservabilityConfig) -> Box<dyn Observer> {
let backends: Vec<&str> = config
.backend
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
match backends.as_slice() {
[] => create_single_observer(&config.backend, config),
[single] => create_single_observer(single, config),
many => Box::new(MultiObserver::new(
many.iter()
.map(|b| create_single_observer(b, config))
.collect(),
)),
}
}
fn create_single_observer(
backend: &str,
#[cfg_attr(not(feature = "observability-otel"), allow(unused_variables))]
config: &ObservabilityConfig,
) -> Box<dyn Observer> {
match backend {
"log" => Box::new(LogObserver::new()),
"verbose" => Box::new(VerboseObserver::new()),
"prometheus" => {
#[cfg(feature = "observability-prometheus")]
{
Box::new(PrometheusObserver::new())
}
#[cfg(not(feature = "observability-prometheus"))]
{
tracing::warn!(
"Prometheus backend requested but this build was compiled without `observability-prometheus`; falling back to noop."
);
Box::new(NoopObserver)
}
}
"otel" | "opentelemetry" | "otlp" => {
#[cfg(feature = "observability-otel")]
match OtelObserver::new(
config.otel_endpoint.as_deref(),
config.otel_service_name.as_deref(),
) {
Ok(obs) => {
tracing::info!(
endpoint = config
.otel_endpoint
.as_deref()
.unwrap_or("http://localhost:4318"),
"OpenTelemetry observer initialized"
);
Box::new(obs)
}
Err(e) => {
tracing::error!("Failed to create OTel observer: {e}. Falling back to noop.");
Box::new(NoopObserver)
}
}
#[cfg(not(feature = "observability-otel"))]
{
tracing::warn!(
"OpenTelemetry backend requested but this build was compiled without `observability-otel`; falling back to noop."
);
Box::new(NoopObserver)
}
}
"none" | "noop" => Box::new(NoopObserver),
_ => {
tracing::warn!("Unknown observability backend '{backend}', falling back to noop");
Box::new(NoopObserver)
}
}
}
static GLOBAL_OBSERVER: OnceLock<Arc<dyn Observer>> = OnceLock::new();
pub fn get_or_init_global(config: &ObservabilityConfig) -> Arc<dyn Observer> {
GLOBAL_OBSERVER
.get_or_init(|| Arc::from(create_observer(config)))
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factory_none_returns_noop() {
let cfg = ObservabilityConfig {
backend: "none".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_noop_returns_noop() {
let cfg = ObservabilityConfig {
backend: "noop".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_log_returns_log() {
let cfg = ObservabilityConfig {
backend: "log".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "log");
}
#[test]
fn factory_verbose_returns_verbose() {
let cfg = ObservabilityConfig {
backend: "verbose".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "verbose");
}
#[test]
fn factory_prometheus_returns_prometheus() {
let cfg = ObservabilityConfig {
backend: "prometheus".into(),
..ObservabilityConfig::default()
};
let expected = if cfg!(feature = "observability-prometheus") {
"prometheus"
} else {
"noop"
};
assert_eq!(create_observer(&cfg).name(), expected);
}
#[test]
fn factory_otel_returns_otel() {
let cfg = ObservabilityConfig {
backend: "otel".into(),
otel_endpoint: Some("http://127.0.0.1:19999".into()),
otel_service_name: Some("test".into()),
..ObservabilityConfig::default()
};
let expected = if cfg!(feature = "observability-otel") {
"otel"
} else {
"noop"
};
assert_eq!(create_observer(&cfg).name(), expected);
}
#[test]
fn factory_opentelemetry_alias() {
let cfg = ObservabilityConfig {
backend: "opentelemetry".into(),
otel_endpoint: Some("http://127.0.0.1:19999".into()),
otel_service_name: Some("test".into()),
..ObservabilityConfig::default()
};
let expected = if cfg!(feature = "observability-otel") {
"otel"
} else {
"noop"
};
assert_eq!(create_observer(&cfg).name(), expected);
}
#[test]
fn factory_otlp_alias() {
let cfg = ObservabilityConfig {
backend: "otlp".into(),
otel_endpoint: Some("http://127.0.0.1:19999".into()),
otel_service_name: Some("test".into()),
..ObservabilityConfig::default()
};
let expected = if cfg!(feature = "observability-otel") {
"otel"
} else {
"noop"
};
assert_eq!(create_observer(&cfg).name(), expected);
}
#[test]
fn factory_multiple_backends_returns_multi() {
let cfg = ObservabilityConfig {
backend: "log,verbose".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "multi");
}
#[test]
fn factory_multiple_backends_trims_whitespace_and_ignores_empties() {
let single = ObservabilityConfig {
backend: " log , ".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&single).name(), "log");
let multi = ObservabilityConfig {
backend: " log , verbose ".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&multi).name(), "multi");
}
#[test]
fn factory_unknown_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: "xyzzy_unknown".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_empty_string_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: String::new(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn factory_garbage_falls_back_to_noop() {
let cfg = ObservabilityConfig {
backend: "xyzzy_garbage_123".into(),
..ObservabilityConfig::default()
};
assert_eq!(create_observer(&cfg).name(), "noop");
}
#[test]
fn global_observer_is_a_shared_singleton() {
let first = get_or_init_global(&ObservabilityConfig {
backend: "log".into(),
..ObservabilityConfig::default()
});
let second = get_or_init_global(&ObservabilityConfig {
backend: "verbose".into(),
..ObservabilityConfig::default()
});
assert!(
Arc::ptr_eq(&first, &second),
"expected the same global observer instance on every call"
);
}
}