ggen-core 26.6.11

Core graph-aware code generation engine
Documentation
//! OpenTelemetry instrumentation for ggen
//!
//! This module provides OTLP (OpenTelemetry Protocol) tracing capabilities for
//! all ggen operations. It enables distributed tracing, performance monitoring,
//! and trace validation by clnrm tests.
//!
//! **NOTE**: OpenTelemetry support is optional. Enable with `--features otel` when building.
//!
//! ## Features
//!
//! - **OTLP Export**: Export traces via OTLP HTTP/gRPC
//! - **Structured Tracing**: Rich span attributes and events
//! - **Console Output**: Local debugging with formatted trace output
//! - **Service Identification**: Tag traces with service name and version
//!
//! ## Configuration
//!
//! Telemetry can be configured via environment variables:
//! - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP endpoint URL (default: http://localhost:4318)
//! - `OTEL_SERVICE_NAME`: Service name for traces (default: "ggen")
//! - `RUST_LOG`: Log level filter (default: "info")
//!
//! ## Examples
//!
//! ### Initializing Telemetry (with "otel" feature enabled)
//!
//! ```rust,no_run
//! use crate::telemetry::{init_telemetry, TelemetryConfig};
//!
//! # fn main() -> crate::utils::error::Result<()> {
//! let config = TelemetryConfig {
//!     endpoint: "http://localhost:4317".to_string(),
//!     service_name: "ggen".to_string(),
//!     console_output: true,
//! };
//!
//! let _guard = init_telemetry(config)?;
//! // Telemetry is now active
//! # Ok(())
//! # }
//! ```

use crate::utils::error::Result;

// OpenTelemetry implementation (only when "otel" feature is enabled)
#[cfg(feature = "otel")]
use crate::utils::error::Error;
#[cfg(feature = "otel")]
use opentelemetry::{global, KeyValue};
#[cfg(feature = "otel")]
use opentelemetry_sdk::trace::SdkTracerProvider;
#[cfg(feature = "otel")]
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};

/// OpenTelemetry configuration
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
    /// OTLP endpoint (default: http://localhost:4317)
    pub endpoint: String,
    /// Service name for traces
    pub service_name: String,
    /// Whether to enable console output
    pub console_output: bool,
}

impl Default for TelemetryConfig {
    fn default() -> Self {
        #[cfg(feature = "otel")]
        {
            Self {
                endpoint: std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
                    .unwrap_or_else(|_| "http://localhost:4317".to_string()),
                service_name: "ggen".to_string(),
                console_output: true,
            }
        }
        #[cfg(not(feature = "otel"))]
        {
            Self {
                endpoint: String::new(),
                service_name: "ggen".to_string(),
                console_output: false,
            }
        }
    }
}

/// Guard that shuts down the tracer provider on drop.
#[cfg(feature = "otel")]
pub struct TelemetryGuard {
    provider: SdkTracerProvider,
}

#[cfg(feature = "otel")]
impl Drop for TelemetryGuard {
    fn drop(&mut self) {
        let _ = self.provider.shutdown();
    }
}

/// Initialize OpenTelemetry with OTLP gRPC exporter
///
/// **NOTE**: This function requires the "otel" feature to be enabled.
/// When the "otel" feature is disabled, this function becomes a no-op.
///
/// This sets up:
/// - OTLP gRPC exporter for traces
/// - Tracing subscriber with OpenTelemetry layer
/// - Console output for local debugging
///
/// # Example
///
/// ```no_run
/// use crate::telemetry::{init_telemetry, TelemetryConfig};
///
/// #[tokio::main]
/// async fn main() -> crate::utils::error::Result<()> {
///     let config = TelemetryConfig::default();
///     let _guard = init_telemetry(config)?;
///
///     // Your application code here
///
///     Ok(())
/// }
/// ```
#[cfg(feature = "otel")]
pub fn init_telemetry(config: TelemetryConfig) -> Result<TelemetryGuard> {
    use opentelemetry_otlp::WithExportConfig;
    use opentelemetry_sdk::Resource;

    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint(&config.endpoint)
        .build()
        .map_err(|e| Error::with_context("Failed to create OTLP exporter", &e.to_string()))?;

    let resource = Resource::builder_empty()
        .with_attributes([
            KeyValue::new("service.name", config.service_name.clone()),
            KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
        ])
        .build();

    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(resource)
        .build();

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

    let tracer = global::tracer("ggen");
    let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);

    let subscriber = Registry::default()
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| {
            EnvFilter::new("info,opentelemetry=off,opentelemetry_sdk=off,opentelemetry_otlp=off")
        }))
        .with(telemetry_layer);

    if config.console_output {
        let fmt_layer = tracing_subscriber::fmt::layer()
            .with_target(true)
            .with_level(true);
        let _ = subscriber.with(fmt_layer).try_init();
    } else {
        let _ = subscriber.try_init();
    }

    tracing::info!(
        endpoint = %config.endpoint,
        service = %config.service_name,
        "OpenTelemetry initialized"
    );

    Ok(TelemetryGuard { provider })
}

/// No-op implementation when "otel" feature is disabled
#[cfg(not(feature = "otel"))]
pub fn init_telemetry(_config: TelemetryConfig) -> Result<()> {
    Ok(())
}

/// Shutdown OpenTelemetry and flush pending spans
///
/// **NOTE**: This function requires the "otel" feature to be enabled.
/// When the "otel" feature is disabled, this function becomes a no-op.
///
/// Call this before application exit to ensure all traces are exported.
/// Prefer using the `TelemetryGuard` returned by `init_telemetry()` instead,
/// which shuts down automatically on drop.
#[cfg(feature = "otel")]
pub fn shutdown_telemetry() {
    tracing::info!("OpenTelemetry shutdown requested — use TelemetryGuard for automatic flush");
    // In opentelemetry 0.31, shutdown is via SdkTracerProvider::shutdown().
    // Use the TelemetryGuard returned by init_telemetry() instead.
}

/// No-op implementation when "otel" feature is disabled
#[cfg(not(feature = "otel"))]
pub fn shutdown_telemetry() {
    // No-op: OpenTelemetry is disabled
}

/// Create a telemetry context with common attributes
///
/// This is useful for adding consistent context to spans across operations.
#[macro_export]
macro_rules! telemetry_context {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let span = tracing::Span::current();
            $(
                span.record($key, &tracing::field::display($value));
            )*
        }
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[cfg(feature = "otel")]
    fn test_telemetry_config_default() {
        let config = TelemetryConfig::default();
        assert_eq!(config.service_name, "ggen");
        assert!(config.console_output);
    }

    #[test]
    #[cfg(not(feature = "otel"))]
    fn test_telemetry_config_default_no_otel() {
        let config = TelemetryConfig::default();
        assert_eq!(config.service_name, "ggen");
        assert!(!config.console_output);
    }

    #[test]
    fn test_telemetry_config_custom() {
        let config = TelemetryConfig {
            endpoint: "http://custom:4317".to_string(),
            service_name: "test-service".to_string(),
            console_output: false,
        };
        assert_eq!(config.endpoint, "http://custom:4317");
        assert_eq!(config.service_name, "test-service");
        assert!(!config.console_output);
    }
}