snarkos_cli/helpers/
logger.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::helpers::{DynamicFormatter, LogWriter};
17
18use anyhow::{Result, bail};
19
20use crossterm::tty::IsTty;
21use std::{
22    fs::File,
23    io,
24    path::Path,
25    str::FromStr,
26    sync::{Arc, atomic::AtomicBool},
27};
28use tokio::sync::mpsc;
29use tracing_subscriber::{
30    EnvFilter,
31    layer::{Layer, SubscriberExt},
32    util::SubscriberInitExt,
33};
34
35fn parse_log_verbosity(verbosity: u8) -> Result<EnvFilter> {
36    // First, set default log verbosity.
37    // Note, that this must not be prefixed with `RUST_LOG=`.
38    let default_log_str = match verbosity {
39        0 => "info",
40        1 => "debug",
41        2.. => "trace",
42    };
43    let filter = EnvFilter::from_str(default_log_str).unwrap();
44
45    // Now, set rules for specific crates.
46    let filter = if verbosity >= 2 {
47        filter.add_directive("snarkos_node_sync=trace".parse().unwrap())
48    } else {
49        filter.add_directive("snarkos_node_sync=debug".parse().unwrap())
50    };
51
52    let filter = if verbosity >= 3 {
53        filter
54            .add_directive("snarkos_node_bft=trace".parse().unwrap())
55            .add_directive("snarkos_node_bft::gateway=debug".parse().unwrap())
56    } else {
57        filter.add_directive("snarkos_node_bft=debug".parse().unwrap())
58    };
59
60    let filter = if verbosity >= 4 {
61        let filter = filter.add_directive("snarkos_node_bft::gateway=trace".parse().unwrap());
62
63        // At high log levels, also show warnings of third-party crates.
64        filter
65            .add_directive("mio=warn".parse().unwrap())
66            .add_directive("tokio_util=warn".parse().unwrap())
67            .add_directive("hyper=warn".parse().unwrap())
68            .add_directive("reqwest=warn".parse().unwrap())
69            .add_directive("want=warn".parse().unwrap())
70            .add_directive("h2=warn".parse().unwrap())
71            .add_directive("tower=warn".parse().unwrap())
72            .add_directive("axum=warn".parse().unwrap())
73            .add_directive("ureq=warn".parse().unwrap())
74    } else {
75        let filter = filter.add_directive("snarkos_node_bft::gateway=debug".parse().unwrap());
76
77        // Disable logs from third-party crates by default.
78        filter
79            .add_directive("mio=off".parse().unwrap())
80            .add_directive("tokio_util=off".parse().unwrap())
81            .add_directive("hyper=off".parse().unwrap())
82            .add_directive("reqwest=off".parse().unwrap())
83            .add_directive("want=off".parse().unwrap())
84            .add_directive("h2=off".parse().unwrap())
85            .add_directive("tower=off".parse().unwrap())
86            .add_directive("axum=off".parse().unwrap())
87            .add_directive("ureq=off".parse().unwrap())
88    };
89
90    let filter = if verbosity >= 5 {
91        filter.add_directive("snarkos_node_router=trace".parse().unwrap())
92    } else {
93        filter.add_directive("snarkos_node_router=debug".parse().unwrap())
94    };
95
96    let filter = if verbosity >= 6 {
97        filter.add_directive("snarkos_node_tcp=trace".parse().unwrap())
98    } else {
99        filter.add_directive("snarkos_node_tcp=off".parse().unwrap())
100    };
101
102    Ok(filter)
103}
104
105fn parse_log_filter(filter_str: &str) -> Result<EnvFilter> {
106    EnvFilter::from_str(filter_str).map_err(|err| err.into())
107}
108
109/// Sets the log filter based on the given verbosity level.
110///
111/// ```ignore
112/// 0 => info
113/// 1 => info, debug
114/// 2 => info, debug, trace, snarkos_node_sync=trace
115/// 3 => info, debug, trace, snarkos_node_bft=trace
116/// 4 => info, debug, trace, snarkos_node_bft::gateway=trace,
117///      [mio|tokio_util|hyper|reqwest|want|h2|tower|axum]=warn
118/// 5 => info, debug, trace, snarkos_node_router=trace
119/// 6 => info, debug, trace, snarkos_node_tcp=trace
120/// ```
121pub fn initialize_logger<P: AsRef<Path>>(
122    verbosity: u8,
123    log_filter: &Option<String>,
124    nodisplay: bool,
125    logfile: P,
126    shutdown: Arc<AtomicBool>,
127) -> Result<mpsc::Receiver<Vec<u8>>> {
128    let [stdout_filter, logfile_filter] = std::array::from_fn(|_| {
129        if let Some(filter) = log_filter { parse_log_filter(filter) } else { parse_log_verbosity(verbosity) }
130    });
131
132    // Create the directories tree for a logfile if it doesn't exist.
133    let Some(logfile_dir) = logfile.as_ref().parent() else { bail!("Root directory passed as a logfile") };
134
135    if !logfile_dir.exists() {
136        if let Err(err) = std::fs::create_dir_all(logfile_dir) {
137            bail!("Failed to create a directory: '{}' ({err})", logfile_dir.display());
138        }
139    }
140    // Create a file to write logs to.
141    let logfile = match File::options().append(true).create(true).open(logfile) {
142        Ok(logfile) => logfile,
143        Err(err) => bail!("Failed to open the file for writing logs: {err}"),
144    };
145
146    // Initialize the log channel.
147    let (log_sender, log_receiver) = mpsc::channel(1024);
148
149    // Initialize the log sender.
150    // This is only needed when the terminal UI (display) wants to process log output.
151    let log_sender = match nodisplay {
152        true => None,
153        false => Some(log_sender),
154    };
155
156    // At high verbosity or when there is a custom log filter we show the target
157    // of the log event, i.e., the file/module where the log message was created.
158    let show_target = verbosity > 2 || log_filter.is_some();
159
160    // Initialize tracing.
161    let _ = tracing_subscriber::registry()
162        .with(
163            // Add layer using LogWriter for stdout / terminal
164            tracing_subscriber::fmt::Layer::default()
165                .with_ansi(log_sender.is_none() && io::stdout().is_tty())
166                .with_writer(move || LogWriter::new(&log_sender))
167                .with_target(show_target)
168                .event_format(DynamicFormatter::new(shutdown))
169                .with_filter(stdout_filter?),
170        )
171        .with(
172            // Add layer redirecting logs to the file
173            tracing_subscriber::fmt::Layer::default()
174                .with_ansi(false)
175                .with_writer(logfile)
176                .with_target(show_target)
177                .with_filter(logfile_filter?),
178        )
179        .try_init();
180
181    Ok(log_receiver)
182}
183
184/// Set up only terminal logging
185pub fn initialize_terminal_logger(verbosity: u8) -> Result<()> {
186    let stdout_filter = parse_log_verbosity(verbosity)?;
187
188    // At high verbosity or when there is a custom log filter we show the target
189    // of the log event, i.e., the file/module where the log message was created.
190    let show_target = verbosity > 2;
191
192    // Initialize tracing.
193    let _ = tracing_subscriber::registry()
194        .with(
195            // Add layer using LogWriter for stdout / terminal
196            tracing_subscriber::fmt::Layer::default()
197                .with_ansi(io::stdout().is_tty())
198                .with_target(show_target)
199                .event_format(DynamicFormatter::new(Arc::new(AtomicBool::new(false))))
200                .with_filter(stdout_filter),
201        )
202        .try_init();
203
204    Ok(())
205}
206
207/// Returns the welcome message as a string.
208pub fn welcome_message() -> String {
209    use colored::Colorize;
210
211    let mut output = String::new();
212    output += &r#"
213
214         ╦╬╬╬╬╬╦
215        ╬╬╬╬╬╬╬╬╬                    ▄▄▄▄        ▄▄▄
216       ╬╬╬╬╬╬╬╬╬╬╬                  ▐▓▓▓▓▌       ▓▓▓
217      ╬╬╬╬╬╬╬╬╬╬╬╬╬                ▐▓▓▓▓▓▓▌      ▓▓▓     ▄▄▄▄▄▄       ▄▄▄▄▄▄
218     ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬              ▐▓▓▓  ▓▓▓▌     ▓▓▓   ▄▓▓▀▀▀▀▓▓▄   ▐▓▓▓▓▓▓▓▓▌
219    ╬╬╬╬╬╬╬╜ ╙╬╬╬╬╬╬╬            ▐▓▓▓▌  ▐▓▓▓▌    ▓▓▓  ▐▓▓▓▄▄▄▄▓▓▓▌ ▐▓▓▓    ▓▓▓▌
220   ╬╬╬╬╬╬╣     ╠╬╬╬╬╬╬           ▓▓▓▓▓▓▓▓▓▓▓▓    ▓▓▓  ▐▓▓▀▀▀▀▀▀▀▀▘ ▐▓▓▓    ▓▓▓▌
221  ╬╬╬╬╬╬╣       ╠╬╬╬╬╬╬         ▓▓▓▓▌    ▐▓▓▓▓   ▓▓▓   ▀▓▓▄▄▄▄▓▓▀   ▐▓▓▓▓▓▓▓▓▌
222 ╬╬╬╬╬╬╣         ╠╬╬╬╬╬╬       ▝▀▀▀▀      ▀▀▀▀▘  ▀▀▀     ▀▀▀▀▀▀       ▀▀▀▀▀▀
223╚╬╬╬╬╬╩           ╩╬╬╬╬╩
224
225
226"#
227    .white()
228    .bold();
229    output += &"👋 Welcome to Aleo! We thank you for running a node and supporting privacy.\n".bold();
230    output
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn log_filter() {
239        let result = parse_log_filter("=");
240        assert!(result.is_err(), "must disallow invalid log filter");
241
242        let result = parse_log_filter("snarkos=trace");
243        assert!(result.is_ok(), "must allow valid log filter");
244    }
245}