ant_logging/
layers.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
9use crate::{
10    LogFormat, LogOutputDest, appender,
11    error::{Error, Result},
12};
13use std::collections::BTreeMap;
14use tracing_appender::non_blocking::WorkerGuard;
15use tracing_core::{Event, Level, Subscriber};
16use tracing_subscriber::{
17    Layer, Registry,
18    filter::Targets,
19    fmt::{
20        self as tracing_fmt, FmtContext, FormatEvent, FormatFields,
21        format::Writer,
22        time::{FormatTime, SystemTime},
23    },
24    layer::Filter,
25    registry::LookupSpan,
26    reload::{self, Handle},
27};
28
29const MAX_LOG_SIZE: usize = 20 * 1024 * 1024;
30const MAX_UNCOMPRESSED_LOG_FILES: usize = 10;
31const MAX_LOG_FILES: usize = 1000;
32// Everything is logged by default
33const ALL_ANT_LOGS: &str = "all";
34// Trace at nodes, clients, debug at networking layer
35const VERBOSE_ANT_LOGS: &str = "v";
36
37/// Handle that implements functions to change the log level on the fly.
38pub struct ReloadHandle(pub(crate) Handle<Box<dyn Filter<Registry> + Send + Sync>, Registry>);
39
40impl ReloadHandle {
41    /// Modify the log level to the provided CSV value
42    /// Example input: `libp2p=DEBUG,tokio=INFO,all,sn_client=ERROR`
43    ///
44    /// Custom keywords will take less precedence if the same target has been manually specified in the CSV.
45    /// `sn_client=ERROR` in the above example will be used instead of the TRACE level set by "all" keyword.
46    pub fn modify_log_level(&self, logging_value: &str) -> Result<()> {
47        let targets: Vec<(String, Level)> = get_logging_targets(logging_value)?;
48        self.0.modify(|old_filter| {
49            let new_filter: Box<dyn Filter<Registry> + Send + Sync> =
50                Box::new(Targets::new().with_targets(targets));
51            *old_filter = new_filter;
52        })?;
53
54        Ok(())
55    }
56}
57
58#[derive(Default)]
59/// Tracing log formatter setup for easier span viewing
60pub(crate) struct LogFormatter;
61
62impl<S, N> FormatEvent<S, N> for LogFormatter
63where
64    S: Subscriber + for<'a> LookupSpan<'a>,
65    N: for<'a> FormatFields<'a> + 'static,
66{
67    fn format_event(
68        &self,
69        ctx: &FmtContext<'_, S, N>,
70        mut writer: Writer,
71        event: &Event<'_>,
72    ) -> std::fmt::Result {
73        // Write level and target
74        let level = *event.metadata().level();
75        let module = event.metadata().module_path().unwrap_or("<unknown module>");
76        let lno = event.metadata().line().unwrap_or(0);
77        let time = SystemTime;
78
79        write!(writer, "[")?;
80        time.format_time(&mut writer)?;
81        write!(writer, " {level} {module} {lno}")?;
82        ctx.visit_spans(|span| write!(writer, "/{}", span.name()))?;
83        write!(writer, "] ")?;
84
85        // Add the log message and any fields associated with the event
86        ctx.field_format().format_fields(writer.by_ref(), event)?;
87
88        writeln!(writer)
89    }
90}
91
92/// The different Subscribers composed into a list of layers
93#[derive(Default)]
94pub(crate) struct TracingLayers {
95    pub(crate) layers: Vec<Box<dyn Layer<Registry> + Send + Sync>>,
96    pub(crate) log_appender_guard: Option<WorkerGuard>,
97}
98
99impl TracingLayers {
100    pub(crate) fn fmt_layer(
101        &mut self,
102        default_logging_targets: Vec<(String, Level)>,
103        output_dest: &LogOutputDest,
104        format: LogFormat,
105        max_uncompressed_log_files: Option<usize>,
106        max_compressed_log_files: Option<usize>,
107        print_updates_to_stdout: bool,
108    ) -> Result<ReloadHandle> {
109        let layer = match output_dest {
110            LogOutputDest::Stdout => {
111                if print_updates_to_stdout {
112                    println!("Logging to stdout");
113                }
114                tracing_fmt::layer()
115                    .with_ansi(false)
116                    .with_target(false)
117                    .event_format(LogFormatter)
118                    .boxed()
119            }
120            LogOutputDest::Stderr => tracing_fmt::layer()
121                .with_ansi(false)
122                .with_target(false)
123                .event_format(LogFormatter)
124                .with_writer(std::io::stderr)
125                .boxed(),
126            LogOutputDest::Path(path) => {
127                // Check if path ends with .log extension
128                if path.extension() == Some(std::ffi::OsStr::new("log")) {
129                    // Direct file logging without rotation
130                    if let Some(parent) = path.parent() {
131                        std::fs::create_dir_all(parent)?;
132                    }
133                    if print_updates_to_stdout {
134                        println!("Logging to file: {path:?}");
135                    }
136
137                    let file = std::fs::OpenOptions::new()
138                        .create(true)
139                        .append(true)
140                        .open(path)?;
141
142                    match format {
143                        LogFormat::Json => tracing_fmt::layer()
144                            .json()
145                            .flatten_event(true)
146                            .with_writer(file)
147                            .boxed(),
148                        LogFormat::Default => tracing_fmt::layer()
149                            .with_ansi(false)
150                            .with_writer(file)
151                            .event_format(LogFormatter)
152                            .boxed(),
153                    }
154                } else {
155                    // Directory logging with rotation
156                    std::fs::create_dir_all(path)?;
157                    if print_updates_to_stdout {
158                        println!("Logging to directory: {path:?}");
159                    }
160
161                    // the number of normal files
162                    let max_uncompressed_log_files =
163                        max_uncompressed_log_files.unwrap_or(MAX_UNCOMPRESSED_LOG_FILES);
164                    // the total number of files; should be greater than uncompressed
165                    let max_log_files =
166                        if let Some(max_compressed_log_files) = max_compressed_log_files {
167                            max_compressed_log_files + max_uncompressed_log_files
168                        } else {
169                            std::cmp::max(max_uncompressed_log_files, MAX_LOG_FILES)
170                        };
171                    let (file_rotation, worker_guard) = appender::file_rotater(
172                        path,
173                        MAX_LOG_SIZE,
174                        max_uncompressed_log_files,
175                        max_log_files,
176                    );
177                    self.log_appender_guard = Some(worker_guard);
178
179                    match format {
180                        LogFormat::Json => tracing_fmt::layer()
181                            .json()
182                            .flatten_event(true)
183                            .with_writer(file_rotation)
184                            .boxed(),
185                        LogFormat::Default => tracing_fmt::layer()
186                            .with_ansi(false)
187                            .with_writer(file_rotation)
188                            .event_format(LogFormatter)
189                            .boxed(),
190                    }
191                }
192            }
193        };
194        let targets = match std::env::var("ANT_LOG") {
195            Ok(sn_log_val) => {
196                if print_updates_to_stdout {
197                    println!("Using ANT_LOG={sn_log_val}");
198                }
199                get_logging_targets(&sn_log_val)?
200            }
201            Err(_) => default_logging_targets,
202        };
203
204        let target_filters: Box<dyn Filter<Registry> + Send + Sync> =
205            Box::new(Targets::new().with_targets(targets));
206
207        let (filter, reload_handle) = reload::Layer::new(target_filters);
208
209        let layer = layer.with_filter(filter);
210        self.layers.push(Box::new(layer));
211
212        Ok(ReloadHandle(reload_handle))
213    }
214
215    #[cfg(feature = "otlp")]
216    pub(crate) fn otlp_layer(
217        &mut self,
218        default_logging_targets: Vec<(String, Level)>,
219    ) -> Result<()> {
220        use opentelemetry::{
221            KeyValue,
222            sdk::{Resource, trace},
223        };
224        use opentelemetry_otlp::WithExportConfig;
225        use opentelemetry_semantic_conventions::resource::{SERVICE_INSTANCE_ID, SERVICE_NAME};
226        use rand::{Rng, distributions::Alphanumeric, thread_rng};
227
228        let service_name = std::env::var("OTLP_SERVICE_NAME").unwrap_or_else(|_| {
229            let random_node_name: String = thread_rng()
230                .sample_iter(&Alphanumeric)
231                .take(10)
232                .map(char::from)
233                .collect();
234            random_node_name
235        });
236        println!("The opentelemetry traces are logged under the name: {service_name}");
237
238        let tracer = opentelemetry_otlp::new_pipeline()
239            .tracing()
240            .with_exporter(opentelemetry_otlp::new_exporter().tonic().with_env())
241            .with_trace_config(trace::config().with_resource(Resource::new(vec![
242                KeyValue::new(SERVICE_NAME, service_name),
243                KeyValue::new(SERVICE_INSTANCE_ID, std::process::id().to_string()),
244            ])))
245            .install_batch(opentelemetry::runtime::Tokio)?;
246
247        let targets = match std::env::var("ANT_LOG_OTLP") {
248            Ok(sn_log_val) => {
249                println!("Using ANT_LOG_OTLP={sn_log_val}");
250                get_logging_targets(&sn_log_val)?
251            }
252            Err(_) => default_logging_targets,
253        };
254
255        let target_filters: Box<dyn Filter<Registry> + Send + Sync> =
256            Box::new(Targets::new().with_targets(targets));
257        let otlp_layer = tracing_opentelemetry::layer()
258            .with_tracer(tracer)
259            .with_filter(target_filters)
260            .boxed();
261        self.layers.push(otlp_layer);
262        Ok(())
263    }
264}
265
266/// Parses the logging targets from the env variable (ANT_LOG). The crates should be given as a CSV, for e.g.,
267/// `export ANT_LOG = libp2p=DEBUG, tokio=INFO, all, sn_client=ERROR`
268/// Custom keywords will take less precedence if the same target has been manually specified in the CSV.
269/// `sn_client=ERROR` in the above example will be used instead of the TRACE level set by "all" keyword.
270fn get_logging_targets(logging_env_value: &str) -> Result<Vec<(String, Level)>> {
271    let mut targets = BTreeMap::new();
272    let mut contains_keyword_all_sn_logs = false;
273    let mut contains_keyword_verbose_sn_logs = false;
274
275    for crate_log_level in logging_env_value.split(',') {
276        // TODO: are there other default short-circuits wanted?
277        // Could we have a default set if NOT on a release commit?
278        if crate_log_level == ALL_ANT_LOGS {
279            contains_keyword_all_sn_logs = true;
280            continue;
281        } else if crate_log_level == VERBOSE_ANT_LOGS {
282            contains_keyword_verbose_sn_logs = true;
283            continue;
284        }
285
286        let mut split = crate_log_level.split('=');
287        let crate_name = split.next().ok_or_else(|| {
288            Error::LoggingConfiguration("Could not obtain crate name in logging string".to_string())
289        })?;
290        let log_level = split.next().unwrap_or("trace");
291        targets.insert(crate_name.to_string(), get_log_level_from_str(log_level)?);
292    }
293
294    let mut to_be_overriden_targets =
295        if contains_keyword_all_sn_logs || contains_keyword_verbose_sn_logs {
296            let mut t = BTreeMap::from_iter(vec![
297                // bins
298                ("ant".to_string(), Level::TRACE),
299                ("evm_testnet".to_string(), Level::TRACE),
300                ("antnode".to_string(), Level::TRACE),
301                ("antctl".to_string(), Level::TRACE),
302                ("node_launchpad".to_string(), Level::DEBUG),
303                // libs
304                ("ant_bootstrap".to_string(), Level::TRACE),
305                ("ant_build_info".to_string(), Level::TRACE),
306                ("ant_evm".to_string(), Level::TRACE),
307                ("ant_logging".to_string(), Level::TRACE),
308                ("ant_node_manager".to_string(), Level::TRACE),
309                ("ant_node_rpc_client".to_string(), Level::TRACE),
310                ("ant_protocol".to_string(), Level::TRACE),
311                ("ant_service_management".to_string(), Level::TRACE),
312                ("service-manager".to_string(), Level::DEBUG),
313                ("autonomi".to_string(), Level::TRACE),
314                ("evmlib".to_string(), Level::TRACE),
315            ]);
316
317            if !t.contains_key("ant_node") {
318                if contains_keyword_all_sn_logs {
319                    t.insert("ant_node".to_string(), Level::TRACE)
320                } else if contains_keyword_verbose_sn_logs {
321                    t.insert("ant_node".to_string(), Level::DEBUG)
322                } else {
323                    t.insert("ant_node".to_string(), Level::INFO)
324                };
325            }
326            t
327        } else {
328            Default::default()
329        };
330    to_be_overriden_targets.extend(targets);
331    Ok(to_be_overriden_targets.into_iter().collect())
332}
333
334fn get_log_level_from_str(log_level: &str) -> Result<Level> {
335    match log_level.to_lowercase().as_str() {
336        "info" => Ok(Level::INFO),
337        "debug" => Ok(Level::DEBUG),
338        "trace" => Ok(Level::TRACE),
339        "warn" => Ok(Level::WARN),
340        "error" => Ok(Level::WARN),
341        _ => Err(Error::LoggingConfiguration(format!(
342            "Log level {log_level} is not supported"
343        ))),
344    }
345}