use cuenv_events::{CuenvEventLayer, EventBus, EventReceiver};
use std::io;
use std::sync::OnceLock;
pub use tracing::Level;
use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;
static GLOBAL_EVENT_BUS: OnceLock<EventBus> = OnceLock::new();
#[must_use]
pub fn subscribe_global_events() -> Option<EventReceiver> {
GLOBAL_EVENT_BUS.get().map(EventBus::subscribe)
}
pub fn shutdown_global_events() {
if let Some(bus) = GLOBAL_EVENT_BUS.get() {
bus.shutdown();
}
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum TracingFormat {
Pretty,
Compact,
Json,
Dev,
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<LogLevel> for Level {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Trace => Self::TRACE,
LogLevel::Debug => Self::DEBUG,
LogLevel::Info => Self::INFO,
LogLevel::Warn => Self::WARN,
LogLevel::Error => Self::ERROR,
}
}
}
impl std::str::FromStr for TracingFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pretty" => Ok(Self::Pretty),
"compact" => Ok(Self::Compact),
"json" => Ok(Self::Json),
"dev" => Ok(Self::Dev),
_ => Err(format!("Unknown tracing format: {s}")),
}
}
}
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub format: TracingFormat,
pub level: Level,
pub enable_correlation_ids: bool,
pub enable_timestamps: bool,
pub enable_file_location: bool,
pub filter: Option<String>,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
format: TracingFormat::Pretty,
level: Level::WARN, enable_correlation_ids: true,
enable_timestamps: true,
enable_file_location: true,
filter: None,
}
}
}
static CORRELATION_ID: std::sync::OnceLock<Uuid> = std::sync::OnceLock::new();
#[must_use]
pub fn correlation_id() -> Uuid {
*CORRELATION_ID.get_or_init(Uuid::new_v4)
}
pub fn init_tracing(config: TracingConfig) -> miette::Result<()> {
let correlation_id = correlation_id();
let env_filter = if let Some(filter) = config.filter {
EnvFilter::try_new(filter)
} else {
EnvFilter::try_from_default_env().or_else(|_| {
let level_str = match config.level {
Level::TRACE => "trace",
Level::DEBUG => "debug",
Level::INFO => "info",
Level::WARN => "warn",
Level::ERROR => "error",
};
EnvFilter::try_new(format!(
"cuenv={level_str},cuenv_cli={level_str},cuenv_core={level_str},cuengine={level_str}"
))
})
}
.map_err(|e| miette::miette!("Failed to create tracing filter: {e}"))?;
let registry = tracing_subscriber::registry().with(env_filter);
match config.format {
TracingFormat::Pretty => {
let layer = tracing_subscriber::fmt::layer()
.pretty()
.with_writer(io::stderr)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true);
registry.with(layer).init();
}
TracingFormat::Compact => {
let layer = tracing_subscriber::fmt::layer()
.compact()
.with_writer(io::stderr)
.with_target(false)
.with_thread_ids(false);
registry.with(layer).init();
}
TracingFormat::Json => {
let layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(io::stderr)
.with_current_span(true)
.with_span_list(true);
registry.with(layer).init();
}
TracingFormat::Dev => {
let layer = tracing_subscriber::fmt::layer()
.with_writer(io::stderr)
.with_file(config.enable_file_location)
.with_line_number(config.enable_file_location)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_level(true);
registry.with(layer).init();
}
}
tracing::info!(
correlation_id = %correlation_id,
version = env!("CARGO_PKG_VERSION"),
format = ?config.format,
"Tracing initialized for cuenv CLI"
);
Ok(())
}
#[allow(clippy::needless_pass_by_value)] pub fn init_tracing_with_events(config: TracingConfig) -> miette::Result<EventReceiver> {
let corr_id = correlation_id();
cuenv_events::set_correlation_id(corr_id);
let event_bus = EventBus::new();
let sender = event_bus
.sender()
.ok_or_else(|| miette::miette!("EventBus sender unavailable"))?;
let event_layer = CuenvEventLayer::new(sender.into_inner());
let main_receiver = event_bus.subscribe();
if GLOBAL_EVENT_BUS.set(event_bus).is_err() {
return Err(miette::miette!("Global event bus already initialized"));
}
let env_filter = if let Some(ref filter) = config.filter {
EnvFilter::try_new(filter)
} else {
EnvFilter::try_from_default_env().or_else(|_| {
let level_str = match config.level {
Level::TRACE => "trace",
Level::DEBUG => "debug",
Level::INFO => "info",
Level::WARN => "warn",
Level::ERROR => "error",
};
EnvFilter::try_new(format!(
"cuenv=info,cuenv_cli={level_str},cuenv_core={level_str},cuengine={level_str}"
))
})
}
.map_err(|e| miette::miette!("Failed to create tracing filter: {e}"))?;
let registry = tracing_subscriber::registry()
.with(env_filter)
.with(event_layer);
let is_verbose = config.level == Level::DEBUG || config.level == Level::TRACE;
match config.format {
TracingFormat::Json => {
let layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(io::stderr)
.with_current_span(true)
.with_span_list(true);
registry.with(layer).init();
}
TracingFormat::Pretty if is_verbose => {
let layer = tracing_subscriber::fmt::layer()
.pretty()
.with_writer(io::stderr)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true);
registry.with(layer).init();
}
TracingFormat::Compact if is_verbose => {
let layer = tracing_subscriber::fmt::layer()
.compact()
.with_writer(io::stderr)
.with_target(false)
.with_thread_ids(false);
registry.with(layer).init();
}
TracingFormat::Dev if is_verbose => {
let layer = tracing_subscriber::fmt::layer()
.with_writer(io::stderr)
.with_file(config.enable_file_location)
.with_line_number(config.enable_file_location)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_level(true);
registry.with(layer).init();
}
_ => {
registry.init();
}
}
tracing::debug!(
correlation_id = %corr_id,
version = env!("CARGO_PKG_VERSION"),
"Event-based tracing initialized"
);
Ok(main_receiver)
}
#[macro_export]
macro_rules! command_span {
($command:expr) => {
tracing::info_span!(
"command",
command = %$command,
correlation_id = %$crate::tracing::correlation_id(),
start_time = %chrono::Utc::now().to_rfc3339(),
)
};
($command:expr, $($key:expr => $value:expr),+ $(,)?) => {
tracing::info_span!(
"command",
command = %$command,
correlation_id = %$crate::tracing::correlation_id(),
start_time = %chrono::Utc::now().to_rfc3339(),
$($key = $value),+
)
};
}
pub use tracing::instrument;
#[macro_export]
macro_rules! perf_span {
($name:expr) => {
tracing::debug_span!(
"perf",
operation = %$name,
correlation_id = %$crate::tracing::correlation_id(),
)
};
}
#[macro_export]
macro_rules! perf_event {
($operation:expr, $duration:expr) => {
tracing::debug!(
operation = %$operation,
duration_ms = $duration.as_millis(),
correlation_id = %$crate::tracing::correlation_id(),
"Performance measurement"
);
};
($operation:expr, $duration:expr, $($key:expr => $value:expr),+ $(,)?) => {
tracing::debug!(
operation = %$operation,
duration_ms = $duration.as_millis(),
correlation_id = %$crate::tracing::correlation_id(),
$($key = $value),+
"Performance measurement"
);
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_parsing() {
assert!(matches!(
"pretty".parse::<TracingFormat>().unwrap(),
TracingFormat::Pretty
));
assert!(matches!(
"json".parse::<TracingFormat>().unwrap(),
TracingFormat::Json
));
assert!("invalid".parse::<TracingFormat>().is_err());
}
#[test]
fn test_format_parsing_all_variants() {
assert!(matches!(
"compact".parse::<TracingFormat>().unwrap(),
TracingFormat::Compact
));
assert!(matches!(
"dev".parse::<TracingFormat>().unwrap(),
TracingFormat::Dev
));
}
#[test]
fn test_format_parsing_case_insensitive() {
assert!(matches!(
"PRETTY".parse::<TracingFormat>().unwrap(),
TracingFormat::Pretty
));
assert!(matches!(
"JSON".parse::<TracingFormat>().unwrap(),
TracingFormat::Json
));
assert!(matches!(
"Compact".parse::<TracingFormat>().unwrap(),
TracingFormat::Compact
));
}
#[test]
fn test_log_level_conversion() {
assert_eq!(Level::from(LogLevel::Trace), Level::TRACE);
assert_eq!(Level::from(LogLevel::Debug), Level::DEBUG);
assert_eq!(Level::from(LogLevel::Info), Level::INFO);
assert_eq!(Level::from(LogLevel::Warn), Level::WARN);
assert_eq!(Level::from(LogLevel::Error), Level::ERROR);
}
#[test]
fn test_tracing_config_default() {
let config = TracingConfig::default();
assert!(matches!(config.format, TracingFormat::Pretty));
assert_eq!(config.level, Level::WARN);
assert!(config.enable_correlation_ids);
assert!(config.enable_timestamps);
assert!(config.enable_file_location);
assert!(config.filter.is_none());
}
#[test]
fn test_tracing_config_clone() {
let config = TracingConfig::default();
let cloned = config.clone();
assert_eq!(config.level, cloned.level);
assert_eq!(config.enable_timestamps, cloned.enable_timestamps);
}
#[test]
fn test_tracing_config_debug() {
let config = TracingConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("TracingConfig"));
}
#[test]
fn test_tracing_format_debug() {
let format = TracingFormat::Pretty;
let debug = format!("{:?}", format);
assert!(debug.contains("Pretty"));
}
#[test]
fn test_tracing_format_clone() {
let format = TracingFormat::Json;
let cloned = format.clone();
assert!(matches!(cloned, TracingFormat::Json));
}
#[test]
fn test_log_level_debug() {
let level = LogLevel::Info;
let debug = format!("{:?}", level);
assert!(debug.contains("Info"));
}
#[test]
fn test_log_level_clone() {
let level = LogLevel::Error;
let cloned = level.clone();
assert!(matches!(cloned, LogLevel::Error));
}
#[test]
fn test_correlation_id_consistency() {
let id1 = correlation_id();
let id2 = correlation_id();
assert_eq!(id1, id2);
}
#[test]
fn test_tracing_macros_smoke() {
let span = command_span!("unit_test_cmd");
let _entered = span.enter();
let pspan = perf_span!("perf_op");
let _e2 = pspan.enter();
let dur = std::time::Duration::from_millis(1);
perf_event!("perf_op", dur);
}
#[test]
fn test_subscribe_global_events_before_init() {
let result = subscribe_global_events();
let _ = result;
}
}