firebase_rs_sdk/logger/
mod.rs

1use chrono::{SecondsFormat, Utc};
2use serde_json::Value;
3use std::borrow::Cow;
4use std::fmt;
5use std::str::FromStr;
6use std::sync::atomic::{AtomicU8, Ordering};
7use std::sync::{Arc, LazyLock, Mutex, RwLock, Weak};
8
9static GLOBAL_LOG_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Info as u8);
10static INSTANCES: LazyLock<Mutex<Vec<Weak<LoggerInner>>>> =
11    LazyLock::new(|| Mutex::new(Vec::new()));
12
13type SharedLogHandler = Arc<dyn Fn(&Logger, LogLevel, &[LogArgument]) + Send + Sync + 'static>;
14
15#[derive(Clone)]
16pub struct Logger {
17    inner: Arc<LoggerInner>,
18}
19
20impl Logger {
21    pub fn new(name: impl Into<String>) -> Self {
22        let inner = Arc::new(LoggerInner::new(name.into()));
23        track_instance(&inner);
24        Self { inner }
25    }
26
27    pub fn name(&self) -> &str {
28        &self.inner.name
29    }
30
31    pub fn log_level(&self) -> LogLevel {
32        LogLevel::from_u8(self.inner.log_level.load(Ordering::SeqCst))
33    }
34
35    pub fn set_log_level<L>(&self, level: L) -> Result<(), LogError>
36    where
37        L: IntoLogLevel,
38    {
39        let level = level.into_log_level()?;
40        self.inner.log_level.store(level as u8, Ordering::SeqCst);
41        Ok(())
42    }
43
44    pub fn log_handler(&self) -> SharedLogHandler {
45        self.inner.log_handler.read().unwrap().clone()
46    }
47
48    pub fn set_log_handler<F>(&self, handler: F)
49    where
50        F: Fn(&Logger, LogLevel, &[LogArgument]) + Send + Sync + 'static,
51    {
52        *self.inner.log_handler.write().unwrap() = Arc::new(handler);
53    }
54
55    pub fn reset_log_handler(&self) {
56        *self.inner.log_handler.write().unwrap() = default_log_handler_arc();
57    }
58
59    pub fn user_log_handler(&self) -> Option<SharedLogHandler> {
60        self.inner.user_log_handler.read().unwrap().clone()
61    }
62
63    pub fn set_user_log_handler<F>(&self, handler: Option<F>)
64    where
65        F: Fn(&Logger, LogLevel, &[LogArgument]) + Send + Sync + 'static,
66    {
67        *self.inner.user_log_handler.write().unwrap() =
68            handler.map(|f| Arc::new(f) as SharedLogHandler);
69    }
70
71    pub fn clear_user_log_handler(&self) {
72        self.inner.user_log_handler.write().unwrap().take();
73    }
74
75    pub fn debug(&self, arg: impl IntoLogArgument) {
76        self.emit_one(LogLevel::Debug, arg);
77    }
78
79    pub fn debug_with<I, T>(&self, args: I)
80    where
81        I: IntoIterator<Item = T>,
82        T: IntoLogArgument,
83    {
84        self.emit_many(LogLevel::Debug, args);
85    }
86
87    pub fn log(&self, arg: impl IntoLogArgument) {
88        self.emit_one(LogLevel::Verbose, arg);
89    }
90
91    pub fn log_with<I, T>(&self, args: I)
92    where
93        I: IntoIterator<Item = T>,
94        T: IntoLogArgument,
95    {
96        self.emit_many(LogLevel::Verbose, args);
97    }
98
99    pub fn info(&self, arg: impl IntoLogArgument) {
100        self.emit_one(LogLevel::Info, arg);
101    }
102
103    pub fn info_with<I, T>(&self, args: I)
104    where
105        I: IntoIterator<Item = T>,
106        T: IntoLogArgument,
107    {
108        self.emit_many(LogLevel::Info, args);
109    }
110
111    pub fn warn(&self, arg: impl IntoLogArgument) {
112        self.emit_one(LogLevel::Warn, arg);
113    }
114
115    pub fn warn_with<I, T>(&self, args: I)
116    where
117        I: IntoIterator<Item = T>,
118        T: IntoLogArgument,
119    {
120        self.emit_many(LogLevel::Warn, args);
121    }
122
123    pub fn error(&self, arg: impl IntoLogArgument) {
124        self.emit_one(LogLevel::Error, arg);
125    }
126
127    pub fn error_with<I, T>(&self, args: I)
128    where
129        I: IntoIterator<Item = T>,
130        T: IntoLogArgument,
131    {
132        self.emit_many(LogLevel::Error, args);
133    }
134
135    fn emit_one(&self, level: LogLevel, arg: impl IntoLogArgument) {
136        self.dispatch(level, vec![arg.into_log_argument()]);
137    }
138
139    fn emit_many<I, T>(&self, level: LogLevel, args: I)
140    where
141        I: IntoIterator<Item = T>,
142        T: IntoLogArgument,
143    {
144        let arguments = args
145            .into_iter()
146            .map(|arg| arg.into_log_argument())
147            .collect();
148        self.dispatch(level, arguments);
149    }
150
151    fn dispatch(&self, level: LogLevel, arguments: Vec<LogArgument>) {
152        let user_handler = self.user_log_handler();
153        if let Some(handler) = user_handler {
154            handler(self, level, &arguments);
155        }
156        (self.log_handler())(self, level, &arguments);
157    }
158
159    fn from_inner(inner: Arc<LoggerInner>) -> Self {
160        Self { inner }
161    }
162}
163
164struct LoggerInner {
165    name: String,
166    log_level: AtomicU8,
167    log_handler: RwLock<SharedLogHandler>,
168    user_log_handler: RwLock<Option<SharedLogHandler>>,
169}
170
171impl LoggerInner {
172    fn new(name: String) -> Self {
173        let level = GLOBAL_LOG_LEVEL.load(Ordering::SeqCst);
174        Self {
175            name,
176            log_level: AtomicU8::new(level),
177            log_handler: RwLock::new(default_log_handler_arc()),
178            user_log_handler: RwLock::new(None),
179        }
180    }
181}
182
183fn track_instance(inner: &Arc<LoggerInner>) {
184    INSTANCES.lock().unwrap().push(Arc::downgrade(inner));
185}
186
187fn default_log_handler_arc() -> SharedLogHandler {
188    Arc::new(default_log_handler)
189}
190
191fn default_log_handler(logger: &Logger, level: LogLevel, args: &[LogArgument]) {
192    if level < logger.log_level() {
193        return;
194    }
195
196    if level == LogLevel::Silent {
197        return;
198    }
199
200    let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
201    let message = build_message(args);
202    let header = format!("[{}]  {}:", now, logger.name());
203
204    match level {
205        LogLevel::Warn | LogLevel::Error => {
206            if message.is_empty() {
207                eprintln!("{header}");
208            } else {
209                eprintln!("{header} {message}");
210            }
211        }
212        _ => {
213            if message.is_empty() {
214                println!("{header}");
215            } else {
216                println!("{header} {message}");
217            }
218        }
219    }
220}
221
222fn build_message(args: &[LogArgument]) -> String {
223    args.iter()
224        .filter_map(LogArgument::to_message_fragment)
225        .collect::<Vec<_>>()
226        .join(" ")
227}
228
229fn with_instances<F>(mut f: F)
230where
231    F: FnMut(Logger),
232{
233    let mut instances = INSTANCES.lock().unwrap();
234    let mut i = 0;
235    while i < instances.len() {
236        match instances[i].upgrade() {
237            Some(inner) => {
238                f(Logger::from_inner(inner));
239                i += 1;
240            }
241            None => {
242                instances.swap_remove(i);
243            }
244        }
245    }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
249#[repr(u8)]
250pub enum LogLevel {
251    Debug = 0,
252    Verbose = 1,
253    Info = 2,
254    Warn = 3,
255    Error = 4,
256    Silent = 5,
257}
258
259impl LogLevel {
260    pub fn as_str(self) -> &'static str {
261        match self {
262            LogLevel::Debug => "debug",
263            LogLevel::Verbose => "verbose",
264            LogLevel::Info => "info",
265            LogLevel::Warn => "warn",
266            LogLevel::Error => "error",
267            LogLevel::Silent => "silent",
268        }
269    }
270
271    fn from_u8(value: u8) -> Self {
272        match value {
273            0 => LogLevel::Debug,
274            1 => LogLevel::Verbose,
275            2 => LogLevel::Info,
276            3 => LogLevel::Warn,
277            4 => LogLevel::Error,
278            _ => LogLevel::Silent,
279        }
280    }
281}
282
283impl fmt::Display for LogLevel {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        let label = match self {
286            LogLevel::Debug => "DEBUG",
287            LogLevel::Verbose => "VERBOSE",
288            LogLevel::Info => "INFO",
289            LogLevel::Warn => "WARN",
290            LogLevel::Error => "ERROR",
291            LogLevel::Silent => "SILENT",
292        };
293        f.write_str(label)
294    }
295}
296
297impl FromStr for LogLevel {
298    type Err = LogError;
299
300    fn from_str(s: &str) -> Result<Self, Self::Err> {
301        match s.to_ascii_lowercase().as_str() {
302            "debug" => Ok(LogLevel::Debug),
303            "verbose" => Ok(LogLevel::Verbose),
304            "info" => Ok(LogLevel::Info),
305            "warn" | "warning" => Ok(LogLevel::Warn),
306            "error" => Ok(LogLevel::Error),
307            "silent" => Ok(LogLevel::Silent),
308            other => Err(LogError::InvalidLogLevel(other.to_string())),
309        }
310    }
311}
312
313pub trait IntoLogLevel {
314    fn into_log_level(self) -> Result<LogLevel, LogError>;
315}
316
317impl IntoLogLevel for LogLevel {
318    fn into_log_level(self) -> Result<LogLevel, LogError> {
319        Ok(self)
320    }
321}
322
323impl IntoLogLevel for &str {
324    fn into_log_level(self) -> Result<LogLevel, LogError> {
325        LogLevel::from_str(self)
326    }
327}
328
329impl IntoLogLevel for String {
330    fn into_log_level(self) -> Result<LogLevel, LogError> {
331        LogLevel::from_str(&self)
332    }
333}
334
335#[derive(Debug, Clone, Default)]
336pub struct LogOptions {
337    pub level: Option<LogLevel>,
338}
339
340impl LogOptions {
341    pub fn with_level<L>(mut self, level: L) -> Result<Self, LogError>
342    where
343        L: IntoLogLevel,
344    {
345        self.level = Some(level.into_log_level()?);
346        Ok(self)
347    }
348}
349
350#[derive(Debug, Clone)]
351pub struct LogCallbackParams {
352    pub level: LogLevel,
353    pub message: String,
354    pub args: Vec<Value>,
355    pub logger_type: String,
356}
357
358impl LogCallbackParams {
359    pub fn level_label(&self) -> &'static str {
360        self.level.as_str()
361    }
362}
363
364pub type LogCallback = Arc<dyn Fn(LogCallbackParams) + Send + Sync + 'static>;
365
366#[derive(Debug, Clone, PartialEq)]
367pub enum LogArgument {
368    Text(String),
369    Value(Value),
370    Null,
371}
372
373impl LogArgument {
374    pub fn text<S: Into<String>>(value: S) -> Self {
375        LogArgument::Text(value.into())
376    }
377
378    pub fn value(value: Value) -> Self {
379        LogArgument::Value(value)
380    }
381
382    pub fn null() -> Self {
383        LogArgument::Null
384    }
385
386    pub fn to_message_fragment(&self) -> Option<String> {
387        match self {
388            LogArgument::Text(text) => Some(text.clone()),
389            LogArgument::Value(Value::Null) | LogArgument::Null => None,
390            LogArgument::Value(Value::String(text)) => Some(text.clone()),
391            LogArgument::Value(Value::Bool(flag)) => Some(flag.to_string()),
392            LogArgument::Value(Value::Number(number)) => Some(number.to_string()),
393            LogArgument::Value(other) => Some(other.to_string()),
394        }
395    }
396
397    pub fn to_callback_value(&self) -> Value {
398        match self {
399            LogArgument::Text(text) => Value::String(text.clone()),
400            LogArgument::Value(value) => value.clone(),
401            LogArgument::Null => Value::Null,
402        }
403    }
404}
405
406pub trait IntoLogArgument {
407    fn into_log_argument(self) -> LogArgument;
408}
409
410impl IntoLogArgument for LogArgument {
411    fn into_log_argument(self) -> LogArgument {
412        self
413    }
414}
415
416impl IntoLogArgument for &LogArgument {
417    fn into_log_argument(self) -> LogArgument {
418        self.clone()
419    }
420}
421
422impl IntoLogArgument for String {
423    fn into_log_argument(self) -> LogArgument {
424        LogArgument::Text(self)
425    }
426}
427
428impl IntoLogArgument for &String {
429    fn into_log_argument(self) -> LogArgument {
430        LogArgument::Text(self.clone())
431    }
432}
433
434impl IntoLogArgument for &str {
435    fn into_log_argument(self) -> LogArgument {
436        LogArgument::Text(self.to_owned())
437    }
438}
439
440impl<'a> IntoLogArgument for Cow<'a, str> {
441    fn into_log_argument(self) -> LogArgument {
442        LogArgument::Text(self.into_owned())
443    }
444}
445
446impl IntoLogArgument for bool {
447    fn into_log_argument(self) -> LogArgument {
448        LogArgument::Value(Value::Bool(self))
449    }
450}
451
452macro_rules! impl_signed_int_argument {
453    ($($ty:ty),* $(,)?) => {
454        $(
455            impl IntoLogArgument for $ty {
456                fn into_log_argument(self) -> LogArgument {
457                    let number = serde_json::Number::from(self as i64);
458                    LogArgument::Value(Value::Number(number))
459                }
460            }
461        )*
462    };
463}
464
465macro_rules! impl_unsigned_int_argument {
466    ($($ty:ty),* $(,)?) => {
467        $(
468            impl IntoLogArgument for $ty {
469                fn into_log_argument(self) -> LogArgument {
470                    let number = serde_json::Number::from(self as u64);
471                    LogArgument::Value(Value::Number(number))
472                }
473            }
474        )*
475    };
476}
477
478impl_signed_int_argument!(i8, i16, i32, i64, isize);
479impl_unsigned_int_argument!(u8, u16, u32, u64, usize);
480
481impl IntoLogArgument for f32 {
482    fn into_log_argument(self) -> LogArgument {
483        (self as f64).into_log_argument()
484    }
485}
486
487impl IntoLogArgument for f64 {
488    fn into_log_argument(self) -> LogArgument {
489        match serde_json::Number::from_f64(self) {
490            Some(number) => LogArgument::Value(Value::Number(number)),
491            None => LogArgument::Null,
492        }
493    }
494}
495
496impl IntoLogArgument for Value {
497    fn into_log_argument(self) -> LogArgument {
498        LogArgument::Value(self)
499    }
500}
501
502impl IntoLogArgument for &Value {
503    fn into_log_argument(self) -> LogArgument {
504        LogArgument::Value(self.clone())
505    }
506}
507
508impl<T> IntoLogArgument for Option<T>
509where
510    T: IntoLogArgument,
511{
512    fn into_log_argument(self) -> LogArgument {
513        match self {
514            Some(value) => value.into_log_argument(),
515            None => LogArgument::Null,
516        }
517    }
518}
519
520impl IntoLogArgument for () {
521    fn into_log_argument(self) -> LogArgument {
522        LogArgument::Null
523    }
524}
525
526pub fn log_arg<T>(value: T) -> LogArgument
527where
528    T: IntoLogArgument,
529{
530    value.into_log_argument()
531}
532
533#[derive(Debug, Clone)]
534pub enum LogError {
535    InvalidLogLevel(String),
536}
537
538impl fmt::Display for LogError {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        match self {
541            LogError::InvalidLogLevel(level) => {
542                write!(f, "Invalid value \"{level}\" assigned to `logLevel`")
543            }
544        }
545    }
546}
547
548impl std::error::Error for LogError {}
549
550pub fn set_log_level<L>(level: L) -> Result<(), LogError>
551where
552    L: IntoLogLevel,
553{
554    let level = level.into_log_level()?;
555    GLOBAL_LOG_LEVEL.store(level as u8, Ordering::SeqCst);
556    with_instances(|logger| {
557        let _ = logger.set_log_level(level);
558    });
559    Ok(())
560}
561
562pub fn set_user_log_handler(callback: Option<LogCallback>, options: Option<LogOptions>) {
563    let options = options.unwrap_or_default();
564
565    match callback {
566        Some(cb) => {
567            let custom_level = options.level;
568            with_instances(|logger| {
569                let handler_cb = Arc::clone(&cb);
570                logger.set_user_log_handler(Some(
571                    move |instance: &Logger, level, args: &[LogArgument]| {
572                        let threshold = custom_level.unwrap_or_else(|| instance.log_level());
573                        if level < threshold {
574                            return;
575                        }
576                        let message = build_message(args);
577                        let params = LogCallbackParams {
578                            level,
579                            message,
580                            args: args.iter().map(LogArgument::to_callback_value).collect(),
581                            logger_type: instance.name().to_owned(),
582                        };
583                        handler_cb(params);
584                    },
585                ));
586            });
587        }
588        None => {
589            with_instances(|logger| {
590                logger.clear_user_log_handler();
591            });
592        }
593    }
594}
595
596pub fn set_user_log_handler_fn<F>(callback: Option<F>, options: Option<LogOptions>)
597where
598    F: Fn(LogCallbackParams) + Send + Sync + 'static,
599{
600    let wrapped = callback.map(|cb| Arc::new(cb) as LogCallback);
601    set_user_log_handler(wrapped, options);
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use std::sync::{Arc, Mutex};
608
609    static TEST_GUARD: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
610
611    fn reset_logging() {
612        set_log_level(LogLevel::Info).unwrap();
613        set_user_log_handler(None, None);
614    }
615
616    #[test]
617    fn log_methods_respect_global_level() {
618        let _guard = TEST_GUARD.lock().unwrap();
619        reset_logging();
620        let logger = Logger::new("@firebase/logger-args-test");
621
622        set_log_level(LogLevel::Debug).unwrap();
623
624        let records = Arc::new(Mutex::new(Vec::new()));
625        let handler_records = Arc::clone(&records);
626
627        logger.set_log_handler(move |instance, level, args| {
628            if level < instance.log_level() {
629                return;
630            }
631            handler_records
632                .lock()
633                .unwrap()
634                .push((level, build_message(args)));
635        });
636
637        logger.debug("debug message");
638        logger.log("verbose message");
639        logger.info("info message");
640        logger.warn("warn message");
641        logger.error("error message");
642
643        let stored = records.lock().unwrap();
644        let levels: Vec<_> = stored.iter().map(|(level, _)| *level).collect();
645        assert_eq!(
646            levels,
647            [
648                LogLevel::Debug,
649                LogLevel::Verbose,
650                LogLevel::Info,
651                LogLevel::Warn,
652                LogLevel::Error,
653            ]
654        );
655        assert_eq!(stored[0].1, "debug message");
656    }
657
658    #[test]
659    fn log_level_string_filtering() {
660        let _guard = TEST_GUARD.lock().unwrap();
661        reset_logging();
662        let logger = Logger::new("@firebase/logger-custom-level");
663        set_log_level("warn").unwrap();
664
665        let records = Arc::new(Mutex::new(Vec::new()));
666        let handler_records = Arc::clone(&records);
667        logger.set_log_handler(move |instance, level, args| {
668            if level < instance.log_level() {
669                return;
670            }
671            handler_records
672                .lock()
673                .unwrap()
674                .push((level, build_message(args)));
675        });
676
677        logger.debug("debug message");
678        logger.log("verbose message");
679        logger.info("info message");
680        logger.warn("warn message");
681        logger.error("error message");
682
683        let stored = records.lock().unwrap();
684        let levels: Vec<_> = stored.iter().map(|(level, _)| *level).collect();
685        assert_eq!(levels, [LogLevel::Warn, LogLevel::Error]);
686        assert_eq!(stored[0].1, "warn message");
687    }
688
689    #[test]
690    fn user_log_handler_receives_arguments() {
691        let _guard = TEST_GUARD.lock().unwrap();
692        reset_logging();
693        let logger = Logger::new("@firebase/test-logger");
694        let logger_name = logger.name().to_owned();
695
696        let captured = Arc::new(Mutex::new(Vec::new()));
697        let captured_cb = Arc::clone(&captured);
698
699        set_user_log_handler_fn(
700            Some({
701                let logger_name = logger_name.clone();
702                move |params: LogCallbackParams| {
703                    if params.logger_type == logger_name {
704                        captured_cb.lock().unwrap().push(params);
705                    }
706                }
707            }),
708            None,
709        );
710
711        assert!(logger.user_log_handler().is_some());
712
713        logger.info_with(vec![
714            log_arg("info message!"),
715            log_arg(serde_json::json!(["hello"])),
716            log_arg(1),
717            log_arg(serde_json::json!({"a": 3})),
718        ]);
719
720        let records = captured.lock().unwrap().clone();
721        assert_eq!(records.len(), 1, "expected a single callback invocation");
722        let levels: Vec<_> = records.iter().map(|params| params.level).collect();
723        assert_eq!(levels, [LogLevel::Info]);
724        let params = records.into_iter().next().unwrap();
725        assert_eq!(params.level, LogLevel::Info);
726        assert_eq!(params.level_label(), "info");
727        assert_eq!(params.message, "info message! [\"hello\"] 1 {\"a\":3}");
728        assert_eq!(params.logger_type, logger_name);
729        assert_eq!(params.args.len(), 4);
730    }
731
732    #[test]
733    fn user_handler_respects_custom_level() {
734        let _guard = TEST_GUARD.lock().unwrap();
735        reset_logging();
736        let logger = Logger::new("@firebase/test-logger");
737        let logger_name = logger.name().to_owned();
738
739        let captured = Arc::new(Mutex::new(Vec::new()));
740        let captured_cb = Arc::clone(&captured);
741
742        set_user_log_handler_fn(
743            Some({
744                let logger_name = logger_name.clone();
745                move |params: LogCallbackParams| {
746                    if params.logger_type == logger_name {
747                        captured_cb.lock().unwrap().push(params.level);
748                    }
749                }
750            }),
751            Some(LogOptions {
752                level: Some(LogLevel::Warn),
753            }),
754        );
755
756        assert!(logger.user_log_handler().is_some());
757
758        logger.info("info message");
759        logger.warn("warn message");
760        logger.error("error message");
761
762        let levels = captured.lock().unwrap();
763        assert_eq!(levels.as_slice(), &[LogLevel::Warn, LogLevel::Error]);
764    }
765}