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}