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