1#![deny(missing_docs)]
27
28use core::convert::Infallible;
29use std::borrow::Cow;
30use std::cell::RefCell;
31use std::future::ready;
32use std::sync::Arc;
33
34use ::id_effect::{BoxFuture, Effect, EffectHashMap, FiberRef, Get, Here, IntoBind, box_future};
35
36mod pipeline;
37
38pub use pipeline::{
39 CompositeLogBackend, JsonLogBackend, LogBackend, LogRecord, Logger, StructuredLogBackend,
40 TracingLogBackend,
41};
42
43use pipeline::TracingLogBackend as TracingSink;
44
45pub trait NeedsEffectLogger: Get<EffectLogKey, Here, Target = EffectLogger> {}
53impl<R: Get<EffectLogKey, Here, Target = EffectLogger>> NeedsEffectLogger for R {}
54
55id_effect::service_key!(
56 pub struct EffectLogKey
58);
59
60id_effect::service_key!(
61 pub struct EffectLogMinLevelKey
63);
64
65thread_local! {
66 static MIN_LOG_LEVEL_FIBER_REF: RefCell<Option<FiberRef<LogLevel>>> = const { RefCell::new(None) };
67}
68
69thread_local! {
70 static COMPOSITE_LOG_BACKEND: RefCell<Option<Arc<CompositeLogBackend>>> = const { RefCell::new(None) };
71}
72
73thread_local! {
74 static LOG_ANNOTATIONS_FIBER_REF: RefCell<Option<FiberRef<EffectHashMap<String, String>>>> =
75 const { RefCell::new(None) };
76}
77
78thread_local! {
79 static LOG_SPAN_STACK_FIBER_REF: RefCell<Option<FiberRef<Vec<String>>>> = const { RefCell::new(None) };
80}
81
82fn install_min_log_level_fiber_ref(fr: FiberRef<LogLevel>) {
83 MIN_LOG_LEVEL_FIBER_REF.with(|c| {
84 *c.borrow_mut() = Some(fr);
85 });
86}
87
88fn install_composite_log_backend(c: Arc<CompositeLogBackend>) {
89 COMPOSITE_LOG_BACKEND.with(|cell| {
90 *cell.borrow_mut() = Some(c);
91 });
92}
93
94fn install_log_annotations_fiber_ref(fr: FiberRef<EffectHashMap<String, String>>) {
95 LOG_ANNOTATIONS_FIBER_REF.with(|c| {
96 *c.borrow_mut() = Some(fr);
97 });
98}
99
100fn install_log_spans_fiber_ref(fr: FiberRef<Vec<String>>) {
101 LOG_SPAN_STACK_FIBER_REF.with(|c| {
102 *c.borrow_mut() = Some(fr);
103 });
104}
105
106#[cfg(test)]
107fn test_clear_min_log_level_fiber_ref() {
108 MIN_LOG_LEVEL_FIBER_REF.with(|c| {
109 *c.borrow_mut() = None;
110 });
111}
112
113#[cfg(test)]
114fn test_clear_composite_log_backend() {
115 COMPOSITE_LOG_BACKEND.with(|c| {
116 *c.borrow_mut() = None;
117 });
118}
119
120#[cfg(test)]
121fn test_clear_log_metadata_fiber_refs() {
122 LOG_ANNOTATIONS_FIBER_REF.with(|c| *c.borrow_mut() = None);
123 LOG_SPAN_STACK_FIBER_REF.with(|c| *c.borrow_mut() = None);
124}
125
126#[cfg(test)]
127fn test_clear_all_logger_tls() {
128 test_clear_min_log_level_fiber_ref();
129 test_clear_composite_log_backend();
130 test_clear_log_metadata_fiber_refs();
131}
132
133#[derive(Clone, Copy, Debug, Default)]
139pub struct EffectLogger;
140
141#[derive(Debug, Clone, ::id_effect::EffectData)]
149pub enum EffectLoggerError {
150 Sink(String),
152}
153
154impl std::fmt::Display for EffectLoggerError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 EffectLoggerError::Sink(msg) => write!(f, "log sink error: {msg}"),
158 }
159 }
160}
161
162impl std::error::Error for EffectLoggerError {}
163
164impl From<Infallible> for EffectLoggerError {
165 fn from(e: Infallible) -> Self {
166 match e {}
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct LogContext {
173 pub timestamp: ::id_effect::UtcDateTime,
175}
176
177impl LogContext {
178 #[inline]
180 pub const fn new(timestamp: ::id_effect::UtcDateTime) -> Self {
181 Self { timestamp }
182 }
183
184 #[inline]
186 pub fn with_now_timestamp() -> Self {
187 Self {
188 timestamp: ::id_effect::UtcDateTime::from_std(std::time::SystemTime::now())
189 .expect("system time should be representable as UtcDateTime"),
190 }
191 }
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196#[repr(u8)]
197pub enum LogLevel {
198 Trace = 0,
200 Debug = 1,
202 Info = 2,
204 Warn = 3,
206 Error = 4,
208 Fatal = 5,
210 None = 255,
212}
213
214impl LogLevel {
215 #[inline]
217 pub const fn severity(self) -> u8 {
218 match self {
219 LogLevel::None => 255,
220 _ => self as u8,
221 }
222 }
223
224 #[inline]
226 pub const fn allows(self, message_level: LogLevel) -> bool {
227 match (self, message_level) {
228 (LogLevel::None, _) | (_, LogLevel::None) => false,
229 _ => message_level.severity() >= self.severity(),
230 }
231 }
232
233 #[inline]
235 pub const fn as_str(self) -> &'static str {
236 match self {
237 LogLevel::Trace => "TRACE",
238 LogLevel::Debug => "DEBUG",
239 LogLevel::Info => "INFO",
240 LogLevel::Warn => "WARN",
241 LogLevel::Error => "ERROR",
242 LogLevel::Fatal => "FATAL",
243 LogLevel::None => "NONE",
244 }
245 }
246}
247
248impl std::str::FromStr for LogLevel {
249 type Err = String;
250
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 match s.trim().to_ascii_lowercase().as_str() {
253 "trace" => Ok(LogLevel::Trace),
254 "debug" => Ok(LogLevel::Debug),
255 "info" => Ok(LogLevel::Info),
256 "warn" | "warning" => Ok(LogLevel::Warn),
257 "error" => Ok(LogLevel::Error),
258 "fatal" => Ok(LogLevel::Fatal),
259 "none" => Ok(LogLevel::None),
260 other => Err(format!("unknown log level: {other:?}")),
261 }
262 }
263}
264
265impl EffectLogger {
266 pub fn with_minimum_log_level<B, E, R>(
268 fiber_ref: FiberRef<LogLevel>,
269 level: LogLevel,
270 inner: Effect<B, E, R>,
271 ) -> Effect<B, E, R>
272 where
273 B: 'static,
274 E: 'static,
275 R: 'static,
276 {
277 fiber_ref.locally(level, inner)
278 }
279
280 pub fn log<R: 'static>(
289 &self,
290 level: LogLevel,
291 msg: impl Into<Cow<'static, str>>,
292 ) -> Effect<(), EffectLoggerError, R> {
293 let msg = msg.into();
294 if level == LogLevel::None {
295 return Effect::new(|_r: &mut R| Ok(()));
296 }
297 Effect::new(move |_r: &mut R| {
298 let emit = MIN_LOG_LEVEL_FIBER_REF.with(|c| match c.borrow().as_ref() {
299 None => true,
300 Some(fr) => ::id_effect::run_blocking(fr.get(), ())
301 .map(|min| min.allows(level))
302 .unwrap_or(true),
303 });
304 if !emit {
305 return Ok(());
306 }
307
308 let annotations = LOG_ANNOTATIONS_FIBER_REF
309 .with(|c| {
310 c.borrow()
311 .as_ref()
312 .and_then(|fr| ::id_effect::run_blocking(fr.get(), ()).ok())
313 })
314 .unwrap_or_default();
315
316 let spans = LOG_SPAN_STACK_FIBER_REF
317 .with(|c| {
318 c.borrow()
319 .as_ref()
320 .and_then(|fr| ::id_effect::run_blocking(fr.get(), ()).ok())
321 })
322 .unwrap_or_default();
323
324 let rec = LogRecord {
325 level,
326 message: msg.clone(),
327 annotations,
328 spans,
329 };
330
331 COMPOSITE_LOG_BACKEND.with(|c| {
332 if let Some(comp) = c.borrow().as_ref() {
333 comp.emit_all(&rec)?;
334 } else {
335 LogBackend::emit(&TracingSink, &rec)?;
336 }
337 Ok::<(), EffectLoggerError>(())
338 })?;
339 Ok(())
340 })
341 }
342
343 #[inline]
345 pub fn log_string<R: 'static>(
346 &self,
347 level: LogLevel,
348 msg: String,
349 ) -> Effect<(), EffectLoggerError, R> {
350 self.log(level, msg)
351 }
352
353 pub fn trace<R: 'static>(
355 &self,
356 msg: impl Into<Cow<'static, str>>,
357 ) -> Effect<(), EffectLoggerError, R> {
358 self.log(LogLevel::Trace, msg)
359 }
360
361 pub fn debug<R: 'static>(
363 &self,
364 msg: impl Into<Cow<'static, str>>,
365 ) -> Effect<(), EffectLoggerError, R> {
366 self.log(LogLevel::Debug, msg)
367 }
368
369 pub fn info<R: 'static>(
371 &self,
372 msg: impl Into<Cow<'static, str>>,
373 ) -> Effect<(), EffectLoggerError, R> {
374 self.log(LogLevel::Info, msg)
375 }
376
377 pub fn warn<R: 'static>(
379 &self,
380 msg: impl Into<Cow<'static, str>>,
381 ) -> Effect<(), EffectLoggerError, R> {
382 self.log(LogLevel::Warn, msg)
383 }
384
385 pub fn error<R: 'static>(
387 &self,
388 msg: impl Into<Cow<'static, str>>,
389 ) -> Effect<(), EffectLoggerError, R> {
390 self.log(LogLevel::Error, msg)
391 }
392
393 pub fn fatal<R: 'static>(
395 &self,
396 msg: impl Into<Cow<'static, str>>,
397 ) -> Effect<(), EffectLoggerError, R> {
398 self.log(LogLevel::Fatal, msg)
399 }
400}
401
402impl<'a, R> IntoBind<'a, R, EffectLogger, EffectLoggerError> for EffectLogger
412where
413 R: Get<EffectLogKey, Here, Target = EffectLogger> + 'a,
414{
415 fn into_bind(self, r: &'a mut R) -> BoxFuture<'a, Result<EffectLogger, EffectLoggerError>> {
416 Box::pin(ready(Ok(*Get::<EffectLogKey, Here>::get(r))))
417 }
418}
419
420#[inline]
422pub fn layer_effect_logger() -> id_effect::layer::LayerFn<
423 impl Fn() -> Result<id_effect::Service<EffectLogKey, EffectLogger>, Infallible>,
424> {
425 id_effect::layer_service(EffectLogger)
426}
427
428#[inline]
431pub fn layer_minimum_log_level(
432 initial: LogLevel,
433) -> id_effect::layer::LayerEffect<
434 id_effect::Service<EffectLogMinLevelKey, FiberRef<LogLevel>>,
435 (),
436 (),
437> {
438 id_effect::layer::effect(FiberRef::make(move || initial).flat_map(|fr| {
439 Effect::new(move |_r: &mut ()| {
440 install_min_log_level_fiber_ref(fr.clone());
441 Ok(id_effect::service::<EffectLogMinLevelKey, _>(fr))
442 })
443 }))
444}
445
446#[inline]
449pub fn layer_composite_logger(
450 composite: Arc<CompositeLogBackend>,
451) -> id_effect::layer::LayerEffect<(), (), ()> {
452 let c = composite.clone();
453 id_effect::layer::effect(Effect::new(move |_r: &mut ()| {
454 install_composite_log_backend(c.clone());
455 Ok(())
456 }))
457}
458
459#[inline]
462pub fn layer_log_metadata() -> id_effect::layer::LayerEffect<(), (), ()> {
463 use ::id_effect::collections::hash_map;
464 let eff = FiberRef::make_with(
465 hash_map::empty::<String, String>,
466 |m| m.clone(),
467 |p, _c| p.clone(),
468 )
469 .flat_map(|ann| {
470 FiberRef::make_with(Vec::<String>::new, |v| v.clone(), |p, _c| p.clone()).flat_map(move |sp| {
471 Effect::new(move |_r: &mut ()| {
472 install_log_annotations_fiber_ref(ann.clone());
473 install_log_spans_fiber_ref(sp.clone());
474 Ok(())
475 })
476 })
477 });
478 id_effect::layer::effect(eff)
479}
480
481pub fn annotate_logs<A, E, R>(
483 key: impl Into<String> + Send + 'static,
484 value: impl Into<String> + Send + 'static,
485 inner: Effect<A, E, R>,
486) -> Effect<A, E, R>
487where
488 A: Send + 'static,
489 E: Send + 'static,
490 R: Send + 'static,
491{
492 let key = key.into();
493 let value = value.into();
494 Effect::new_async(move |r| {
495 let tls = LOG_ANNOTATIONS_FIBER_REF.with(|c| c.borrow().clone());
496 let Some(fr) = tls else {
497 return box_future(async move { inner.run(r).await });
498 };
499 let cur = match ::id_effect::run_blocking(fr.get(), ()) {
500 Ok(m) => m,
501 Err(_) => return box_future(async move { inner.run(r).await }),
502 };
503 let next = ::id_effect::collections::hash_map::set(&cur, key, value);
504 let fr = fr.clone();
505 box_future(async move { fr.locally(next, inner).run(r).await })
506 })
507}
508
509pub fn with_log_span<A, E, R>(
511 label: impl Into<String> + Send + 'static,
512 inner: Effect<A, E, R>,
513) -> Effect<A, E, R>
514where
515 A: Send + 'static,
516 E: Send + 'static,
517 R: Send + 'static,
518{
519 let label = label.into();
520 Effect::new_async(move |r| {
521 let tls = LOG_SPAN_STACK_FIBER_REF.with(|c| c.borrow().clone());
522 let Some(fr) = tls else {
523 return box_future(async move { inner.run(r).await });
524 };
525 let cur = ::id_effect::run_blocking(fr.get(), ()).unwrap_or_default();
526 let mut next = cur.clone();
527 next.push(label);
528 let fr = fr.clone();
529 box_future(async move { fr.locally(next, inner).run(r).await })
530 })
531}
532
533#[cfg(test)]
534mod tests {
535 use rstest::rstest;
536
537 use super::*;
538 use ::id_effect::{Cons, Context, Layer, Nil, Service, run_blocking};
539
540 type LogCtx = Context<Cons<Service<EffectLogKey, EffectLogger>, Nil>>;
543
544 type LogCtxMin = Context<
545 Cons<
546 Service<EffectLogKey, EffectLogger>,
547 Cons<Service<EffectLogMinLevelKey, FiberRef<LogLevel>>, Nil>,
548 >,
549 >;
550
551 fn test_ctx() -> LogCtx {
552 Context::new(Cons(Service::<EffectLogKey, _>::new(EffectLogger), Nil))
553 }
554
555 fn test_ctx_with_min(initial: LogLevel) -> LogCtxMin {
556 test_clear_all_logger_tls();
557 let logger = layer_effect_logger().build().expect("logger layer");
558 let min = layer_minimum_log_level(initial).build().expect("min layer");
559 Context::new(Cons(logger, Cons(min, Nil)))
560 }
561
562 fn init_subscriber() {
563 test_clear_all_logger_tls();
564 let _ = tracing_subscriber::fmt()
565 .with_env_filter(tracing_subscriber::EnvFilter::new("trace"))
566 .with_test_writer()
567 .try_init();
568 }
569
570 mod fiber_min_log_level {
573 use super::*;
574 use std::sync::{Arc, Mutex};
575 use tracing::Subscriber;
576 use tracing_subscriber::Registry;
577 use tracing_subscriber::layer::SubscriberExt;
578 use tracing_subscriber::layer::{Context, Layer};
579
580 struct Capture(Arc<Mutex<Vec<tracing::Level>>>);
581
582 impl<S: Subscriber> Layer<S> for Capture {
583 fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
584 self
585 .0
586 .lock()
587 .expect("capture mutex")
588 .push(*event.metadata().level());
589 }
590 }
591
592 fn subscriber_with_capture(levels: Arc<Mutex<Vec<tracing::Level>>>) -> impl Subscriber {
593 Registry::default().with(Capture(levels))
594 }
595
596 #[test]
597 fn logger_filters_below_minimum_level() {
598 test_clear_all_logger_tls();
599 let levels = Arc::new(Mutex::new(Vec::new()));
600 let _g = tracing::subscriber::set_default(subscriber_with_capture(levels.clone()));
601
602 let ctx = test_ctx_with_min(LogLevel::Trace);
603 let fr = ctx.0.1.0.value.clone();
604 run_blocking(fr.set(LogLevel::Warn), ()).expect("set min");
605 run_blocking(EffectLogger.info::<LogCtxMin>("filtered-info"), ctx).expect("log");
606
607 let got = levels.lock().expect("capture");
608 assert!(
609 !got.contains(&tracing::Level::INFO),
610 "expected INFO suppressed, got {got:?}"
611 );
612 }
613
614 #[test]
615 fn logger_with_minimum_log_level_overrides_globally() {
616 test_clear_all_logger_tls();
617 let levels = Arc::new(Mutex::new(Vec::new()));
618 let _g = tracing::subscriber::set_default(subscriber_with_capture(levels.clone()));
619
620 let ctx = test_ctx_with_min(LogLevel::Trace);
621 let fr = ctx.0.1.0.value.clone();
622 let inner = EffectLogger.info::<LogCtxMin>("inside-scope");
623 run_blocking(
624 EffectLogger::with_minimum_log_level(fr.clone(), LogLevel::Warn, inner),
625 ctx,
626 )
627 .expect("scoped");
628
629 assert!(
630 !levels
631 .lock()
632 .expect("capture")
633 .contains(&tracing::Level::INFO),
634 "expected INFO suppressed inside locally"
635 );
636
637 levels.lock().expect("capture").clear();
638 let ctx = test_ctx_with_min(LogLevel::Trace);
639 run_blocking(EffectLogger.info::<LogCtxMin>("outside-scope"), ctx).expect("outer");
640
641 assert!(
642 levels
643 .lock()
644 .expect("capture")
645 .contains(&tracing::Level::INFO),
646 "expected INFO after new stack without Warn override"
647 );
648 }
649
650 #[test]
651 fn logger_restores_level_after_locally_scope() {
652 test_clear_all_logger_tls();
653 let _g =
654 tracing::subscriber::set_default(subscriber_with_capture(Arc::new(Mutex::new(Vec::new()))));
655
656 let ctx = test_ctx_with_min(LogLevel::Trace);
657 let fr = ctx.0.1.0.value.clone();
658 run_blocking(fr.set(LogLevel::Debug), ()).expect("set");
659 assert_eq!(run_blocking(fr.get::<()>(), ()), Ok(LogLevel::Debug));
660
661 let scoped =
662 EffectLogger::with_minimum_log_level(fr.clone(), LogLevel::Warn, fr.get::<LogCtxMin>());
663 assert_eq!(run_blocking(scoped, ctx), Ok(LogLevel::Warn));
664 assert_eq!(run_blocking(fr.get::<()>(), ()), Ok(LogLevel::Debug));
665 }
666 }
667
668 mod log_context {
671 use super::*;
672 use ::id_effect::UtcDateTime;
673
674 #[test]
675 fn log_context_timestamp_format_iso() {
676 let ctx = LogContext::new(UtcDateTime::unsafe_make(1_700_000_000_000));
677 let s = ctx.timestamp.format_iso();
678 assert!(
679 s.ends_with('Z'),
680 "format_iso should be UTC / RFC 3339 style: {s}"
681 );
682 assert!(s.contains('T'), "expected date-time separator: {s}");
683 }
684
685 #[test]
686 fn log_context_with_now_timestamp_is_valid_iso() {
687 let ctx = LogContext::with_now_timestamp();
688 let s = ctx.timestamp.format_iso();
689 assert!(s.ends_with('Z'), "{s}");
690 assert!(s.contains('T'), "{s}");
691 }
692 }
693
694 mod effect_logger_log {
697 use super::*;
698
699 mod with_unit_env {
700 use super::*;
701
702 #[rstest]
703 #[case::trace(LogLevel::Trace)]
704 #[case::debug(LogLevel::Debug)]
705 #[case::info(LogLevel::Info)]
706 #[case::warn(LogLevel::Warn)]
707 #[case::error(LogLevel::Error)]
708 #[case::fatal(LogLevel::Fatal)]
709 fn returns_ok_for_every_level(#[case] level: LogLevel) {
710 init_subscriber();
711 let result = run_blocking(EffectLogger.log::<()>(level, "msg"), ());
712 assert_eq!(result, Ok(()));
713 }
714 }
715
716 mod with_context_env {
717 use super::*;
718
719 #[rstest]
720 #[case::trace(LogLevel::Trace)]
721 #[case::debug(LogLevel::Debug)]
722 #[case::info(LogLevel::Info)]
723 #[case::warn(LogLevel::Warn)]
724 #[case::error(LogLevel::Error)]
725 #[case::fatal(LogLevel::Fatal)]
726 fn returns_ok_for_every_level(#[case] level: LogLevel) {
727 init_subscriber();
728 let result = run_blocking(EffectLogger.log::<LogCtx>(level, "msg"), test_ctx());
729 assert_eq!(result, Ok(()));
730 }
731 }
732 }
733
734 mod no_tls_paths {
737 use super::*;
738 use ::id_effect::run_blocking;
739
740 #[test]
741 fn annotate_logs_without_tls_still_runs_inner() {
742 crate::test_clear_all_logger_tls();
743 let result = run_blocking(
744 annotate_logs("k", "v", id_effect::succeed::<i32, (), ()>(42)),
745 (),
746 );
747 assert_eq!(result, Ok(42));
748 }
749
750 #[test]
751 fn with_log_span_without_tls_still_runs_inner() {
752 crate::test_clear_all_logger_tls();
753 let result = run_blocking(
754 with_log_span("span", id_effect::succeed::<i32, (), ()>(99)),
755 (),
756 );
757 assert_eq!(result, Ok(99));
758 }
759 }
760
761 mod effect_logger_level_methods {
762 use super::*;
763
764 #[test]
765 fn trace_delegates_to_log_at_trace_level() {
766 init_subscriber();
767 assert_eq!(run_blocking(EffectLogger.trace::<()>("t"), ()), Ok(()));
768 }
769
770 #[test]
771 fn debug_delegates_to_log_at_debug_level() {
772 init_subscriber();
773 assert_eq!(run_blocking(EffectLogger.debug::<()>("d"), ()), Ok(()));
774 }
775
776 #[test]
777 fn info_delegates_to_log_at_info_level() {
778 init_subscriber();
779 assert_eq!(run_blocking(EffectLogger.info::<()>("i"), ()), Ok(()));
780 }
781
782 #[test]
783 fn warn_delegates_to_log_at_warn_level() {
784 init_subscriber();
785 assert_eq!(run_blocking(EffectLogger.warn::<()>("w"), ()), Ok(()));
786 }
787
788 #[test]
789 fn error_delegates_to_log_at_error_level() {
790 init_subscriber();
791 assert_eq!(run_blocking(EffectLogger.error::<()>("e"), ()), Ok(()));
792 }
793
794 #[test]
795 fn log_string_delegates_at_info_level() {
796 init_subscriber();
797 assert_eq!(
798 run_blocking(
799 EffectLogger.log_string::<()>(LogLevel::Info, "owned msg".to_string()),
800 (),
801 ),
802 Ok(()),
803 );
804 }
805
806 #[test]
807 fn info_accepts_formatted_string() {
808 init_subscriber();
809 let n = 42u32;
810 assert_eq!(
811 run_blocking(EffectLogger.info::<()>(format!("n={n}")), ()),
812 Ok(()),
813 );
814 }
815
816 #[test]
817 fn fatal_delegates_to_log_at_fatal_level() {
818 init_subscriber();
819 assert_eq!(run_blocking(EffectLogger.fatal::<()>("f"), ()), Ok(()));
820 }
821
822 #[test]
823 fn log_none_level_returns_ok_without_side_effects() {
824 init_subscriber();
825 assert_eq!(
826 run_blocking(EffectLogger.log::<()>(LogLevel::None, "silenced"), ()),
827 Ok(())
828 );
829 }
830 }
831
832 mod into_bind_extraction {
835 use super::*;
836
837 #[test]
838 fn extracts_logger_copy_from_context() {
839 let effect: ::id_effect::Effect<EffectLogger, EffectLoggerError, LogCtx> =
840 ::id_effect::Effect::new_async(move |r| {
841 Box::pin(async move { IntoBind::into_bind(EffectLogger, r).await })
842 });
843 let result = run_blocking(effect, test_ctx());
844 assert!(result.is_ok());
845 }
846
847 #[test]
848 fn extracted_logger_can_emit_log_via_run_blocking() {
849 init_subscriber();
850 let effect: ::id_effect::Effect<EffectLogger, EffectLoggerError, LogCtx> =
851 ::id_effect::Effect::new_async(move |r| {
852 Box::pin(async move { IntoBind::into_bind(EffectLogger, r).await })
853 });
854 let logger = run_blocking(effect, test_ctx()).expect("extraction is infallible");
855 assert_eq!(run_blocking(logger.info::<()>("extracted"), ()), Ok(()));
856 }
857 }
858
859 mod layer_effect_logger_fn {
862 use super::*;
863
864 #[test]
865 fn builds_without_error() {
866 let result = layer_effect_logger().build();
867 assert!(result.is_ok());
868 }
869
870 #[test]
871 fn produced_service_can_be_placed_in_context() {
872 let cell = layer_effect_logger().build().expect("infallible");
873 let ctx: LogCtx = Context::new(Cons(cell, Nil));
874 let result = run_blocking(EffectLogger.info::<LogCtx>("layer build ok"), ctx);
875 assert_eq!(result, Ok(()));
876 }
877 }
878
879 mod effect_logger_error {
882 use super::*;
883
884 #[test]
885 fn sink_variant_display_contains_message() {
886 let err = EffectLoggerError::Sink("oops".to_owned());
887 assert!(err.to_string().contains("oops"));
888 }
889
890 #[test]
891 fn sink_variant_display_has_prefix() {
892 let err = EffectLoggerError::Sink("x".to_owned());
893 assert!(err.to_string().starts_with("log sink error:"));
894 }
895
896 #[test]
897 fn sink_variant_implements_error_trait() {
898 let err: Box<dyn std::error::Error> = Box::new(EffectLoggerError::Sink("e".to_owned()));
899 assert!(err.to_string().contains("e"));
900 }
901
902 #[test]
903 fn two_equal_sink_errors_are_eq() {
904 assert_eq!(
905 EffectLoggerError::Sink("a".to_owned()),
906 EffectLoggerError::Sink("a".to_owned())
907 );
908 }
909
910 #[test]
911 fn two_different_sink_errors_are_ne() {
912 assert_ne!(
913 EffectLoggerError::Sink("a".to_owned()),
914 EffectLoggerError::Sink("b".to_owned())
915 );
916 }
917 }
918
919 mod wave5_full_logger {
922 use std::sync::{Arc, Mutex};
923
924 use ::id_effect::{Layer, run_blocking};
925
926 use crate::{
927 CompositeLogBackend, EffectLogger, EffectLoggerError, JsonLogBackend, LogBackend, LogLevel,
928 LogRecord, Logger, StructuredLogBackend, annotate_logs, with_log_span,
929 };
930
931 struct MsgCap(Arc<Mutex<Vec<String>>>);
932
933 impl LogBackend for MsgCap {
934 fn emit(&self, rec: &LogRecord<'_>) -> Result<(), EffectLoggerError> {
935 self.0.lock().expect("cap").push(rec.message.to_string());
936 Ok(())
937 }
938 }
939
940 fn setup_json() -> Arc<Mutex<Vec<u8>>> {
941 crate::test_clear_all_logger_tls();
942 let jb = JsonLogBackend::new(Vec::<u8>::new());
943 let buf = jb.writer_arc();
944 let comp = Arc::new(CompositeLogBackend::new());
945 comp.add(Arc::new(jb)).expect("add json");
946 crate::layer_log_metadata().build().expect("metadata layer");
947 crate::layer_composite_logger(comp)
948 .build()
949 .expect("composite layer");
950 buf
951 }
952
953 #[test]
954 fn logger_json_backend_produces_valid_json() {
955 let buf = setup_json();
956 run_blocking(
957 annotate_logs("k", "v", EffectLogger.info::<()>("hello")),
958 (),
959 )
960 .expect("log");
961 let bytes = buf.lock().expect("buf");
962 let line = std::str::from_utf8(bytes.as_slice()).expect("utf8");
963 let line = line.trim();
964 let v: serde_json::Value = serde_json::from_str(line).expect("valid json");
965 assert_eq!(v["level"], "INFO");
966 assert_eq!(v["message"], "hello");
967 assert_eq!(v["fields"]["k"], "v");
968 }
969
970 #[test]
971 fn logger_add_replaces_layer() {
972 crate::test_clear_all_logger_tls();
973 let a = Arc::new(Mutex::new(Vec::new()));
974 let b = Arc::new(Mutex::new(Vec::new()));
975 let comp = Arc::new(CompositeLogBackend::new());
976 comp.add(Arc::new(MsgCap(a.clone()))).unwrap();
977 comp.add(Arc::new(MsgCap(b.clone()))).unwrap();
978 crate::layer_composite_logger(comp.clone()).build().unwrap();
979 run_blocking(EffectLogger.info::<()>("m1"), ()).unwrap();
980 assert_eq!(*a.lock().unwrap(), vec!["m1".to_string()]);
981 assert_eq!(*b.lock().unwrap(), vec!["m1".to_string()]);
982
983 let c = Arc::new(Mutex::new(Vec::new()));
984 comp.replace(0, Arc::new(MsgCap(c.clone()))).unwrap();
985 a.lock().unwrap().clear();
986 b.lock().unwrap().clear();
987 run_blocking(EffectLogger.info::<()>("m2"), ()).unwrap();
988 assert!(a.lock().unwrap().is_empty());
989 assert_eq!(*b.lock().unwrap(), vec!["m2".to_string()]);
990 assert_eq!(*c.lock().unwrap(), vec!["m2".to_string()]);
991 }
992
993 #[test]
994 fn logger_fatal_is_highest_level() {
995 assert!(LogLevel::Fatal.severity() > LogLevel::Error.severity());
996 assert!(LogLevel::Trace.allows(LogLevel::Fatal));
997 assert!(!LogLevel::Fatal.allows(LogLevel::Error));
998 assert!(LogLevel::Fatal.allows(LogLevel::Fatal));
999 assert!(!LogLevel::None.allows(LogLevel::Info));
1000 }
1001
1002 #[test]
1003 fn annotate_logs_visible_in_structured_output() {
1004 crate::test_clear_all_logger_tls();
1005 let structured = StructuredLogBackend::new(Vec::<u8>::new());
1006 let buf = structured.writer_arc();
1007 let comp = Arc::new(CompositeLogBackend::new());
1008 comp.add(Arc::new(structured)).unwrap();
1009 crate::layer_log_metadata().build().unwrap();
1010 crate::layer_composite_logger(comp).build().unwrap();
1011 run_blocking(
1012 annotate_logs("trace_id", "abc", EffectLogger.info::<()>("done")),
1013 (),
1014 )
1015 .unwrap();
1016 let s = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
1017 assert!(
1018 s.contains("trace_id") && s.contains("abc"),
1019 "expected annotation in output: {s:?}"
1020 );
1021 }
1022
1023 #[test]
1024 fn with_log_span_visible_in_json() {
1025 let buf = setup_json();
1026 run_blocking(
1027 with_log_span("outer", EffectLogger.warn::<()>("inside")),
1028 (),
1029 )
1030 .unwrap();
1031 let bytes = buf.lock().unwrap().clone();
1032 let line = std::str::from_utf8(&bytes).unwrap().trim();
1033 let v: serde_json::Value = serde_json::from_str(line).unwrap();
1034 assert_eq!(v["spans"], serde_json::json!(["outer"]));
1035 }
1036 }
1037}