Skip to main content

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