linera_service/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 opentelemetry::{global, trace::TracerProvider};
7use opentelemetry_otlp::{SpanExporter, WithExportConfig};
8#[cfg(with_testing)]
9use opentelemetry_sdk::trace::InMemorySpanExporter;
10use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
11use tracing_opentelemetry::OpenTelemetryLayer;
12use tracing_subscriber::{
13    filter::{filter_fn, FilterFn},
14    layer::Layer,
15    prelude::__tracing_subscriber_SubscriberExt as _,
16    util::SubscriberInitExt,
17};
18
19/// Creates a filter that excludes spans with the `opentelemetry.skip` field.
20///
21/// Any span that declares an `opentelemetry.skip` field will be excluded from export,
22/// regardless of the field's value. This is a limitation of the tracing metadata API.
23///
24/// Usage examples:
25/// ```ignore
26/// // Always skip this span
27/// #[tracing::instrument(fields(opentelemetry.skip = true))]
28/// fn internal_helper() { }
29///
30/// // Conditionally skip based on a parameter
31/// #[tracing::instrument(fields(opentelemetry.skip = should_skip))]
32/// fn my_function(should_skip: bool) {
33///     // Will be skipped if should_skip is true when called
34///     // Note: The field must be declared in the span, so the span is
35///     // created with knowledge that it might be skipped
36/// }
37/// ```
38fn opentelemetry_skip_filter() -> FilterFn<impl Fn(&tracing::Metadata<'_>) -> bool> {
39    filter_fn(|metadata| {
40        if !metadata.is_span() {
41            return false;
42        }
43        metadata.fields().field("opentelemetry.skip").is_none()
44    })
45}
46
47/// Initializes tracing with a custom OpenTelemetry tracer provider.
48///
49/// This is an internal function used by both production and test code.
50fn init_with_tracer_provider(log_name: &str, tracer_provider: SdkTracerProvider) {
51    global::set_tracer_provider(tracer_provider.clone());
52    let tracer = tracer_provider.tracer("linera");
53
54    let opentelemetry_layer =
55        OpenTelemetryLayer::new(tracer).with_filter(opentelemetry_skip_filter());
56
57    let config = crate::tracing::get_env_config(log_name);
58    let maybe_log_file_layer = config.maybe_log_file_layer();
59    let stderr_layer = config.stderr_layer();
60
61    tracing_subscriber::registry()
62        .with(opentelemetry_layer)
63        .with(config.env_filter)
64        .with(maybe_log_file_layer)
65        .with(stderr_layer)
66        .init();
67}
68
69/// Builds an OpenTelemetry layer with the opentelemetry.skip filter.
70///
71/// This is used for testing to avoid setting the global subscriber.
72/// Returns the layer, exporter, and tracer provider (which must be kept alive and shutdown).
73#[cfg(with_testing)]
74pub fn build_opentelemetry_layer_with_test_exporter(
75    log_name: &str,
76) -> (
77    impl tracing_subscriber::Layer<tracing_subscriber::Registry>,
78    InMemorySpanExporter,
79    SdkTracerProvider,
80) {
81    let exporter = InMemorySpanExporter::default();
82    let exporter_clone = exporter.clone();
83
84    let resource = Resource::builder()
85        .with_service_name(log_name.to_string())
86        .build();
87
88    let tracer_provider = SdkTracerProvider::builder()
89        .with_resource(resource)
90        .with_simple_exporter(exporter)
91        .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
92        .build();
93
94    global::set_tracer_provider(tracer_provider.clone());
95    let tracer = tracer_provider.tracer("linera");
96    let opentelemetry_layer =
97        OpenTelemetryLayer::new(tracer).with_filter(opentelemetry_skip_filter());
98
99    (opentelemetry_layer, exporter_clone, tracer_provider)
100}
101
102/// Initializes tracing with OpenTelemetry OTLP exporter.
103///
104/// Exports traces using the OTLP protocol to any OpenTelemetry-compatible backend.
105/// Requires the `opentelemetry` feature.
106/// Only enables OpenTelemetry if LINERA_OTLP_EXPORTER_ENDPOINT env var is set.
107/// This prevents DNS errors in environments where OpenTelemetry is not deployed.
108pub fn init(log_name: &str, otlp_endpoint: Option<&str>) {
109    // Check if OpenTelemetry endpoint is configured via parameter or env var
110    let endpoint = match otlp_endpoint {
111        Some(ep) if !ep.is_empty() => ep.to_string(),
112        _ => match std::env::var("LINERA_OTLP_EXPORTER_ENDPOINT") {
113            Ok(ep) if !ep.is_empty() => ep,
114            _ => {
115                eprintln!(
116                    "LINERA_OTLP_EXPORTER_ENDPOINT not set and no endpoint provided. \
117                     Falling back to standard tracing without OpenTelemetry support."
118                );
119                crate::tracing::init(log_name);
120                return;
121            }
122        },
123    };
124
125    let resource = Resource::builder()
126        .with_service_name(log_name.to_string())
127        .build();
128
129    let exporter = SpanExporter::builder()
130        .with_tonic()
131        .with_endpoint(endpoint)
132        .build()
133        .expect("Failed to create OTLP exporter");
134
135    let tracer_provider = SdkTracerProvider::builder()
136        .with_resource(resource)
137        .with_batch_exporter(exporter)
138        .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
139        .build();
140
141    init_with_tracer_provider(log_name, tracer_provider);
142}