velocia 0.3.2

velocia – production-ready AI agent framework using ADK-Rust, A2A protocol, and AWS DynamoDB
//! OpenTelemetry-based observability.
//!
//! Mirrors Python's `Observability` singleton, supporting Phoenix (Arize)
//! and standard OTLP exporters.  Enabled via the `observability` feature.

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

#[cfg(any(feature = "adk", feature = "observability"))]
use tracing::info;
#[cfg(any(feature = "adk", feature = "observability"))]
use crate::error::Result;
#[cfg(feature = "adk")]
use crate::agents::factory::MonitoringConfig;

// ── Logging / tracing bootstrap ───────────────────────────────────────────────

/// Initialise a human-readable console tracing subscriber.
///
/// Uses compact single-line output suitable for `docker logs` and local dev.
/// Override the log level with the `RUST_LOG` environment variable
/// (e.g. `RUST_LOG=debug`).  Defaults to `info`.
pub fn init_console_tracing() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    tracing_subscriber::registry()
        .with(filter)
        .with(
            tracing_subscriber::fmt::layer()
                .compact()
                .with_target(false)
                .with_thread_ids(false)
                .with_thread_names(false),
        )
        .init();
}

/// Initialise a JSON tracing subscriber (structured logs for production ingestion).
pub fn init_tracing() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    tracing_subscriber::registry()
        .with(filter)
        .with(tracing_subscriber::fmt::layer().json())
        .init();
}

// ── OpenTelemetry / OTLP ──────────────────────────────────────────────────────

#[cfg(feature = "observability")]
pub struct TelemetryHandle {
    _provider: opentelemetry_sdk::trace::TracerProvider,
}

#[cfg(feature = "observability")]
impl TelemetryHandle {
    pub fn shutdown(self) {
        use opentelemetry::global;
        global::shutdown_tracer_provider();
    }
}

/// Set up OpenTelemetry tracing with an OTLP gRPC exporter.
///
/// `endpoint` should be in the form `http://host:port` (e.g. the Phoenix
/// collector at `http://localhost:4317`).
#[cfg(feature = "observability")]
pub fn init_otlp_tracing(
    endpoint: &str,
    service_name: &str,
) -> Result<TelemetryHandle> {
    use opentelemetry::global;
    use opentelemetry_otlp::WithExportConfig;
    use opentelemetry_sdk::{runtime, trace::TracerProvider};
    use crate::error::AgentKitError;

    let exporter = opentelemetry_otlp::new_exporter()
        .tonic()
        .with_endpoint(endpoint)
        .build_span_exporter()
        .map_err(|e| AgentKitError::Observability(e.to_string()))?;

    let provider = TracerProvider::builder()
        .with_batch_exporter(exporter, runtime::Tokio)
        .build();

    global::set_tracer_provider(provider.clone());

    info!("OTLP tracing initialised → {endpoint} (service: {service_name})");

    Ok(TelemetryHandle { _provider: provider })
}

// ── Factory helper ────────────────────────────────────────────────────────────

/// Initialise observability from `MonitoringConfig`.
/// Returns `None` when the `observability` feature is disabled.
#[cfg(feature = "adk")]
pub fn setup_from_config(monitoring: &MonitoringConfig) -> Result<()> {
    if let Some(phoenix) = &monitoring.phoenix {
        let endpoint = format!("http://{}:{}", phoenix.host, phoenix.port);

        #[cfg(feature = "observability")]
        {
            let service_name = phoenix.project_name.as_deref().unwrap_or("default");
            let _ = init_otlp_tracing(&endpoint, service_name)?;
        }

        #[cfg(not(feature = "observability"))]
        info!("Phoenix configured at {endpoint} but 'observability' feature is not enabled");
    }

    if let Some(arize) = &monitoring.arize {
        let _api_key = std::env::var("ARIZE_API_KEY").unwrap_or_default();
        let project = arize.arize_project_name.as_deref().unwrap_or("default");

        // TODO: Arize has a gRPC endpoint; wire up OTLP exporter with their
        // collector URL and api-key header once the arize-otel Rust crate is
        // available.  For now we log the configuration.
        info!(
            "Arize observability configured: space={}, project={project}",
            arize.arize_space_id
        );
    }

    Ok(())
}