Skip to main content

fyrox_core/
log.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Simple logger. By default, it writes in the console only. To enable logging into a file, call
22//! [`Log::set_file_name`] somewhere in your `main` function.
23
24use crate::instant::Instant;
25use crate::parking_lot::Mutex;
26#[cfg(target_arch = "wasm32")]
27use crate::wasm_bindgen::{self, prelude::*};
28use crate::{reflect::prelude::*, visitor::prelude::*};
29use fxhash::FxHashMap;
30use std::collections::hash_map::Entry;
31use std::fmt::{Debug, Display};
32#[cfg(not(target_arch = "wasm32"))]
33use std::io::{self, Write};
34use std::path::Path;
35use std::sync::mpsc::Sender;
36use std::sync::LazyLock;
37use std::time::Duration;
38
39#[cfg(target_arch = "wasm32")]
40#[wasm_bindgen]
41extern "C" {
42    // Use `js_namespace` here to bind `console.log(..)` instead of just
43    // `log(..)`
44    #[wasm_bindgen(js_namespace = console)]
45    fn log(s: &str);
46}
47
48/// A message that could be sent by the logger to all listeners.
49pub struct LogMessage {
50    /// Kind of the message: information, warning or error.
51    pub kind: MessageKind,
52    /// The source message without logger prefixes.
53    pub content: String,
54    /// Time point at which the message was recorded. It is relative to the moment when the
55    /// logger was initialized.
56    pub time: Duration,
57}
58
59static LOG: LazyLock<Mutex<Log>> = LazyLock::new(|| {
60    Mutex::new(Log {
61        #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
62        file: None,
63        verbosity: MessageKind::Information,
64        listeners: Default::default(),
65        time_origin: Instant::now(),
66        one_shot_sources: Default::default(),
67        write_to_stdout: true,
68    })
69});
70
71/// A kind of message.
72#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq, Eq, Ord, Hash, Visit, Reflect)]
73#[repr(u32)]
74pub enum MessageKind {
75    /// Some useful information.
76    #[default]
77    Information = 0,
78    /// A warning.
79    Warning = 1,
80    /// An error of some kind.
81    Error = 2,
82}
83
84impl MessageKind {
85    fn as_str(self) -> &'static str {
86        match self {
87            MessageKind::Information => "[INFO]: ",
88            MessageKind::Warning => "[WARNING]: ",
89            MessageKind::Error => "[ERROR]: ",
90        }
91    }
92}
93
94/// See module docs.
95pub struct Log {
96    #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
97    file: Option<std::fs::File>,
98    verbosity: MessageKind,
99    listeners: Vec<Sender<LogMessage>>,
100    time_origin: Instant,
101    one_shot_sources: FxHashMap<usize, String>,
102    write_to_stdout: bool,
103}
104
105impl Log {
106    /// Creates a new log file at the specified path.
107    pub fn set_file_name<P: AsRef<Path>>(#[allow(unused_variables)] path: P) {
108        #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
109        {
110            let mut guard = LOG.lock();
111            guard.file = std::fs::File::create(path).ok();
112        }
113    }
114
115    /// Sets new file to write the log to.
116    pub fn set_file(#[allow(unused_variables)] file: Option<std::fs::File>) {
117        #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
118        {
119            let mut guard = LOG.lock();
120            guard.file = file;
121        }
122    }
123
124    fn write_internal<S>(&mut self, id: Option<usize>, kind: MessageKind, message: S) -> bool
125    where
126        S: AsRef<str>,
127    {
128        let mut msg = message.as_ref().to_owned();
129        if kind as u32 >= self.verbosity as u32 {
130            if let Some(id) = id {
131                let mut need_write = false;
132                match self.one_shot_sources.entry(id) {
133                    Entry::Occupied(mut message) => {
134                        if message.get() != &msg {
135                            message.insert(msg.clone());
136                            need_write = true;
137                        }
138                    }
139                    Entry::Vacant(entry) => {
140                        entry.insert(msg.clone());
141                        need_write = true;
142                    }
143                }
144
145                if !need_write {
146                    return false;
147                }
148            }
149
150            // Notify listeners about the message and remove all disconnected listeners.
151            self.listeners.retain(|listener| {
152                listener
153                    .send(LogMessage {
154                        kind,
155                        content: msg.clone(),
156                        time: Instant::now() - self.time_origin,
157                    })
158                    .is_ok()
159            });
160
161            msg.insert_str(0, kind.as_str());
162
163            #[cfg(target_arch = "wasm32")]
164            {
165                log(&msg);
166            }
167
168            #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
169            {
170                if self.write_to_stdout {
171                    let _ = io::stdout().write_all(msg.as_bytes());
172                }
173
174                if let Some(log_file) = self.file.as_mut() {
175                    let _ = log_file.write_all(msg.as_bytes());
176                    let _ = log_file.flush();
177                }
178            }
179
180            #[cfg(target_os = "android")]
181            {
182                if self.write_to_stdout {
183                    let _ = io::stdout().write_all(msg.as_bytes());
184                }
185            }
186        }
187
188        true
189    }
190
191    fn writeln_internal<S>(&mut self, id: Option<usize>, kind: MessageKind, message: S) -> bool
192    where
193        S: AsRef<str>,
194    {
195        let mut msg = message.as_ref().to_owned();
196        msg.push('\n');
197        self.write_internal(id, kind, msg)
198    }
199
200    /// Writes a string to the console and optionally into the file (if set).
201    pub fn write<S>(kind: MessageKind, msg: S)
202    where
203        S: AsRef<str>,
204    {
205        LOG.lock().write_internal(None, kind, msg);
206    }
207
208    /// Writes a string to the console and optionally into the file (if set). Unlike [`Self::write`]
209    /// this method writes the message only once per given id if the message remains the same. If
210    /// the message changes, then the new version will be printed to the log. This method is useful
211    /// if you need to print error messages, but prevent them from flooding the log.
212    pub fn write_once<S>(id: usize, kind: MessageKind, msg: S) -> bool
213    where
214        S: AsRef<str>,
215    {
216        LOG.lock().write_internal(Some(id), kind, msg)
217    }
218
219    /// Writes a string to the console and optionally into the file (if set), adds a new line to the
220    /// end of the message.
221    pub fn writeln<S>(kind: MessageKind, msg: S)
222    where
223        S: AsRef<str>,
224    {
225        LOG.lock().writeln_internal(None, kind, msg);
226    }
227
228    /// Writes a string to the console and optionally into the file (if set), adds a new line to the
229    /// end of the message. Prints the message only once. See [`Self::write_once`] for more info.
230    pub fn writeln_once<S>(id: usize, kind: MessageKind, msg: S) -> bool
231    where
232        S: AsRef<str>,
233    {
234        LOG.lock().writeln_internal(Some(id), kind, msg)
235    }
236
237    /// Writes an information message.
238    pub fn info<S>(msg: S)
239    where
240        S: AsRef<str>,
241    {
242        Self::writeln(MessageKind::Information, msg)
243    }
244
245    /// Writes a warning message.
246    pub fn warn<S>(msg: S)
247    where
248        S: AsRef<str>,
249    {
250        Self::writeln(MessageKind::Warning, msg)
251    }
252
253    /// Writes error message.
254    pub fn err<S>(msg: S)
255    where
256        S: AsRef<str>,
257    {
258        Self::writeln(MessageKind::Error, msg)
259    }
260
261    /// Writes an information message once. See [`Self::write_once`] for more info.
262    pub fn info_once<S>(id: usize, msg: S) -> bool
263    where
264        S: AsRef<str>,
265    {
266        Self::writeln_once(id, MessageKind::Information, msg)
267    }
268
269    /// Writes a warning message. See [`Self::write_once`] for more info.
270    pub fn warn_once<S>(id: usize, msg: S) -> bool
271    where
272        S: AsRef<str>,
273    {
274        Self::writeln_once(id, MessageKind::Warning, msg)
275    }
276
277    /// Writes an error message once. See [`Self::write_once`] for more info.
278    pub fn err_once<S>(id: usize, msg: S) -> bool
279    where
280        S: AsRef<str>,
281    {
282        Self::writeln_once(id, MessageKind::Error, msg)
283    }
284
285    /// Enables or disables writing the messages to stdout stream.
286    pub fn enable_writing_to_stdout(enabled: bool) {
287        LOG.lock().write_to_stdout = enabled;
288    }
289
290    /// Returns `true` if the logger allowed writing to stdout stream, `false` - otherwise.
291    pub fn is_writing_to_stdout() -> bool {
292        LOG.lock().write_to_stdout
293    }
294
295    /// Sets verbosity level.
296    pub fn set_verbosity(kind: MessageKind) {
297        LOG.lock().verbosity = kind;
298    }
299
300    /// Returns current verbosity level of the logger.
301    pub fn verbosity() -> MessageKind {
302        LOG.lock().verbosity
303    }
304
305    /// Adds a listener that will receive a copy of every message passed into the log.
306    pub fn add_listener(listener: Sender<LogMessage>) {
307        LOG.lock().listeners.push(listener)
308    }
309
310    /// Allows you to verify that the result of the operation is Ok, or print the error in the log.
311    ///
312    /// # Use cases
313    ///
314    /// Typical use case for this method is that when you _can_ ignore errors, but want them to
315    /// be in the log.
316    pub fn verify<T, E>(result: Result<T, E>)
317    where
318        E: Display,
319    {
320        if let Err(e) = result {
321            Self::writeln(MessageKind::Error, format!("Operation failed! Reason: {e}"));
322        }
323    }
324
325    /// Allows you to verify that the result of the operation is Ok, or print the error in the log.
326    ///
327    /// # Use cases
328    ///
329    /// Typical use case for this method is that when you _can_ ignore errors, but want them to
330    /// be in the log.
331    pub fn verify_message<S, T, E>(result: Result<T, E>, msg: S)
332    where
333        E: Debug,
334        S: Display,
335    {
336        if let Err(e) = result {
337            Self::writeln(MessageKind::Error, format!("{msg}. Reason: {e:?}"));
338        }
339    }
340}
341
342#[macro_export]
343macro_rules! info {
344    ($($arg:tt)*) => {
345        $crate::log::Log::info(format!($($arg)*))
346    };
347}
348
349#[macro_export]
350macro_rules! warn {
351    ($($arg:tt)*) => {
352        $crate::log::Log::warn(format!($($arg)*))
353    };
354}
355
356#[macro_export]
357macro_rules! err {
358    ($($arg:tt)*) => {
359        $crate::log::Log::err(format!($($arg)*))
360    };
361}
362
363#[macro_export]
364macro_rules! info_once {
365    ($id:expr, $($arg:tt)*) => {
366        $crate::log::Log::info_once($id, format!($($arg)*))
367    };
368}
369
370#[macro_export]
371macro_rules! warn_once {
372    ($id:expr, $($arg:tt)*) => {
373        $crate::log::Log::warn_once($id, format!($($arg)*))
374    };
375}
376
377#[macro_export]
378macro_rules! err_once {
379    ($id:expr, $($arg:tt)*) => {
380        $crate::log::Log::err_once($id, format!($($arg)*))
381    };
382}