dynamic_config/log.rs
1//! Diagnostics from the places with nobody to return an error to.
2//!
3//! A file watcher on its own thread, a remote watch loop, a cache that could
4//! not be written, a start that fell back to yesterday's configuration: each
5//! reports and carries on. Where the report lands is layered, most specific
6//! wins:
7//!
8//! 1. **The `tracing` feature**, when compiled in, takes everything: the
9//! lines become events under the `dynamic_config` target and the layers
10//! below never run. Filtering is the subscriber's business.
11//! 2. **An installed sink** ([`set_log_sink`]) receives every line that
12//! passes the level ([`set_log_level`]). This is the runtime path — it
13//! is how the language bindings hand these lines to `logging` and to a
14//! JavaScript callback, from a wheel that cannot flip a cargo feature.
15//! 3. **The `log` feature**, when compiled in, forwards to the `log` crate's
16//! global logger.
17//! 4. **stderr**, prefixed `[dynamic-config]` — the default, unchanged since
18//! 0.1: a library must not choose a logging framework for its users, and
19//! silence would hide a watcher that is failing every reload.
20
21use core::sync::atomic::{AtomicU8, Ordering};
22use std::sync::Arc;
23
24use arc_swap::ArcSwapOption;
25
26/// How loud the engine's own diagnostics are, for every path except a
27/// compiled-in `tracing` subscriber (which does its own filtering).
28///
29/// Ordered: a level admits itself and everything louder, so
30/// [`LogLevel::Info`] — the default, matching what the engine has always
31/// printed — admits warnings too, and [`LogLevel::Off`] admits nothing.
32#[non_exhaustive]
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34#[repr(u8)]
35pub enum LogLevel {
36 /// Nothing at all, the sink included.
37 Off = 0,
38 /// Only what went wrong: failed reloads, watcher errors, a cache that
39 /// could not be written, a start that fell back to the last known good
40 /// configuration.
41 Warn = 1,
42 /// The above, plus one line per successful reload. The default.
43 Info = 2,
44}
45
46static LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Info as u8);
47
48/// Where a diagnostic line goes when the `tracing` feature is not compiled
49/// in: the level it was emitted at, and the formatted line without the
50/// `[dynamic-config]` prefix (the sink knows who it installed).
51pub type LogSink = dyn Fn(LogLevel, &str) + Send + Sync;
52
53static SINK: ArcSwapOption<Box<LogSink>> = ArcSwapOption::const_empty();
54
55/// Sets how loud the engine's diagnostics are. Process-wide, effective
56/// immediately, cheap enough to call per test.
57///
58/// Under a compiled-in `tracing` subscriber this is a no-op: events are
59/// always emitted and the subscriber filters.
60pub fn set_log_level(level: LogLevel) {
61 LEVEL.store(level as u8, Ordering::Relaxed);
62}
63
64/// Routes every diagnostic line that passes the level to `sink`, instead
65/// of stderr or the `log` crate.
66///
67/// The contract, and it is load-bearing:
68///
69/// - **It is called on engine threads** — the watcher, a remote poll loop,
70/// whatever thread called `reload()`. It must not block: a sink that
71/// waits stalls reloads.
72/// - **It must not call back into the engine.** Some lines are emitted
73/// mid-transition; re-entrancy is not promised anywhere.
74/// - One sink per process. Installing a second replaces the first;
75/// [`clear_log_sink`] restores the default.
76pub fn set_log_sink(sink: impl Fn(LogLevel, &str) + Send + Sync + 'static) {
77 SINK.store(Some(Arc::new(Box::new(sink))));
78}
79
80/// Removes an installed sink: lines fall back to the `log` crate when that
81/// feature is compiled in, and to stderr otherwise.
82pub fn clear_log_sink() {
83 SINK.store(None);
84}
85
86/// The runtime dispatch, shared by both macros' non-`tracing` arms.
87///
88/// Wait-free on the hot path: one atomic load for the level and one
89/// arc-swap load for the sink, and the line is not even formatted when the
90/// level refuses it (the macros pass `format_args!` through).
91///
92/// Compiled out under `tracing`, whose macros never call it — the sink
93/// and level still exist there as public API, inert by documented design.
94#[cfg(not(feature = "tracing"))]
95pub(crate) fn emit(level: LogLevel, line: core::fmt::Arguments<'_>) {
96 if (level as u8) > LEVEL.load(Ordering::Relaxed) {
97 return;
98 }
99
100 if let Some(sink) = SINK.load_full() {
101 sink(level, &line.to_string());
102
103 return;
104 }
105
106 #[cfg(feature = "log")]
107 {
108 let mapped = match level {
109 LogLevel::Warn => ::log::Level::Warn,
110 _ => ::log::Level::Info,
111 };
112 ::log::log!(target: "dynamic_config", mapped, "{line}");
113 }
114
115 #[cfg(not(feature = "log"))]
116 {
117 ::std::eprintln!("[dynamic-config] {line}");
118 }
119}
120
121#[cfg(feature = "tracing")]
122macro_rules! info {
123 ($($arg:tt)*) => { ::tracing::info!(target: "dynamic_config", $($arg)*) };
124}
125
126#[cfg(not(feature = "tracing"))]
127macro_rules! info {
128 ($($arg:tt)*) => {
129 crate::log::emit(crate::log::LogLevel::Info, ::std::format_args!($($arg)*))
130 };
131}
132
133#[cfg(feature = "tracing")]
134macro_rules! warning {
135 ($($arg:tt)*) => { ::tracing::warn!(target: "dynamic_config", $($arg)*) };
136}
137
138#[cfg(not(feature = "tracing"))]
139macro_rules! warning {
140 ($($arg:tt)*) => {
141 crate::log::emit(crate::log::LogLevel::Warn, ::std::format_args!($($arg)*))
142 };
143}
144
145pub(crate) use {info, warning};