Skip to main content

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};