Skip to main content

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