mod log_sink;
mod otel;
use std::sync::Arc;
use leviath_core::config::{ObservabilityConfig, TelemetryExporterKind};
use leviath_core::telemetry::TelemetrySink;
pub use log_sink::LogSink;
pub use otel::OtelSink;
pub type LogLayer = Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync>;
pub struct BuiltTelemetry {
pub sink: Arc<dyn TelemetrySink>,
pub log_layer: Option<LogLayer>,
}
pub fn build_sink(cfg: &ObservabilityConfig) -> Option<BuiltTelemetry> {
if !cfg.enabled {
return None;
}
match cfg.exporter {
TelemetryExporterKind::None => None,
TelemetryExporterKind::Stdout => Some(BuiltTelemetry {
sink: Arc::new(LogSink),
log_layer: None,
}),
TelemetryExporterKind::Otlp => {
let cfg = cfg.clone();
let built = std::thread::spawn(move || OtelSink::from_config(&cfg))
.join()
.expect("exporter construction reports errors rather than panicking");
match built {
Ok(sink) => {
let log_layer = Some(sink.tracing_log_layer());
Some(BuiltTelemetry {
sink: Arc::new(sink),
log_layer,
})
}
Err(err) => {
tracing::warn!("telemetry disabled: {err}");
None
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config(enabled: bool, exporter: TelemetryExporterKind) -> ObservabilityConfig {
ObservabilityConfig {
enabled,
exporter,
endpoint: None,
service_name: None,
}
}
#[test]
fn disabled_config_builds_no_sink() {
assert!(build_sink(&config(false, TelemetryExporterKind::Otlp)).is_none());
}
#[test]
fn none_exporter_builds_no_sink() {
assert!(build_sink(&config(true, TelemetryExporterKind::None)).is_none());
}
#[test]
fn stdout_exporter_builds_the_log_sink_without_a_log_layer() {
let built = build_sink(&config(true, TelemetryExporterKind::Stdout)).unwrap();
assert!(built.log_layer.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn otlp_exporter_builds_from_a_runtime_thread_with_a_log_layer() {
let built = build_sink(&config(true, TelemetryExporterKind::Otlp)).unwrap();
assert!(built.log_layer.is_some());
}
#[test]
fn an_unparseable_endpoint_disables_telemetry_with_a_warning() {
let cfg = ObservabilityConfig {
enabled: true,
exporter: TelemetryExporterKind::Otlp,
endpoint: Some("not a url at all".to_string()),
service_name: None,
};
assert!(build_sink(&cfg).is_none());
}
}