#![cfg(any(feature = "prometheus", feature = "otlp-json", feature = "otlp"))]
#![allow(clippy::module_name_repetitions)]
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use csm_core_lib::error::{MemoryError, Result};
#[cfg(feature = "prometheus")]
pub mod prom;
#[cfg(feature = "otlp-json")]
pub mod otlp;
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
pub mod otlp_grpc;
static INITIALISED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
#[default]
Pretty,
Json,
Ndjson,
}
#[derive(Debug, Clone, Default)]
pub struct ObservabilityConfig {
pub service_name: String,
pub otlp_endpoint: Option<String>,
pub prometheus_bind: Option<SocketAddr>,
pub log_format: LogFormat,
}
#[must_use = "dropping the guard stops the prometheus scrape server and OTLP exporter"]
pub struct Guard {
#[cfg(feature = "prometheus")]
prom_handle: Option<prom::PromServerHandle>,
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
otel_guard: Option<otlp_grpc::OtlpGuard>,
_priv: (),
}
impl Drop for Guard {
fn drop(&mut self) {
#[cfg(feature = "prometheus")]
if let Some(handle) = self.prom_handle.take() {
handle.shutdown();
}
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
if let Some(mut guard) = self.otel_guard.take() {
guard.shutdown();
}
}
}
pub fn init(config: ObservabilityConfig) -> Result<Option<Guard>> {
if INITIALISED.swap(true, Ordering::SeqCst) {
return Err(MemoryError::ObservabilityAlreadyInitialised);
}
#[cfg(not(feature = "prometheus"))]
if config.prometheus_bind.is_some() {
INITIALISED.store(false, Ordering::SeqCst);
return Err(MemoryError::ObservabilityFeatureDisabled {
feature: "prometheus",
});
}
#[cfg(not(feature = "otlp-json"))]
if matches!(config.log_format, LogFormat::Json | LogFormat::Ndjson) {
INITIALISED.store(false, Ordering::SeqCst);
return Err(MemoryError::ObservabilityFeatureDisabled {
feature: "otlp-json",
});
}
#[cfg(all(not(feature = "otlp"), not(target_arch = "wasm32")))]
if config.otlp_endpoint.is_some() {
INITIALISED.store(false, Ordering::SeqCst);
return Err(MemoryError::ObservabilityFeatureDisabled { feature: "otlp" });
}
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
let otel_guard = if let Some(ref endpoint) = config.otlp_endpoint {
Some(otlp_grpc::install_grpc_tracer(
endpoint,
&config.service_name,
)?)
} else {
None
};
#[cfg(not(all(feature = "otlp", not(target_arch = "wasm32"))))]
let _otel_guard: Option<()> = None;
#[cfg(feature = "otlp-json")]
{
if matches!(config.log_format, LogFormat::Json | LogFormat::Ndjson) {
otlp::install_json_subscriber();
}
}
let have_prom_server = cfg!(feature = "prometheus") && config.prometheus_bind.is_some();
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
let have_otel = config.otlp_endpoint.is_some();
#[cfg(not(all(feature = "otlp", not(target_arch = "wasm32"))))]
let have_otel = false;
#[cfg(feature = "prometheus")]
let prom_handle = if let Some(bind) = config.prometheus_bind {
Some(prom::start_server(bind)?)
} else {
None
};
#[cfg(not(feature = "prometheus"))]
let _ = have_prom_server;
tracing::info!(
service = %config.service_name,
otlp_endpoint = ?config.otlp_endpoint,
prometheus_bind = ?config.prometheus_bind,
"observability initialised"
);
if !have_prom_server && !have_otel && matches!(config.log_format, LogFormat::Pretty) {
INITIALISED.store(false, Ordering::SeqCst);
return Ok(None);
}
Ok(Some(Guard {
#[cfg(feature = "prometheus")]
prom_handle,
#[cfg(all(feature = "otlp", not(target_arch = "wasm32")))]
otel_guard,
_priv: (),
}))
}
pub fn render_metrics() -> Result<String> {
#[cfg(feature = "prometheus")]
{
prom::render()
}
#[cfg(not(feature = "prometheus"))]
{
Err(MemoryError::ObservabilityFeatureDisabled {
feature: "prometheus",
})
}
}