use std::sync::OnceLock;
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter, Layer,
};
#[derive(Debug, Clone, Copy, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
Compact,
}
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub level: String,
pub format: OutputFormat,
pub with_thread_ids: bool,
pub with_thread_names: bool,
pub with_target: bool,
pub with_file: bool,
pub with_line_number: bool,
pub span_events: FmtSpan,
pub ansi: bool,
pub env_filter_override: Option<String>,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: OutputFormat::default(),
with_thread_ids: false,
with_thread_names: false,
with_target: true,
with_file: false,
with_line_number: false,
span_events: FmtSpan::NONE,
ansi: true,
env_filter_override: None,
}
}
}
impl TracingConfig {
pub fn production() -> Self {
Self {
level: "info".to_string(),
format: OutputFormat::Json,
with_thread_ids: true,
with_thread_names: true,
with_target: true,
with_file: false,
with_line_number: false,
span_events: FmtSpan::CLOSE,
ansi: false,
env_filter_override: None,
}
}
pub fn development() -> Self {
Self {
level: "debug".to_string(),
format: OutputFormat::Text,
with_thread_ids: false,
with_thread_names: false,
with_target: true,
with_file: true,
with_line_number: true,
span_events: FmtSpan::NEW | FmtSpan::CLOSE,
ansi: true,
env_filter_override: None,
}
}
pub fn testing() -> Self {
Self {
level: "trace".to_string(),
format: OutputFormat::Compact,
with_thread_ids: false,
with_thread_names: false,
with_target: false,
with_file: false,
with_line_number: false,
span_events: FmtSpan::NONE,
ansi: false,
env_filter_override: None,
}
}
}
static TRACING_INITIALIZED: OnceLock<bool> = OnceLock::new();
pub fn init_tracing(config: TracingConfig) {
if TRACING_INITIALIZED.get().is_some() {
return;
}
let env_filter = if let Some(ref filter) = config.env_filter_override {
EnvFilter::new(filter)
} else {
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&config.level))
};
let result = TRACING_INITIALIZED.set(true);
if result.is_err() {
return;
}
match config.format {
OutputFormat::Json => {
let layer = fmt::layer()
.json()
.with_thread_ids(config.with_thread_ids)
.with_thread_names(config.with_thread_names)
.with_target(config.with_target)
.with_file(config.with_file)
.with_line_number(config.with_line_number)
.with_span_events(config.span_events)
.with_filter(env_filter);
if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
eprintln!("Failed to initialize tracing: {:?}", e);
}
}
OutputFormat::Text => {
let layer = fmt::layer()
.with_thread_ids(config.with_thread_ids)
.with_thread_names(config.with_thread_names)
.with_target(config.with_target)
.with_file(config.with_file)
.with_line_number(config.with_line_number)
.with_span_events(config.span_events)
.with_ansi(config.ansi)
.with_filter(env_filter);
if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
eprintln!("Failed to initialize tracing: {:?}", e);
}
}
OutputFormat::Compact => {
let layer = fmt::layer()
.compact()
.with_thread_ids(config.with_thread_ids)
.with_thread_names(config.with_thread_names)
.with_target(config.with_target)
.with_file(config.with_file)
.with_line_number(config.with_line_number)
.with_ansi(config.ansi)
.with_filter(env_filter);
if let Err(e) = tracing_subscriber::registry().with(layer).try_init() {
eprintln!("Failed to initialize tracing: {:?}", e);
}
}
}
}
pub fn init_default() {
init_tracing(TracingConfig::default());
}
pub fn is_initialized() -> bool {
TRACING_INITIALIZED.get().is_some()
}
use std::sync::atomic::{AtomicU64, Ordering};
use uuid::Uuid;
static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn generate_request_id() -> String {
let uuid = Uuid::new_v4();
let uuid_prefix = &uuid.to_string().replace('-', "")[..8];
let counter = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{:06}", uuid_prefix, counter)
}
pub fn generate_span_id() -> String {
let uuid = Uuid::new_v4();
uuid.to_string().replace('-', "")[..16].to_string()
}
#[macro_export]
macro_rules! query_span {
($request_id:expr) => {
tracing::info_span!(
"query",
request_id = %$request_id,
sdk.component = "query",
sdk.version = env!("CARGO_PKG_VERSION")
)
};
}
#[macro_export]
macro_rules! transport_span {
($operation:expr, $transport_type:expr) => {
tracing::debug_span!(
"transport",
operation = %$operation,
transport_type = %$transport_type,
sdk.component = "transport"
)
};
}
#[macro_export]
macro_rules! skill_span {
($skill_name:expr, $operation:expr) => {
tracing::info_span!(
"skill",
skill_name = %$skill_name,
operation = %$operation,
sdk.component = "skills"
)
};
}
#[macro_export]
macro_rules! pool_span {
($operation:expr) => {
tracing::debug_span!(
"connection_pool",
operation = %$operation,
sdk.component = "pool"
)
};
}
#[macro_export]
macro_rules! mcp_span {
($tool_name:expr, $operation:expr) => {
tracing::info_span!(
"mcp",
tool_name = %$tool_name,
operation = %$operation,
sdk.component = "mcp"
)
};
}
#[macro_export]
macro_rules! log_error_with_category {
($error:expr, $category:expr, $message:expr) => {
match $category {
$crate::errors::ErrorCategory::Network => {
tracing::error!(
error.category = "network",
error.code = %$error.error_code(),
error.retryable = $error.is_retryable(),
error.http_status = $error.http_status().code(),
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Process => {
tracing::error!(
error.category = "process",
error.code = %$error.error_code(),
error.retryable = $error.is_retryable(),
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Parsing => {
tracing::error!(
error.category = "parsing",
error.code = %$error.error_code(),
error.retryable = false,
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Configuration => {
tracing::error!(
error.category = "configuration",
error.code = %$error.error_code(),
error.retryable = false,
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Validation => {
tracing::error!(
error.category = "validation",
error.code = %$error.error_code(),
error.retryable = false,
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Permission => {
tracing::error!(
error.category = "permission",
error.code = %$error.error_code(),
error.retryable = false,
error.http_status = $error.http_status().code(),
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Resource => {
tracing::error!(
error.category = "resource",
error.code = %$error.error_code(),
error.retryable = $error.is_retryable(),
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::Internal => {
tracing::error!(
error.category = "internal",
error.code = %$error.error_code(),
error.retryable = false,
message = %$message,
error = %$error
)
}
$crate::errors::ErrorCategory::External => {
tracing::error!(
error.category = "external",
error.code = %$error.error_code(),
error.retryable = $error.is_retryable(),
message = %$message,
error = %$error
)
}
}
};
}
#[macro_export]
macro_rules! log_retryable_error {
($error:expr, $attempt:expr, $max_attempts:expr, $message:expr) => {
tracing::warn!(
error.category = ?$error.category(),
error.code = %$error.error_code(),
retry.attempt = $attempt,
retry.max_attempts = $max_attempts,
message = %$message,
error = %$error,
"Retryable error, will retry"
)
};
}
pub fn log_timing(operation: &str, duration_ms: u64, labels: &[(&str, &str)]) {
let labels_str = labels
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",");
tracing::info!(
metric.name = operation,
metric.kind = "timing",
metric.value_ms = duration_ms,
metric.labels = %labels_str,
"Operation completed"
);
}
pub fn log_counter(name: &str, increment: u64, labels: &[(&str, &str)]) {
let labels_str = labels
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",");
tracing::debug!(
metric.name = name,
metric.kind = "counter",
metric.increment = increment,
metric.labels = %labels_str,
"Counter incremented"
);
}
pub fn log_gauge(name: &str, value: f64, labels: &[(&str, &str)]) {
let labels_str = labels
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",");
tracing::debug!(
metric.name = name,
metric.kind = "gauge",
metric.value = value,
metric.labels = %labels_str,
"Gauge recorded"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_request_id() {
let id1 = generate_request_id();
let id2 = generate_request_id();
assert_ne!(id1, id2);
assert_eq!(id1.len(), 15); assert!(id1.contains('-'));
let parts: Vec<&str> = id1.split('-').collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 6);
}
#[test]
fn test_generate_span_id() {
let span_id = generate_span_id();
assert_eq!(span_id.len(), 16);
assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_tracing_config_defaults() {
let config = TracingConfig::default();
assert_eq!(config.level, "info");
assert!(matches!(config.format, OutputFormat::Text));
assert!(config.with_target);
assert!(!config.with_file);
}
#[test]
fn test_tracing_config_production() {
let config = TracingConfig::production();
assert_eq!(config.level, "info");
assert!(matches!(config.format, OutputFormat::Json));
assert!(config.with_thread_ids);
assert!(!config.ansi);
}
#[test]
fn test_tracing_config_development() {
let config = TracingConfig::development();
assert_eq!(config.level, "debug");
assert!(matches!(config.format, OutputFormat::Text));
assert!(config.ansi);
assert!(config.with_file);
}
#[test]
fn test_is_initialized_before_init() {
let _ = is_initialized();
}
#[test]
fn test_log_timing() {
log_timing("test_operation", 100, &[("key", "value")]);
}
#[test]
fn test_log_counter() {
log_counter("test_counter", 1, &[("endpoint", "/api/test")]);
}
#[test]
fn test_log_gauge() {
log_gauge("test_gauge", 42.5, &[("location", "room1")]);
}
}