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
28#[cfg(not(target_arch = "wasm32"))]
29pub use crate::tracing_opentelemetry::{
30    init_with_chrome_trace_exporter, init_with_opentelemetry, ChromeTraceGuard,
31};
32
33pub(crate) struct EnvConfig {
34    pub(crate) env_filter: EnvFilter,
35    span_events: FmtSpan,
36    format: Option<String>,
37    color_output: bool,
38    log_name: String,
39}
40
41impl EnvConfig {
42    pub(crate) fn stderr_layer<S>(&self) -> Box<dyn Layer<S> + Send + Sync>
43    where
44        S: Subscriber + for<'span> LookupSpan<'span>,
45    {
46        prepare_formatted_layer(
47            self.format.as_deref(),
48            fmt::layer()
49                .with_span_events(self.span_events.clone())
50                .with_writer(std::io::stderr)
51                .with_ansi(self.color_output),
52        )
53    }
54
55    pub(crate) fn maybe_log_file_layer<S>(&self) -> Option<Box<dyn Layer<S> + Send + Sync>>
56    where
57        S: Subscriber + for<'span> LookupSpan<'span>,
58    {
59        open_log_file(&self.log_name).map(|file_writer| {
60            prepare_formatted_layer(
61                self.format.as_deref(),
62                fmt::layer()
63                    .with_span_events(self.span_events.clone())
64                    .with_writer(Arc::new(file_writer))
65                    .with_ansi(false),
66            )
67        })
68    }
69}
70
71/// Initializes tracing in a standard way.
72///
73/// The environment variables `RUST_LOG`, `RUST_LOG_SPAN_EVENTS`, and `RUST_LOG_FORMAT`
74/// can be used to control the verbosity, the span event verbosity, and the output format,
75/// respectively.
76///
77/// The `LINERA_LOG_DIR` environment variable can be used to configure a directory to
78/// store log files. If it is set, a file named `log_name` with the `log` extension is
79/// created in the directory.
80pub fn init(log_name: &str) {
81    let config = get_env_config(log_name);
82    let maybe_log_file_layer = config.maybe_log_file_layer();
83    let stderr_layer = config.stderr_layer();
84
85    tracing_subscriber::registry()
86        .with(config.env_filter)
87        .with(maybe_log_file_layer)
88        .with(stderr_layer)
89        .init();
90}
91
92pub(crate) fn get_env_config(log_name: &str) -> EnvConfig {
93    let env_filter = EnvFilter::builder()
94        .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
95        .from_env_lossy();
96
97    let span_events = std::env::var("RUST_LOG_SPAN_EVENTS")
98        .ok()
99        .map_or(FmtSpan::NONE, |s| fmt_span_from_str(&s));
100
101    let format = std::env::var("RUST_LOG_FORMAT").ok();
102    let color_output =
103        !std::env::var("NO_COLOR").is_ok_and(|x| !x.is_empty()) && std::io::stderr().is_terminal();
104
105    EnvConfig {
106        env_filter,
107        span_events,
108        format,
109        color_output,
110        log_name: log_name.to_string(),
111    }
112}
113
114/// Opens a log file for writing.
115///
116/// The location of the file is determined by the `LINERA_LOG_DIR` environment variable,
117/// and its name by the `log_name` parameter.
118///
119/// Returns [`None`] if the `LINERA_LOG_DIR` environment variable is not set.
120pub(crate) fn open_log_file(log_name: &str) -> Option<File> {
121    let log_directory = env::var_os("LINERA_LOG_DIR")?;
122    let mut log_file_path = Path::new(&log_directory).join(log_name);
123    log_file_path.set_extension("log");
124
125    Some(
126        OpenOptions::new()
127            .append(true)
128            .create(true)
129            .open(log_file_path)
130            .expect("Failed to open log file for writing"),
131    )
132}
133
134/// Applies a requested `formatting` to the log output of the provided `layer`.
135///
136/// Returns a boxed [`Layer`] with the formatting applied to the original `layer`.
137pub(crate) fn prepare_formatted_layer<S, N, W, T>(
138    formatting: Option<&str>,
139    layer: fmt::Layer<S, N, Format<Full, T>, W>,
140) -> Box<dyn Layer<S> + Send + Sync>
141where
142    S: Subscriber + for<'span> LookupSpan<'span>,
143    N: for<'writer> FormatFields<'writer> + Send + Sync + 'static,
144    W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
145    T: FormatTime + Send + Sync + 'static,
146{
147    match formatting.unwrap_or("plain") {
148        "json" => layer.json().boxed(),
149        "pretty" => layer.pretty().boxed(),
150        "plain" => layer.boxed(),
151        format => {
152            panic!("Invalid RUST_LOG_FORMAT: `{format}`.  Valid values are `json` or `pretty`.")
153        }
154    }
155}
156
157pub(crate) fn fmt_span_from_str(events: &str) -> FmtSpan {
158    let mut fmt_span = FmtSpan::NONE;
159    for event in events.split(',') {
160        fmt_span |= match event {
161            "new" => FmtSpan::NEW,
162            "enter" => FmtSpan::ENTER,
163            "exit" => FmtSpan::EXIT,
164            "close" => FmtSpan::CLOSE,
165            "active" => FmtSpan::ACTIVE,
166            "full" => FmtSpan::FULL,
167            _ => FmtSpan::NONE,
168        };
169    }
170    fmt_span
171}