Skip to main content

liteforge/
otel_init.rs

1//! Optional OTel initialisation helpers.
2//!
3//! When the `otel` cargo feature is enabled, [`init_otel`] sets the
4//! global tracer provider and W3C propagator so that
5//! `#[tracing::instrument]`-emitted spans (and the headers injected in
6//! [`crate::transport::build_headers`]) are exported via OTLP HTTP/proto
7//! to the configured endpoint.
8//!
9//! When the feature is off, [`init_otel`] is a no-op that returns `Ok(())`.
10//! This lets binding crates (Python, JS, Java) expose `init_otel(...)`
11//! unconditionally, calling it in a feature-disabled build is harmless.
12
13use crate::config::OtelConfig;
14use crate::error::Result;
15#[cfg(feature = "otel")]
16use crate::error::ForgeError;
17
18/// Initialise the global OTel tracer provider + propagator from
19/// [`OtelConfig`]. Idempotent: safe to call multiple times. When the
20/// `otel` cargo feature is disabled, returns `Ok(())` without doing
21/// anything.
22///
23/// Existing `OTEL_EXPORTER_OTLP_*` env vars are honoured automatically by
24/// the OTLP exporter as a fallback for any field not supplied here.
25pub fn init_otel(config: &OtelConfig) -> Result<()> {
26    init_otel_inner(config)
27}
28
29#[cfg(feature = "otel")]
30fn init_otel_inner(config: &OtelConfig) -> Result<()> {
31    use opentelemetry::global;
32    use opentelemetry::KeyValue;
33    use opentelemetry_otlp::WithExportConfig;
34    use opentelemetry_sdk::propagation::TraceContextPropagator;
35    use opentelemetry_sdk::trace::Config as SdkConfig;
36    use opentelemetry_sdk::Resource;
37    use std::sync::OnceLock;
38
39    static INIT: OnceLock<()> = OnceLock::new();
40    if INIT.get().is_some() {
41        return Ok(());
42    }
43
44    // Resource attributes, start with service.name (highest priority),
45    // merge in the caller's resource_attributes map.
46    let mut resource_kvs: Vec<KeyValue> = Vec::new();
47    if let Some(name) = &config.service_name {
48        resource_kvs.push(KeyValue::new("service.name", name.clone()));
49    }
50    for (k, v) in &config.resource_attributes {
51        resource_kvs.push(KeyValue::new(k.clone(), v.clone()));
52    }
53    let resource = Resource::new(resource_kvs);
54
55    // OTLP HTTP/proto exporter. Endpoint may be None, the SDK will fall
56    // back to OTEL_EXPORTER_OTLP_ENDPOINT.
57    let mut exporter_builder = opentelemetry_otlp::new_exporter()
58        .http()
59        .with_protocol(opentelemetry_otlp::Protocol::HttpBinary);
60    if let Some(endpoint) = &config.endpoint {
61        exporter_builder = exporter_builder.with_endpoint(endpoint.clone());
62    }
63    if !config.headers.is_empty() {
64        exporter_builder = exporter_builder.with_headers(config.headers.clone());
65    }
66
67    let tracer_provider = opentelemetry_otlp::new_pipeline()
68        .tracing()
69        .with_exporter(exporter_builder)
70        .with_trace_config(SdkConfig::default().with_resource(resource))
71        .install_batch(opentelemetry_sdk::runtime::Tokio)
72        .map_err(|e| ForgeError::config(format!("failed to install OTLP pipeline: {}", e)))?;
73
74    global::set_text_map_propagator(TraceContextPropagator::new());
75    global::set_tracer_provider(tracer_provider);
76    let _ = INIT.set(());
77    Ok(())
78}
79
80#[cfg(not(feature = "otel"))]
81#[inline]
82fn init_otel_inner(_config: &OtelConfig) -> Result<()> {
83    // Feature disabled, caller's request is a no-op rather than an error
84    // so that bindings can expose init_otel() unconditionally.
85    Ok(())
86}
87
88/// True when this build of the SDK was compiled with the `otel` feature
89/// enabled. Bindings can use this to decide whether to log a warning when
90/// the user requests OTel from a feature-disabled build.
91pub const fn otel_feature_enabled() -> bool {
92    cfg!(feature = "otel")
93}