linera_base/
tracing_opentelemetry.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! OpenTelemetry integration for tracing with OTLP export and Chrome trace export.
5
6use tracing_chrome::ChromeLayerBuilder;
7use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
8#[cfg(feature = "opentelemetry")]
9use {
10    opentelemetry::{global, trace::TracerProvider},
11    opentelemetry_otlp::{SpanExporter, WithExportConfig},
12    opentelemetry_sdk::{
13        trace::{InMemorySpanExporter, SdkTracerProvider},
14        Resource,
15    },
16    tracing_opentelemetry::OpenTelemetryLayer,
17    tracing_subscriber::{
18        filter::{filter_fn, FilterFn},
19        layer::Layer,
20    },
21};
22
23/// Creates a filter that excludes spans with the `opentelemetry.skip` field.
24///
25/// Any span that declares an `opentelemetry.skip` field will be excluded from export,
26/// regardless of the field's value. This is a limitation of the tracing metadata API.
27///
28/// Usage examples:
29/// ```ignore
30/// // Always skip this span
31/// #[tracing::instrument(fields(opentelemetry.skip = true))]
32/// fn internal_helper() { }
33///
34/// // Conditionally skip based on a parameter
35/// #[tracing::instrument(fields(opentelemetry.skip = should_skip))]
36/// fn my_function(should_skip: bool) {
37///     // Will be skipped if should_skip is true when called
38///     // Note: The field must be declared in the span, so the span is
39///     // created with knowledge that it might be skipped
40/// }
41/// ```
42#[cfg(feature = "opentelemetry")]
43fn opentelemetry_skip_filter() -> FilterFn<impl Fn(&tracing::Metadata<'_>) -> bool> {
44    filter_fn(|metadata| {
45        if !metadata.is_span() {
46            return false;
47        }
48        metadata.fields().field("opentelemetry.skip").is_none()
49    })
50}
51
52/// Initializes tracing with a custom OpenTelemetry tracer provider.
53///
54/// This is an internal function used by both production and test code.
55#[cfg(feature = "opentelemetry")]
56fn init_with_tracer_provider(log_name: &str, tracer_provider: SdkTracerProvider) {
57    global::set_tracer_provider(tracer_provider.clone());
58    let tracer = tracer_provider.tracer("linera");
59
60    let opentelemetry_layer =
61        OpenTelemetryLayer::new(tracer).with_filter(opentelemetry_skip_filter());
62
63    let config = crate::tracing::get_env_config(log_name);
64    let maybe_log_file_layer = config.maybe_log_file_layer();
65    let stderr_layer = config.stderr_layer();
66
67    tracing_subscriber::registry()
68        .with(opentelemetry_layer)
69        .with(config.env_filter)
70        .with(maybe_log_file_layer)
71        .with(stderr_layer)
72        .init();
73}
74
75/// Builds an OpenTelemetry layer with the opentelemetry.skip filter.
76///
77/// This is used for testing to avoid setting the global subscriber.
78/// Returns the layer, exporter, and tracer provider (which must be kept alive and shutdown).
79#[cfg(all(with_testing, feature = "opentelemetry"))]
80pub fn build_opentelemetry_layer_with_test_exporter(
81    log_name: &str,
82) -> (
83    impl tracing_subscriber::Layer<tracing_subscriber::Registry>,
84    InMemorySpanExporter,
85    SdkTracerProvider,
86) {
87    let exporter = InMemorySpanExporter::default();
88    let exporter_clone = exporter.clone();
89
90    let resource = Resource::builder()
91        .with_service_name(log_name.to_string())
92        .build();
93
94    let tracer_provider = SdkTracerProvider::builder()
95        .with_resource(resource)
96        .with_simple_exporter(exporter)
97        .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
98        .build();
99
100    global::set_tracer_provider(tracer_provider.clone());
101    let tracer = tracer_provider.tracer("linera");
102    let opentelemetry_layer =
103        OpenTelemetryLayer::new(tracer).with_filter(opentelemetry_skip_filter());
104
105    (opentelemetry_layer, exporter_clone, tracer_provider)
106}
107
108/// Initializes tracing with OpenTelemetry OTLP exporter.
109///
110/// Exports traces using the OTLP protocol to any OpenTelemetry-compatible backend.
111/// Requires the `opentelemetry` feature.
112/// Only enables OpenTelemetry if LINERA_OTLP_EXPORTER_ENDPOINT env var is set.
113/// This prevents DNS errors in environments where OpenTelemetry is not deployed.
114#[cfg(feature = "opentelemetry")]
115pub fn init_with_opentelemetry(log_name: &str, otlp_endpoint: Option<&str>) {
116    // Check if OpenTelemetry endpoint is configured via parameter or env var
117    let endpoint = match otlp_endpoint {
118        Some(ep) if !ep.is_empty() => ep.to_string(),
119        _ => match std::env::var("LINERA_OTLP_EXPORTER_ENDPOINT") {
120            Ok(ep) if !ep.is_empty() => ep,
121            _ => {
122                eprintln!(
123                    "LINERA_OTLP_EXPORTER_ENDPOINT not set and no endpoint provided. \
124                     Falling back to standard tracing without OpenTelemetry support."
125                );
126                crate::tracing::init(log_name);
127                return;
128            }
129        },
130    };
131
132    let resource = Resource::builder()
133        .with_service_name(log_name.to_string())
134        .build();
135
136    let exporter = SpanExporter::builder()
137        .with_tonic()
138        .with_endpoint(endpoint)
139        .build()
140        .expect("Failed to create OTLP exporter");
141
142    let tracer_provider = SdkTracerProvider::builder()
143        .with_resource(resource)
144        .with_batch_exporter(exporter)
145        .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
146        .build();
147
148    init_with_tracer_provider(log_name, tracer_provider);
149}
150
151/// Fallback when opentelemetry feature is not enabled.
152#[cfg(not(feature = "opentelemetry"))]
153pub fn init_with_opentelemetry(log_name: &str, _otlp_endpoint: Option<&str>) {
154    eprintln!(
155        "OTLP export requires the 'opentelemetry' feature to be enabled! Falling back to default tracing initialization."
156    );
157    crate::tracing::init(log_name);
158}
159
160/// Guard that flushes Chrome trace file when dropped.
161///
162/// Store this guard in a variable that lives for the duration of your program.
163/// When it's dropped, the trace file will be completed and closed.
164pub type ChromeTraceGuard = tracing_chrome::FlushGuard;
165
166/// Builds a Chrome trace layer and guard.
167///
168/// Returns a subscriber and guard. The subscriber should be used with `with_default`
169/// to avoid global state conflicts.
170pub fn build_chrome_trace_layer_with_exporter<W>(
171    log_name: &str,
172    writer: W,
173) -> (impl tracing::Subscriber + Send + Sync, ChromeTraceGuard)
174where
175    W: std::io::Write + Send + 'static,
176{
177    let (chrome_layer, guard) = ChromeLayerBuilder::new().writer(writer).build();
178
179    let config = crate::tracing::get_env_config(log_name);
180    let maybe_log_file_layer = config.maybe_log_file_layer();
181    let stderr_layer = config.stderr_layer();
182
183    let subscriber = tracing_subscriber::registry()
184        .with(chrome_layer)
185        .with(config.env_filter)
186        .with(maybe_log_file_layer)
187        .with(stderr_layer);
188
189    (subscriber, guard)
190}
191
192/// Initializes tracing with Chrome Trace JSON exporter.
193///
194/// Returns a guard that must be kept alive for the duration of the program.
195/// When the guard is dropped, the trace data is flushed and completed.
196///
197/// Exports traces to Chrome Trace JSON format which can be visualized in:
198/// - Chrome: `chrome://tracing`
199/// - Perfetto UI: <https://ui.perfetto.dev>
200///
201/// Note: Uses `try_init()` to avoid panicking if a global subscriber is already set.
202/// In that case, tracing may not work as expected.
203pub fn init_with_chrome_trace_exporter<W>(log_name: &str, writer: W) -> ChromeTraceGuard
204where
205    W: std::io::Write + Send + 'static,
206{
207    let (subscriber, guard) = build_chrome_trace_layer_with_exporter(log_name, writer);
208    let _ = subscriber.try_init();
209    guard
210}