at-jet 0.7.2

High-performance HTTP + Protobuf API framework for mobile services
Documentation
//! Tracing and logging initialization
//!
//! Provides unified tracing setup with:
//! - Configurable log level and format (JSON for k8s, pretty for local)
//! - Optional Jaeger integration for distributed tracing (requires `tracing-otel` feature)
//!
//! # Example
//!
//! ```ignore
//! use at_jet::tracing_init::{init_tracing, TracingConfig};
//!
//! let config = TracingConfig {
//!     level: "info".to_string(),
//!     format: "json".to_string(),
//!     jaeger_enabled: true,
//!     jaeger_endpoint: "http://jaeger:14268/api/traces".to_string(),
//!     service_name: "my-service".to_string(),
//!     ..Default::default()
//! };
//!
//! let _guard = init_tracing(&config);
//! ```

use {tracing::Level,
     tracing_subscriber::{EnvFilter,
                          fmt,
                          layer::SubscriberExt,
                          util::SubscriberInitExt}};

/// Tracing configuration
#[derive(Debug, Clone)]
pub struct TracingConfig {
  /// Log level: trace, debug, info, warn, error
  pub level:           String,
  /// Log format: json (for k8s) or pretty (for local dev)
  pub format:          String,
  /// Per-module log level filters (e.g., "sqlx=warn", "hyper=warn")
  pub env_filter_conf: Vec<String>,
  /// Enable Jaeger tracing (requires `tracing-otel` feature)
  pub jaeger_enabled:  bool,
  /// Jaeger collector endpoint
  pub jaeger_endpoint: String,
  /// Service name for Jaeger
  pub service_name:    String,
}

impl Default for TracingConfig {
  fn default() -> Self {
    Self {
      level:           "info".to_string(),
      format:          "pretty".to_string(),
      env_filter_conf: Vec::new(),
      jaeger_enabled:  false,
      jaeger_endpoint: "localhost:6831".to_string(),
      service_name:    "at-jet-service".to_string(),
    }
  }
}

/// Guard that shuts down the tracer provider when dropped
pub struct TracingGuard {
  _has_jaeger: bool,
}

impl Drop for TracingGuard {
  fn drop(&mut self) {
    #[cfg(feature = "tracing-otel")]
    if self._has_jaeger {
      opentelemetry::global::shutdown_tracer_provider();
    }
  }
}

/// Initialize tracing with the given configuration
///
/// Returns a guard that must be kept alive for the duration of the program.
/// When the guard is dropped, the Jaeger tracer provider is shut down.
///
/// When the `tracing-otel` feature is disabled, Jaeger fields are accepted but
/// ignored — only log format and filter are applied.
pub fn init_tracing(config: &TracingConfig) -> TracingGuard {
  let filter = build_filter(&config.level, &config.env_filter_conf);

  #[cfg(feature = "tracing-otel")]
  {
    let jaeger_tracer = if config.jaeger_enabled {
      match init_jaeger(&config.jaeger_endpoint, &config.service_name) {
        | Ok(tracer) => Some(tracer),
        | Err(e) => {
          eprintln!("Failed to initialize Jaeger: {}", e);
          None
        }
      }
    } else {
      None
    };

    let has_jaeger = jaeger_tracer.is_some();

    if config.format == "json" {
      if let Some(tracer) = jaeger_tracer {
        let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
        tracing_subscriber::registry()
          .with(filter)
          .with(fmt::layer().json().with_target(true).with_thread_ids(true))
          .with(telemetry)
          .init();
      } else {
        tracing_subscriber::registry()
          .with(filter)
          .with(fmt::layer().json().with_target(true).with_thread_ids(true))
          .init();
      }
    } else if let Some(tracer) = jaeger_tracer {
      let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
      tracing_subscriber::registry()
        .with(filter)
        .with(fmt::layer().with_target(true).with_thread_ids(true))
        .with(telemetry)
        .init();
    } else {
      tracing_subscriber::registry()
        .with(filter)
        .with(fmt::layer().with_target(true).with_thread_ids(true))
        .init();
    }

    TracingGuard {
      _has_jaeger: has_jaeger,
    }
  }

  #[cfg(not(feature = "tracing-otel"))]
  {
    if config.format == "json" {
      tracing_subscriber::registry()
        .with(filter)
        .with(fmt::layer().json().with_target(true).with_thread_ids(true))
        .init();
    } else {
      tracing_subscriber::registry()
        .with(filter)
        .with(fmt::layer().with_target(true).with_thread_ids(true))
        .init();
    }

    TracingGuard { _has_jaeger: false }
  }
}

