actr_runtime/
observability.rs

1use crate::error::RuntimeResult;
2use actr_config::ObservabilityConfig;
3#[cfg(feature = "opentelemetry")]
4use opentelemetry::{KeyValue, trace::TracerProvider as _};
5#[cfg(feature = "opentelemetry")]
6use opentelemetry_otlp::WithExportConfig;
7#[cfg(feature = "opentelemetry")]
8use opentelemetry_sdk::{
9    propagation::TraceContextPropagator, resource::Resource, trace::SdkTracerProvider,
10};
11use tracing_subscriber::{filter::EnvFilter, fmt, layer::SubscriberExt, prelude::*};
12
13/// Guard for observability resources. Shuts down tracing exporter on drop.
14#[derive(Default)]
15pub struct ObservabilityGuard {
16    #[cfg(feature = "opentelemetry")]
17    tracer_provider: Option<SdkTracerProvider>,
18}
19
20impl Drop for ObservabilityGuard {
21    fn drop(&mut self) {
22        #[cfg(feature = "opentelemetry")]
23        if let Some(provider) = self.tracer_provider.take() {
24            if let Err(err) = provider.shutdown() {
25                tracing::warn!("Failed to shutdown tracer provider: {err:?}");
26            }
27        }
28    }
29}
30
31/// Initialize logging + (optional) tracing subscriber.
32///
33/// - `RUST_LOG` wins over configured level; fallback to `info` if unset.
34/// - Tracing exporter only activates when both the `opentelemetry` feature is enabled and
35///   `cfg.tracing_enabled` is true.
36/// - Invalid endpoints fail fast; runtime delivery errors log but do not abort.
37pub fn init_observability(
38    cfg: &actr_config::ObservabilityConfig,
39) -> RuntimeResult<ObservabilityGuard> {
40    let level_directive = std::env::var("RUST_LOG")
41        .ok()
42        .filter(|s| !s.is_empty())
43        .unwrap_or_else(|| cfg.filter_level.clone());
44    let env_filter =
45        EnvFilter::try_new(level_directive.clone()).unwrap_or_else(|_| EnvFilter::new("info"));
46
47    init_subscriber(cfg, env_filter)
48}
49
50#[cfg(not(feature = "opentelemetry"))]
51fn init_subscriber(
52    _cfg: &ObservabilityConfig,
53    env_filter: EnvFilter,
54) -> RuntimeResult<ObservabilityGuard> {
55    let fmt_layer = fmt::layer()
56        .with_target(true)
57        .with_level(true)
58        .with_line_number(true)
59        .with_file(true);
60
61    let _ = tracing_subscriber::registry()
62        .with(env_filter)
63        .with(fmt_layer)
64        .try_init();
65    Ok(ObservabilityGuard::default())
66}
67
68#[cfg(feature = "opentelemetry")]
69fn init_subscriber(
70    cfg: &ObservabilityConfig,
71    env_filter: EnvFilter,
72) -> RuntimeResult<ObservabilityGuard> {
73    if cfg.tracing_enabled {
74        let provider = build_otel_provider(cfg)?;
75        let tracer = provider.tracer("actr-runtime");
76        let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
77        let fmt_layer = fmt::layer()
78            .with_target(true)
79            .with_level(true)
80            .with_line_number(true)
81            .with_file(true);
82
83        let _ = tracing_subscriber::registry()
84            .with(env_filter)
85            .with(otel_layer)
86            .with(fmt_layer)
87            .try_init();
88        Ok(ObservabilityGuard {
89            tracer_provider: Some(provider),
90        })
91    } else {
92        let fmt_layer = fmt::layer()
93            .with_target(true)
94            .with_level(true)
95            .with_line_number(true)
96            .with_file(true);
97
98        let _ = tracing_subscriber::registry()
99            .with(env_filter)
100            .with(fmt_layer)
101            .try_init();
102        Ok(ObservabilityGuard::default())
103    }
104}
105
106#[cfg(feature = "opentelemetry")]
107fn build_otel_provider(config: &ObservabilityConfig) -> RuntimeResult<SdkTracerProvider> {
108    let exporter = opentelemetry_otlp::SpanExporter::builder()
109        .with_tonic()
110        .with_endpoint(config.tracing_endpoint.clone())
111        .build()
112        .map_err(|e| {
113            crate::error::RuntimeError::InitializationError(format!(
114                "OTLP exporter build failed: {e}"
115            ))
116        })?;
117
118    let resource = Resource::builder()
119        .with_service_name(config.tracing_service_name.clone())
120        .with_attributes([KeyValue::new("telemetry.sdk.language", "rust")])
121        .build();
122
123    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
124        .with_resource(resource)
125        .with_batch_exporter(exporter)
126        .build();
127
128    opentelemetry::global::set_tracer_provider(tracer_provider.clone());
129    opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
130
131    Ok(tracer_provider)
132}