fluence_sdk_main/
logger.rs

1/*
2 * Copyright 2018 Fluence Labs Limited
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 */
16
17//! This module allows log messages from the Wasm side. It is implemented as a logging facade for
18//! crate [`log`].
19//!
20//! # Examples
21//!
22//! This example initializes [`WasmLogger`] with setting log level.
23//! Macros from crate [`log`] are used as a logging facade.
24//!
25//! ```ignore
26//!     use fluence::logger;
27//!     use log::{error, trace};
28//!     use simple_logger;
29//!
30//!     fn main() {
31//!         logger::WasmLoggerBuilder::new()
32//!             .with_log_leve(log::Level::Info)
33//!             .build()
34//!             .unwrap();
35//!
36//!         error!("This message will be logged.");
37//!         trace!("This message will not be logged.");
38//!     }
39//!
40//! ```
41//!
42//! [`WasmLogger`]: struct.WasmLogger.html
43//! [`log`]: https://docs.rs/log
44
45use log::LevelFilter;
46use std::collections::HashMap;
47
48/// By default, logger will be initialized with log level from this environment variable.
49pub const WASM_LOG_ENV_NAME: &str = "WASM_LOG";
50
51/// If WASM_LOG_ENV isn't set, then this level will be used as the default.
52const WASM_DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Info;
53
54/// Mapping from logging namespace string to its bitmask.
55/// TODO: use i64 for bitmask when wasmpack/bindgen issue with i64 is fixed.
56///       Currently, i64 doesn't work on some versions of V8 because log_utf8_string function
57///       isn't marked as #[wasm_bindgen]. In result, TS/JS code throws 'TypeError' on every log.
58pub type TargetMap = HashMap<&'static str, i32>;
59
60/// This structure is used to save information about particular log level for a particular module.
61#[derive(Debug)]
62struct LogDirective {
63    module_name: String,
64    level: LevelFilter,
65}
66
67impl LogDirective {
68    pub fn new(module_name: String, level: LevelFilter) -> Self {
69        Self { module_name, level }
70    }
71}
72
73/// The Wasm Logger.
74///
75/// This struct implements the [`Log`] trait from the [`log`] crate, which allows it to act as a
76/// logger.
77///
78/// Builder pattern is used here for logger initialization. Please be aware that build must be called
79/// to set the logger up.
80///
81/// [log-crate-url]: https://docs.rs/log/
82/// [`Log`]: https://docs.rs/log/0.4.11/log/trait.Log.html
83struct WasmLogger {
84    target_map: TargetMap,
85    modules_directives: Vec<LogDirective>,
86    default_log_level: LevelFilter,
87}
88
89/// The Wasm logger builder.
90///
91/// Build logger for the Fluence network, allows specifying target map and log level while building.
92pub struct WasmLoggerBuilder {
93    wasm_logger: WasmLogger,
94}
95
96impl WasmLoggerBuilder {
97    /// Initializes a builder of the global logger. Set log level based on the WASM_LOG environment variable if it set,
98    /// or [[WASM_DEFAULT_LOG_LEVEL]] otherwise. It is an initial method in this builder chain, please note,
99    /// that logger wouldn't work without subsequent build() call.
100    pub fn new() -> Self {
101        use std::str::FromStr;
102
103        let default_log_level = std::env::var(WASM_LOG_ENV_NAME)
104            .map_or(WASM_DEFAULT_LOG_LEVEL, |log_level_str| {
105                LevelFilter::from_str(&log_level_str).unwrap_or(WASM_DEFAULT_LOG_LEVEL)
106            });
107
108        let wasm_logger = WasmLogger {
109            target_map: HashMap::new(),
110            modules_directives: Vec::new(),
111            default_log_level,
112        };
113
114        Self { wasm_logger }
115    }
116
117    /// Set the log level.
118    pub fn with_log_level(mut self, level: LevelFilter) -> Self {
119        self.wasm_logger.default_log_level = level;
120        self
121    }
122
123    /// Set mapping between logging targets and numbers.
124    /// Used to efficiently enable & disable logs per target on the host.
125    pub fn with_target_map(mut self, map: TargetMap) -> Self {
126        self.wasm_logger.target_map = map;
127        self
128    }
129
130    pub fn filter(mut self, module_name: impl Into<String>, level: LevelFilter) -> Self {
131        let module_name = module_name.into();
132        let log_directive = LogDirective::new(module_name, level);
133
134        self.wasm_logger.modules_directives.push(log_directive);
135        self
136    }
137
138    /// Build the real logger.
139    ///
140    /// This method is a last one in this builder chain and MUST be called to set logger up.
141    /// Returns a error
142    ///
143    /// ```ignore
144    /// # use fluence::logger;
145    /// # use log::info;
146    /// #
147    /// # fn main() {
148    ///     logger::WasmLoggerBuilder::new()
149    ///         .with_log_level(log::LevelFilter::Trace)
150    ///         .with_target_map(<_>::default())
151    ///         .build()
152    ///         .unwrap();
153    /// # }
154    /// ```
155    pub fn build(mut self) -> Result<(), log::SetLoggerError> {
156        let max_level = self.max_log_level();
157        self.sort_directives();
158
159        let Self { wasm_logger } = self;
160
161        log::set_boxed_logger(Box::new(wasm_logger))?;
162        log::set_max_level(max_level);
163        Ok(())
164    }
165
166    /// Sort supplied directive ny length of module names to make more efficient lookup at runtime.
167    fn sort_directives(&mut self) {
168        self.wasm_logger.modules_directives.sort_by(|l, r| {
169            let llen = l.module_name.len();
170            let rlen = r.module_name.len();
171
172            rlen.cmp(&llen)
173        });
174    }
175
176    fn max_log_level(&self) -> log::LevelFilter {
177        let default_level = self.wasm_logger.default_log_level;
178        let max_filter_level = self
179            .wasm_logger
180            .modules_directives
181            .iter()
182            .map(|d| d.level)
183            .max()
184            .unwrap_or(LevelFilter::Off);
185
186        std::cmp::max(default_level, max_filter_level)
187    }
188}
189
190impl log::Log for WasmLogger {
191    #[inline]
192    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
193        let target = metadata.target();
194
195        for directive in self.modules_directives.iter() {
196            if target.starts_with(&directive.module_name) {
197                return metadata.level() <= directive.level;
198            }
199        }
200
201        metadata.level() <= self.default_log_level
202    }
203
204    #[inline]
205    fn log(&self, record: &log::Record<'_>) {
206        if !self.enabled(record.metadata()) {
207            return;
208        }
209
210        let level = record.metadata().level() as i32;
211        let default_target = 0;
212        let target = *self
213            .target_map
214            .get(record.metadata().target())
215            .unwrap_or(&default_target);
216        let msg = record.args().to_string();
217
218        log_utf8_string(level, target, msg.as_ptr() as _, msg.len() as _);
219    }
220
221    // in our case flushing is performed by a host itself
222    #[inline]
223    fn flush(&self) {}
224}
225
226#[cfg(target_arch = "wasm32")]
227pub fn log_utf8_string(level: i32, target: i32, msg_ptr: i32, msg_size: i32) {
228    unsafe { log_utf8_string_impl(level, target, msg_ptr, msg_size) };
229}
230
231#[cfg(not(target_arch = "wasm32"))]
232pub fn log_utf8_string(level: i32, target: i32, msg_ptr: i32, msg_size: i32) {
233    use std::str::from_utf8_unchecked;
234    use core::slice::from_raw_parts;
235
236    let level = level_from_i32(level);
237    let msg = unsafe { from_utf8_unchecked(from_raw_parts(msg_ptr as _, msg_size as _)) };
238    println!("[{}] {} {}", level, target, msg);
239}
240
241/// TODO: mark `log_utf8_string_impl` as #[wasm_bindgen], so it is polyfilled by bindgen
242/// log_utf8_string should be provided directly by a host.
243#[cfg(target_arch = "wasm32")]
244#[link(wasm_import_module = "host")]
245extern "C" {
246    // Writes a byte string of size bytes that starts from ptr to a logger
247    #[link_name = "log_utf8_string"]
248    fn log_utf8_string_impl(level: i32, target: i32, msg_ptr: i32, msg_size: i32);
249}
250
251#[allow(dead_code)]
252fn level_from_i32(level: i32) -> log::Level {
253    match level {
254        1 => log::Level::Error,
255        2 => log::Level::Warn,
256        3 => log::Level::Info,
257        4 => log::Level::Debug,
258        5 => log::Level::Trace,
259        _ => log::Level::max(),
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::WasmLogger;
266    use super::LogDirective;
267    use super::WasmLoggerBuilder;
268    use log::LevelFilter;
269    use log::Log;
270
271    use std::collections::HashMap;
272
273    fn create_metadata(module_name: &str, level: log::Level) -> log::Metadata<'_> {
274        log::MetadataBuilder::new()
275            .level(level)
276            .target(module_name)
277            .build()
278    }
279
280    #[test]
281    fn enabled_by_module_name() {
282        let module_1_name = "module_1";
283        let module_2_name = "module_2";
284
285        let modules_directives = vec![
286            LogDirective::new(module_1_name.to_string(), LevelFilter::Info),
287            LogDirective::new(module_2_name.to_string(), LevelFilter::Warn),
288        ];
289
290        let logger = WasmLogger {
291            target_map: HashMap::new(),
292            modules_directives,
293            default_log_level: LevelFilter::Error,
294        };
295
296        let allowed_metadata = create_metadata(module_1_name, log::Level::Info);
297        assert!(logger.enabled(&allowed_metadata));
298
299        let allowed_metadata = create_metadata(module_1_name, log::Level::Warn);
300        assert!(logger.enabled(&allowed_metadata));
301
302        let allowed_metadata = create_metadata(module_2_name, log::Level::Warn);
303        assert!(logger.enabled(&allowed_metadata));
304
305        let not_allowed_metadata = create_metadata(module_1_name, log::Level::Debug);
306        assert!(!logger.enabled(&not_allowed_metadata));
307
308        let not_allowed_metadata = create_metadata(module_2_name, log::Level::Info);
309        assert!(!logger.enabled(&not_allowed_metadata));
310    }
311
312    #[test]
313    fn default_log_level() {
314        let modules_directives = vec![LogDirective::new("module_1".to_string(), LevelFilter::Info)];
315
316        let logger = WasmLogger {
317            target_map: HashMap::new(),
318            modules_directives,
319            default_log_level: LevelFilter::Warn,
320        };
321
322        let module_name = "some_module";
323        let allowed_metadata = create_metadata(module_name, log::Level::Warn);
324        assert!(logger.enabled(&allowed_metadata));
325
326        let not_allowed_metadata = create_metadata(module_name, log::Level::Info);
327        assert!(!logger.enabled(&not_allowed_metadata));
328    }
329
330    #[test]
331    fn longest_directive_first() {
332        let module_1_name = "module_1";
333        let module_2_name = "module_1::some_name::func_name";
334
335        WasmLoggerBuilder::new()
336            .filter(module_1_name, LevelFilter::Info)
337            .filter(module_2_name, LevelFilter::Warn)
338            .build()
339            .unwrap();
340
341        let logger = log::logger();
342
343        let allowed_metadata = create_metadata(module_1_name, log::Level::Info);
344        assert!(logger.enabled(&allowed_metadata));
345
346        let not_allowed_metadata = create_metadata(module_2_name, log::Level::Info);
347        assert!(!logger.enabled(&not_allowed_metadata));
348    }
349}