ant_logging/
lib.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9mod appender;
10mod error;
11mod layers;
12#[cfg(feature = "process-metrics")]
13pub mod metrics;
14
15use crate::error::Result;
16use layers::TracingLayers;
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19use tracing::info;
20use tracing_core::dispatcher::DefaultGuard;
21use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt};
22
23pub use error::Error;
24pub use layers::ReloadHandle;
25pub use tracing_appender::non_blocking::WorkerGuard;
26
27// re-exporting the tracing crate's Level as it is used in our public API
28pub use tracing_core::Level;
29
30#[derive(Debug, Clone)]
31pub enum LogOutputDest {
32    Stderr,
33    Stdout,
34    Path(PathBuf),
35}
36
37impl LogOutputDest {
38    pub fn parse_from_str(val: &str) -> Result<Self> {
39        match val {
40            "stdout" => Ok(LogOutputDest::Stdout),
41            "data-dir" => {
42                // Get the current timestamp and format it to be human readable
43                let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
44
45                // Get the data directory path and append the timestamp to the log file name
46                let dir = match dirs_next::data_dir() {
47                    Some(dir) => dir
48                        .join("autonomi")
49                        .join("client")
50                        .join("logs")
51                        .join(format!("log_{timestamp}")),
52                    None => {
53                        return Err(Error::LoggingConfiguration(
54                            "could not obtain data directory path".to_string(),
55                        ))
56                    }
57                };
58                Ok(LogOutputDest::Path(dir))
59            }
60            // The path should be a directory, but we can't use something like `is_dir` to check
61            // because the path doesn't need to exist. We can create it for the user.
62            value => Ok(LogOutputDest::Path(PathBuf::from(value))),
63        }
64    }
65}
66
67impl std::fmt::Display for LogOutputDest {
68    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69        match self {
70            LogOutputDest::Stderr => write!(f, "stderr"),
71            LogOutputDest::Stdout => write!(f, "stdout"),
72            LogOutputDest::Path(p) => write!(f, "{}", p.to_string_lossy()),
73        }
74    }
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
78pub enum LogFormat {
79    Default,
80    Json,
81}
82
83impl LogFormat {
84    pub fn parse_from_str(val: &str) -> Result<Self> {
85        match val {
86            "default" => Ok(LogFormat::Default),
87            "json" => Ok(LogFormat::Json),
88            _ => Err(Error::LoggingConfiguration(
89                "The only valid values for this argument are \"default\" or \"json\"".to_string(),
90            )),
91        }
92    }
93
94    pub fn as_str(&self) -> &'static str {
95        match self {
96            LogFormat::Default => "default",
97            LogFormat::Json => "json",
98        }
99    }
100}
101
102pub struct LogBuilder {
103    default_logging_targets: Vec<(String, Level)>,
104    output_dest: LogOutputDest,
105    format: LogFormat,
106    max_log_files: Option<usize>,
107    max_archived_log_files: Option<usize>,
108    /// Setting this would print the ant_logging related updates to stdout.
109    print_updates_to_stdout: bool,
110}
111
112impl LogBuilder {
113    /// Create a new builder
114    /// Provide the default_logging_targets that are used if the `ANT_LOG` env variable is not set.
115    ///
116    /// By default, we use log to the StdOut with the default format.
117    pub fn new(default_logging_targets: Vec<(String, Level)>) -> Self {
118        Self {
119            default_logging_targets,
120            output_dest: LogOutputDest::Stderr,
121            format: LogFormat::Default,
122            max_log_files: None,
123            max_archived_log_files: None,
124            print_updates_to_stdout: true,
125        }
126    }
127
128    /// Set the logging output destination
129    pub fn output_dest(&mut self, output_dest: LogOutputDest) {
130        self.output_dest = output_dest;
131    }
132
133    /// Set the logging format
134    pub fn format(&mut self, format: LogFormat) {
135        self.format = format
136    }
137
138    /// The max number of uncompressed log files to store
139    pub fn max_log_files(&mut self, files: usize) {
140        self.max_log_files = Some(files);
141    }
142
143    /// The max number of compressed files to store
144    pub fn max_archived_log_files(&mut self, files: usize) {
145        self.max_archived_log_files = Some(files);
146    }
147
148    /// Setting this to false would prevent ant_logging from printing things to stdout.
149    pub fn print_updates_to_stdout(&mut self, print: bool) {
150        self.print_updates_to_stdout = print;
151    }
152
153    /// Inits node logging, returning the NonBlocking guard if present.
154    /// This guard should be held for the life of the program.
155    ///
156    /// Logging should be instantiated only once.
157    pub fn initialize(self) -> Result<(ReloadHandle, Option<WorkerGuard>)> {
158        let mut layers = TracingLayers::default();
159
160        let reload_handle = layers.fmt_layer(
161            self.default_logging_targets.clone(),
162            &self.output_dest,
163            self.format,
164            self.max_log_files,
165            self.max_archived_log_files,
166            self.print_updates_to_stdout,
167        )?;
168
169        #[cfg(feature = "otlp")]
170        {
171            match std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
172                Ok(_) => layers.otlp_layer(self.default_logging_targets)?,
173                Err(_) => println!(
174                "The OTLP feature is enabled but the OTEL_EXPORTER_OTLP_ENDPOINT variable is not \
175                set, so traces will not be submitted."
176            ),
177            }
178        }
179
180        if tracing_subscriber::registry()
181            .with(layers.layers)
182            .try_init()
183            .is_err()
184        {
185            eprintln!("Tried to initialize and set global default subscriber more than once");
186        }
187
188        Ok((reload_handle, layers.log_appender_guard))
189    }
190
191    /// Logs to the data_dir. Should be called from a single threaded tokio/non-tokio context.
192    /// Provide the test file name to capture tracings from the test.
193    ///
194    /// subscriber.set_default() should be used if under a single threaded tokio / single threaded non-tokio context.
195    /// Refer here for more details: <https://github.com/tokio-rs/tracing/discussions/1626>
196    pub fn init_single_threaded_tokio_test(
197        test_file_name: &str,
198        disable_networking_logs: bool,
199    ) -> (Option<WorkerGuard>, DefaultGuard) {
200        let layers = Self::get_test_layers(test_file_name, disable_networking_logs);
201        let log_guard = tracing_subscriber::registry()
202            .with(layers.layers)
203            .set_default();
204        // this is the test_name and not the test_file_name
205        if let Some(test_name) = std::thread::current().name() {
206            info!("Running test: {test_name}");
207        }
208        (layers.log_appender_guard, log_guard)
209    }
210
211    /// Logs to the data_dir. Should be called from a multi threaded tokio context.
212    /// Provide the test file name to capture tracings from the test.
213    ///
214    /// subscriber.init() should be used under multi threaded tokio context. If you have 1+ multithreaded tokio tests under
215    /// the same integration test, this might result in loss of logs. Hence use .init() (instead of .try_init()) to panic
216    /// if called more than once.
217    pub fn init_multi_threaded_tokio_test(
218        test_file_name: &str,
219        disable_networking_logs: bool,
220    ) -> Option<WorkerGuard> {
221        let layers = Self::get_test_layers(test_file_name, disable_networking_logs);
222        tracing_subscriber::registry()
223        .with(layers.layers)
224        .try_init()
225        .expect("You have tried to init multi_threaded tokio logging twice\nRefer ant_logging::get_test_layers docs for more.");
226
227        layers.log_appender_guard
228    }
229
230    /// Initialize just the fmt_layer for testing purposes.
231    ///
232    /// Also overwrites the ANT_LOG variable to log everything including the test_file_name
233    fn get_test_layers(test_file_name: &str, disable_networking_logs: bool) -> TracingLayers {
234        // overwrite ANT_LOG
235        if disable_networking_logs {
236            std::env::set_var(
237                "ANT_LOG",
238                format!("{test_file_name}=TRACE,all,ant_networking=WARN,all"),
239            );
240        } else {
241            std::env::set_var("ANT_LOG", format!("{test_file_name}=TRACE,all"));
242        }
243
244        let output_dest = match dirs_next::data_dir() {
245            Some(dir) => {
246                // Get the current timestamp and format it to be human readable
247                let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
248                let path = dir
249                    .join("autonomi")
250                    .join("client")
251                    .join("logs")
252                    .join(format!("log_{timestamp}"));
253                LogOutputDest::Path(path)
254            }
255            None => LogOutputDest::Stdout,
256        };
257
258        println!("Logging test at {test_file_name:?} to {output_dest:?}");
259
260        let mut layers = TracingLayers::default();
261
262        let _reload_handle = layers
263            .fmt_layer(vec![], &output_dest, LogFormat::Default, None, None, false)
264            .expect("Failed to get TracingLayers");
265        layers
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use crate::{layers::LogFormatter, ReloadHandle};
272    use color_eyre::Result;
273    use tracing::{trace, warn, Level};
274    use tracing_subscriber::{
275        filter::Targets,
276        fmt as tracing_fmt,
277        layer::{Filter, SubscriberExt},
278        reload,
279        util::SubscriberInitExt,
280        Layer, Registry,
281    };
282    use tracing_test::internal::global_buf;
283
284    #[test]
285    // todo: break down the TracingLayers so that we can plug in the writer without having to rewrite the whole function
286    // here.
287    fn reload_handle_should_change_log_levels() -> Result<()> {
288        // A mock write that writes to stdout + collects events to a global buffer. We can later read from this buffer.
289        let mock_writer = tracing_test::internal::MockWriter::new(global_buf());
290
291        // Constructing the fmt layer manually.
292        let layer = tracing_fmt::layer()
293            .with_ansi(false)
294            .with_target(false)
295            .event_format(LogFormatter)
296            .with_writer(mock_writer)
297            .boxed();
298
299        let test_target = "ant_logging::tests".to_string();
300        // to enable logs just for the test.
301        let target_filters: Box<dyn Filter<Registry> + Send + Sync> =
302            Box::new(Targets::new().with_targets(vec![(test_target.clone(), Level::TRACE)]));
303
304        // add the reload layer
305        let (filter, handle) = reload::Layer::new(target_filters);
306        let reload_handle = ReloadHandle(handle);
307        let layer = layer.with_filter(filter);
308        tracing_subscriber::registry().with(layer).try_init()?;
309
310        // Span is not controlled by the ReloadHandle. So we can set any span here.
311        let _span = tracing::info_span!("info span");
312
313        trace!("First trace event");
314
315        {
316            let buf = global_buf().lock().unwrap();
317
318            let events: Vec<&str> = std::str::from_utf8(&buf)
319                .expect("Logs contain invalid UTF8")
320                .lines()
321                .collect();
322            assert_eq!(events.len(), 1);
323            assert!(events[0].contains("First trace event"));
324        }
325
326        reload_handle.modify_log_level("ant_logging::tests=WARN")?;
327
328        // trace should not be logged now.
329        trace!("Second trace event");
330        warn!("First warn event");
331
332        {
333            let buf = global_buf().lock().unwrap();
334
335            let events: Vec<&str> = std::str::from_utf8(&buf)
336                .expect("Logs contain invalid UTF8")
337                .lines()
338                .collect();
339
340            assert_eq!(events.len(), 2);
341            assert!(events[0].contains("First trace event"));
342            assert!(events[1].contains("First warn event"));
343        }
344
345        Ok(())
346    }
347}