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