warmplane 0.7.0

Local control plane that keeps MCP sessions warm with compact capability/resource/prompt facades.
// Rust guideline compliant 2026-08-13

use anyhow::Result;
use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{runtime::Tokio, trace::TracerProvider, Resource};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

/// Resource guard managing OpenTelemetry tracer provider shutdown on drop.
pub struct TelemetryGuard {
    tracer_provider: Option<TracerProvider>,
}

impl Drop for TelemetryGuard {
    fn drop(&mut self) {
        if let Some(provider) = self.tracer_provider.take() {
            let _ = provider.shutdown();
        }
    }
}

/// Initializes structured logging subscriber and optional OpenTelemetry OTLP tracing pipeline.
///
/// # Returns
/// A `TelemetryGuard` ensuring provider shutdown on drop.
///
/// # Errors
/// Returns an error if OTLP exporter initialization fails.
pub fn init() -> Result<TelemetryGuard> {
    let env_filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info,warmplane=debug"));

    let fmt_layer = tracing_subscriber::fmt::layer()
        .json()
        .with_current_span(true)
        .with_span_list(true)
        .with_target(true)
        .with_file(false)
        .with_line_number(false)
        .with_thread_ids(false)
        .with_thread_names(false);

    let otel_enabled = std::env::var("WARMPLANE_OTEL_ENABLED")
        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
        .unwrap_or(false);

    if !otel_enabled {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(fmt_layer)
            .init();
        return Ok(TelemetryGuard {
            tracer_provider: None,
        });
    }

    let service_name =
        std::env::var("WARMPLANE_SERVICE_NAME").unwrap_or_else(|_| "warmplane".to_string());
    let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
        .or_else(|_| std::env::var("WARMPLANE_OTEL_ENDPOINT"))
        .unwrap_or_else(|_| "http://127.0.0.1:4317".to_string());

    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint(endpoint)
        .build()?;

    let provider = TracerProvider::builder()
        .with_batch_exporter(exporter, Tokio)
        .with_resource(Resource::new_with_defaults(vec![KeyValue::new(
            "service.name",
            service_name,
        )]))
        .build();

    let tracer = provider.tracer("warmplane");
    global::set_tracer_provider(provider.clone());

    let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);

    tracing_subscriber::registry()
        .with(env_filter)
        .with(fmt_layer)
        .with(otel_layer)
        .init();

    Ok(TelemetryGuard {
        tracer_provider: Some(provider),
    })
}