actr_runtime/
observability.rs1use 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#[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
31pub 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}