linera_service/tracing/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module provides unified handling for tracing subscribers within Linera binaries.
5
6pub mod chrome;
7pub mod opentelemetry;
8
9use std::{
10    env,
11    fs::{File, OpenOptions},
12    path::Path,
13    sync::Arc,
14};
15
16use is_terminal::IsTerminal as _;
17use tracing::Subscriber;
18use tracing_subscriber::{
19    fmt::{
20        self,
21        format::{FmtSpan, Format, Full},
22        time::FormatTime,
23        FormatFields, MakeWriter,
24    },
25    layer::{Layer, SubscriberExt as _},
26    registry::LookupSpan,
27    util::SubscriberInitExt,
28    EnvFilter,
29};
30#[cfg(not(target_arch = "wasm32"))]
31use {
32    ::opentelemetry::trace::TraceContextExt as _, tracing_opentelemetry::OtelData,
33    tracing_subscriber::fmt::FormatEvent,
34};
35
36pub(crate) struct EnvConfig {
37    pub(crate) env_filter: EnvFilter,
38    span_events: FmtSpan,
39    format: Option<String>,
40    color_output: bool,
41    log_name: String,
42}
43
44impl EnvConfig {
45    pub(crate) fn stderr_layer<S>(&self) -> Box<dyn Layer<S> + Send + Sync>
46    where
47        S: Subscriber + for<'span> LookupSpan<'span>,
48    {
49        prepare_formatted_layer(
50            self.format.as_deref(),
51            fmt::layer()
52                .with_span_events(self.span_events.clone())
53                .with_writer(std::io::stderr)
54                .with_ansi(self.color_output),
55        )
56    }
57
58    pub(crate) fn maybe_log_file_layer<S>(&self) -> Option<Box<dyn Layer<S> + Send + Sync>>
59    where
60        S: Subscriber + for<'span> LookupSpan<'span>,
61    {
62        open_log_file(&self.log_name).map(|file_writer| {
63            prepare_formatted_layer(
64                self.format.as_deref(),
65                fmt::layer()
66                    .with_span_events(self.span_events.clone())
67                    .with_writer(Arc::new(file_writer))
68                    .with_ansi(false),
69            )
70        })
71    }
72}
73
74/// Initializes tracing in a standard way.
75///
76/// The environment variables `RUST_LOG`, `RUST_LOG_SPAN_EVENTS`, and `RUST_LOG_FORMAT`
77/// can be used to control the verbosity, the span event verbosity, and the output format,
78/// respectively.
79///
80/// The `LINERA_LOG_DIR` environment variable can be used to configure a directory to
81/// store log files. If it is set, a file named `log_name` with the `log` extension is
82/// created in the directory.
83pub fn init(log_name: &str) {
84    let config = get_env_config(log_name);
85    let maybe_log_file_layer = config.maybe_log_file_layer();
86    let stderr_layer = config.stderr_layer();
87
88    tracing_subscriber::registry()
89        .with(config.env_filter)
90        .with(maybe_log_file_layer)
91        .with(stderr_layer)
92        .init();
93}
94
95pub(crate) fn get_env_config(log_name: &str) -> EnvConfig {
96    let env_filter = EnvFilter::builder()
97        .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
98        .from_env_lossy();
99
100    let span_events = std::env::var("RUST_LOG_SPAN_EVENTS")
101        .ok()
102        .map_or(FmtSpan::NONE, |s| fmt_span_from_str(&s));
103
104    let format = std::env::var("RUST_LOG_FORMAT").ok();
105    let color_output =
106        !std::env::var("NO_COLOR").is_ok_and(|x| !x.is_empty()) && std::io::stderr().is_terminal();
107
108    EnvConfig {
109        env_filter,
110        span_events,
111        format,
112        color_output,
113        log_name: log_name.to_string(),
114    }
115}
116
117/// Opens a log file for writing.
118///
119/// The location of the file is determined by the `LINERA_LOG_DIR` environment variable,
120/// and its name by the `log_name` parameter.
121///
122/// Returns [`None`] if the `LINERA_LOG_DIR` environment variable is not set.
123pub(crate) fn open_log_file(log_name: &str) -> Option<File> {
124    let log_directory = env::var_os("LINERA_LOG_DIR")?;
125    let mut log_file_path = Path::new(&log_directory).join(log_name);
126    log_file_path.set_extension("log");
127
128    Some(
129        OpenOptions::new()
130            .append(true)
131            .create(true)
132            .open(log_file_path)
133            .expect("Failed to open log file for writing"),
134    )
135}
136
137#[cfg(not(target_arch = "wasm32"))]
138struct WithTraceContext;
139
140#[cfg(not(target_arch = "wasm32"))]
141impl<S, N> FormatEvent<S, N> for WithTraceContext
142where
143    S: Subscriber + for<'span> LookupSpan<'span>,
144    N: for<'writer> FormatFields<'writer> + 'static,
145{
146    fn format_event(
147        &self,
148        ctx: &fmt::FmtContext<'_, S, N>,
149        mut writer: fmt::format::Writer<'_>,
150        event: &tracing::Event<'_>,
151    ) -> std::fmt::Result {
152        if let Some(scope) = ctx.event_scope() {
153            for span in scope {
154                let extensions = span.extensions();
155                if let Some(otel_data) = extensions.get::<OtelData>() {
156                    // For root spans, trace_id is on the builder.
157                    // For child spans, it's inherited from the parent context.
158                    let trace_id = otel_data
159                        .builder
160                        .trace_id
161                        .unwrap_or_else(|| otel_data.parent_cx.span().span_context().trace_id());
162                    if trace_id != ::opentelemetry::trace::TraceId::INVALID {
163                        write!(writer, "traceID={trace_id} ")?;
164                    }
165                    if let Some(span_id) = otel_data.builder.span_id {
166                        write!(writer, "spanID={span_id} ")?;
167                    }
168                    break;
169                }
170            }
171        }
172        Format::default().format_event(ctx, writer, event)
173    }
174}
175
176/// Applies a requested `formatting` to the log output of the provided `layer`.
177///
178/// Returns a boxed [`Layer`] with the formatting applied to the original `layer`.
179pub(crate) fn prepare_formatted_layer<S, N, W, T>(
180    formatting: Option<&str>,
181    layer: fmt::Layer<S, N, Format<Full, T>, W>,
182) -> Box<dyn Layer<S> + Send + Sync>
183where
184    S: Subscriber + for<'span> LookupSpan<'span>,
185    N: for<'writer> FormatFields<'writer> + Send + Sync + 'static,
186    W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
187    T: FormatTime + Send + Sync + 'static,
188{
189    match formatting.unwrap_or("plain") {
190        "json" => layer.json().boxed(),
191        "pretty" => layer.pretty().boxed(),
192        "plain" => {
193            #[cfg(not(target_arch = "wasm32"))]
194            {
195                layer.event_format(WithTraceContext).boxed()
196            }
197            #[cfg(target_arch = "wasm32")]
198            {
199                layer.boxed()
200            }
201        }
202        format => {
203            panic!("Invalid RUST_LOG_FORMAT: `{format}`.  Valid values are `json` or `pretty`.")
204        }
205    }
206}
207
208pub(crate) fn fmt_span_from_str(events: &str) -> FmtSpan {
209    let mut fmt_span = FmtSpan::NONE;
210    for event in events.split(',') {
211        fmt_span |= match event {
212            "new" => FmtSpan::NEW,
213            "enter" => FmtSpan::ENTER,
214            "exit" => FmtSpan::EXIT,
215            "close" => FmtSpan::CLOSE,
216            "active" => FmtSpan::ACTIVE,
217            "full" => FmtSpan::FULL,
218            _ => FmtSpan::NONE,
219        };
220    }
221    fmt_span
222}