/// Build EnvFilter from log level string and custom per-module filters
fn build_filter(level: &str, env_filter_conf: &[String]) -> EnvFilter {
  // First try to get from RUST_LOG env var
  EnvFilter::try_from_default_env().unwrap_or_else(|_| {
    let base_level = match level.to_lowercase().as_str() {
      | "trace" => Level::TRACE,
      | "debug" => Level::DEBUG,
      | "info" => Level::INFO,
      | "warn" | "warning" => Level::WARN,
      | "error" => Level::ERROR,
      | _ => Level::INFO,
    };

    let mut filter_parts = vec![base_level.to_string()];

    if !env_filter_conf.is_empty() {
      filter_parts.extend(env_filter_conf.iter().cloned());
    } else {
      // Default: reduce verbosity for noisy crates
      filter_parts.extend([
        "sqlx=warn".to_string(),
        "hyper=warn".to_string(),
        "reqwest=warn".to_string(),
        "h2=warn".to_string(),
        "tower=warn".to_string(),
        "rustls=warn".to_string(),
      ]);
    }

    EnvFilter::new(filter_parts.join(","))
  })
}

/// Initialize Jaeger tracer
///
/// Supports two modes based on endpoint format:
/// - HTTP collector: endpoint starts with "http://" or "https://"
/// - UDP agent: endpoint is host:port
#[cfg(feature = "tracing-otel")]
fn init_jaeger(
  endpoint: &str,
  service_name: &str,
) -> std::result::Result<opentelemetry_sdk::trace::Tracer, opentelemetry::trace::TraceError> {
  if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
    opentelemetry_jaeger::new_collector_pipeline()
      .with_service_name(service_name)
      .with_endpoint(endpoint)
      .with_reqwest()
      .install_batch(opentelemetry_sdk::runtime::Tokio)
  } else {
    opentelemetry_jaeger::new_agent_pipeline()
      .with_service_name(service_name)
      .with_endpoint(endpoint)
      .with_auto_split_batch(true)
      .install_batch(opentelemetry_sdk::runtime::Tokio)
  }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
  use super::*;

  #[test]
  fn test_tracing_config_default() {
    let config = TracingConfig::default();
    assert_eq!(config.level, "info");
    assert_eq!(config.format, "pretty");
    assert!(config.env_filter_conf.is_empty());
    assert!(!config.jaeger_enabled);
    assert_eq!(config.service_name, "at-jet-service");
  }

  #[test]
  fn test_build_filter_default_noisy_crate_suppression() {
    let filter = build_filter("info", &[]);
    let filter_str = format!("{}", filter);
    assert!(filter_str.contains("info"));
  }

  #[test]
  fn test_build_filter_custom_modules() {
    let custom = vec!["myapp=debug".to_string(), "sqlx=error".to_string()];
    let filter = build_filter("warn", &custom);
    let filter_str = format!("{}", filter);
    assert!(filter_str.contains("warn"));
  }

  #[test]
  fn test_build_filter_level_parsing() {
    // Valid levels
    let _ = build_filter("trace", &[]);
    let _ = build_filter("debug", &[]);
    let _ = build_filter("info", &[]);
    let _ = build_filter("warn", &[]);
    let _ = build_filter("warning", &[]);
    let _ = build_filter("error", &[]);

    // Invalid level defaults to info
    let _ = build_filter("invalid", &[]);
  }
}