Skip to main content

krabka_telemetry/
log_levels.rs

1//! Runtime control of the stdout log filter, which is what Kafka's
2//! `BROKER_LOGGER` config resource alters.
3//!
4//! A JVM broker keeps a live log4j2 `LoggerContext`. `kafka-configs
5//! --entity-type broker-loggers --describe` reads every logger out of it, and
6//! `--alter` writes one logger's level back into it. Neither call touches
7//! cluster metadata: the change lives in the node that served the request and
8//! is gone when that node restarts. This module is the same thing for
9//! `tracing`.
10//!
11//! ## The model
12//!
13//! [`LogLevelController`] owns one editable [`EnvFilter`] behind a lock and
14//! the *model* it was rendered from: a root level and a per-target level map.
15//! Every level change mutates the model, re-renders it into a fresh
16//! `EnvFilter`, swaps it in, and rebuilds the callsite interest cache so a
17//! level *raise* takes effect at callsites `tracing` had already cached as
18//! disabled. Mutation, render and swap happen under one lock, so a describe
19//! never reports a level the live filter does not yet carry, and two
20//! concurrent alterations cannot install their filters in the opposite order
21//! from their model edits.
22//!
23//! The directives the model cannot express — a span or field directive such
24//! as `[my_span{field=1}]=debug` — go into a second `EnvFilter` that is built
25//! once and never replaced. `EnvFilter` keeps the state of the spans a
26//! dynamic directive matches inside itself, and that state cannot be
27//! transplanted into a new filter, so a rebuild would silence the events
28//! inside a span that was already open. A callsite is enabled when either
29//! filter enables it, which is exactly how one `EnvFilter` combines its
30//! dynamic and static directives.
31//!
32//! [`LogLevelFilter`] is the [`Filter`] side of the same state. It delegates
33//! to the carried filter and to whichever editable `EnvFilter` is current,
34//! and records the target of every callsite it is asked about, which is how
35//! the controller learns that a logger exists. `tracing` offers a new
36//! callsite to every registered subscriber, so the recorded set covers the
37//! whole process and not only the layer this filter is attached to. Together
38//! with the targets the starting spec names, it answers to log4j2's
39//! `LoggerContext.getLoggers()` plus the loggers a `log4j2.properties`
40//! declares: a name stays a logger once it is known, whatever its level is
41//! later set to or cleared back to.
42//!
43//! ## Levels
44//!
45//! Kafka's `LogLevelConfig.VALID_LOG_LEVELS` is `FATAL, ERROR, WARN, INFO,
46//! DEBUG, TRACE`. `tracing` has no `FATAL`, so [`LogLevel::Fatal`] renders as
47//! the `off` directive: no krabka event is more severe than `ERROR`, so a
48//! target held at `FATAL` emits nothing, which is exactly what log4j2 does
49//! with the same setting. The mapping round-trips, so a `FATAL` an operator
50//! writes is the `FATAL` the describe reads back.
51
52use {
53    std::{
54        collections::{BTreeMap, BTreeSet},
55        sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard},
56    },
57    tracing::{Metadata, span},
58    tracing_core::{Interest, callsite},
59    tracing_subscriber::{
60        EnvFilter,
61        filter::LevelFilter,
62        layer::{Context, Filter},
63    },
64};
65
66#[cfg(test)]
67mod tests;
68
69/// The name log4j2 and Kafka both give the level every unmatched target
70/// falls back to. `kafka-configs` prints it beside the real loggers.
71pub const ROOT_LOGGER: &str = "root";
72
73/// The level names a `BROKER_LOGGER` value may carry, in the order Kafka
74/// renders them into its rejection message.
75///
76/// `RuntimeLoggerManager` builds that
77/// list as `String.join(", ", sorted(LogLevelConfig.VALID_LOG_LEVELS))`, so a
78/// caller joins these with `", "` and not with a bare comma.
79pub const VALID_LOG_LEVELS: [&str; 6] = ["DEBUG", "ERROR", "FATAL", "INFO", "TRACE", "WARN"];
80
81/// One level in Kafka's `BROKER_LOGGER` vocabulary.
82///
83/// The order is the log4j2 one, least verbose first, so `Trace > Info`.
84///
85/// `kafka_name` is the uppercase name Kafka puts on the wire, and
86/// `from_kafka_name` parses a `BROKER_LOGGER` config value. Kafka tests the
87/// value against a `Set<String>` of uppercase names, so a lowercase `info` is
88/// rejected there and is rejected here.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, krabka_macros::EnumStr)]
90#[enum_str(case = "UPPERCASE", as_str = kafka_name, parse = from_kafka_name)]
91pub enum LogLevel {
92    /// Nothing krabka emits reaches this level, so it silences the target.
93    Fatal,
94    /// `ERROR` and above.
95    Error,
96    /// `WARN` and above.
97    Warn,
98    /// `INFO` and above.
99    Info,
100    /// `DEBUG` and above.
101    Debug,
102    /// Everything.
103    Trace,
104}
105
106impl LogLevel {
107    /// The `EnvFilter` level this renders as.
108    const fn directive(self) -> &'static str {
109        match self {
110            Self::Fatal => "off",
111            Self::Error => "error",
112            Self::Warn => "warn",
113            Self::Info => "info",
114            Self::Debug => "debug",
115            Self::Trace => "trace",
116        }
117    }
118
119    /// Parse one `EnvFilter` level.
120    ///
121    /// `EnvFilter` hands the level of a directive to `LevelFilter`'s
122    /// `FromStr`, which takes a case-insensitive name *or* a number from `0`
123    /// to `5`, so `foo=4` is `foo=debug` and a bare `3` is the root at
124    /// `INFO`. The empty level of a `foo=` directive never reaches it: the
125    /// directive parser reads that as `TRACE` itself, which is not what
126    /// `FromStr` would say. The name `EnvFilter` gives the level Kafka calls
127    /// `FATAL` is `off`.
128    fn from_directive(value: &str) -> Option<Self> {
129        if value.is_empty() {
130            return Some(Self::Trace);
131        }
132        if let Ok(number) = value.parse::<usize>() {
133            return match number {
134                0 => Some(Self::Fatal),
135                1 => Some(Self::Error),
136                2 => Some(Self::Warn),
137                3 => Some(Self::Info),
138                4 => Some(Self::Debug),
139                5 => Some(Self::Trace),
140                _ => None,
141            };
142        }
143        match value.to_ascii_lowercase().as_str() {
144            "off" => Some(Self::Fatal),
145            "error" => Some(Self::Error),
146            "warn" => Some(Self::Warn),
147            "info" => Some(Self::Info),
148            "debug" => Some(Self::Debug),
149            "trace" => Some(Self::Trace),
150            _ => None,
151        }
152    }
153}
154
155/// The filter as a set of parts that can be edited and rendered back.
156#[derive(Debug, Clone, PartialEq, Eq)]
157struct Model {
158    /// The level a target with no matching directive gets. An `EnvFilter`
159    /// with no bare-level directive disables everything it does not match,
160    /// which is [`LogLevel::Fatal`].
161    root: LogLevel,
162    /// Explicit per-target levels. `EnvFilter` matches a target directive by
163    /// string prefix and the longest match wins, so this map is read the same
164    /// way. A target drops out of here when its level is cleared; it stays a
165    /// logger, because [`Shared::known`] remembers the name.
166    targets: BTreeMap<String, LogLevel>,
167}
168
169/// An `EnvFilter` spec split into the model and the directives it only carries.
170struct ParsedSpec {
171    model: Model,
172    carried: Vec<String>,
173}
174
175impl Model {
176    /// Split an `EnvFilter` spec into the parts this module edits and the
177    /// ones it only carries.
178    ///
179    /// A carried directive is a span or field directive, which is not a
180    /// logger, or text `EnvFilter` itself rejects, which it drops with a
181    /// warning either way.
182    fn parse(spec: &str) -> ParsedSpec {
183        let mut model = Self {
184            root: LogLevel::Fatal,
185            targets: BTreeMap::new(),
186        };
187        let mut carried = Vec::new();
188        for directive in spec.split(',').map(str::trim).filter(|d| !d.is_empty()) {
189            model.absorb(directive, &mut carried);
190        }
191        ParsedSpec { model, carried }
192    }
193
194    /// Fold one directive into the model, or hand it to `carried` verbatim.
195    fn absorb(&mut self, directive: &str, carried: &mut Vec<String>) {
196        // A span directive (`[name{field}]=level`) or a field directive is
197        // not a logger, so the model only carries it.
198        if directive.contains('[') {
199            carried.push(directive.to_owned());
200            return;
201        }
202        match directive.split_once('=') {
203            Some((target, level)) => match LogLevel::from_directive(level) {
204                Some(level) if !target.is_empty() => {
205                    self.targets.insert(target.to_owned(), level);
206                }
207                _ => carried.push(directive.to_owned()),
208            },
209            // A bare word is a level when it names one and a target held at
210            // `TRACE` otherwise, which is how `EnvFilter` reads it.
211            None => match LogLevel::from_directive(directive) {
212                Some(level) => self.root = level,
213                None => {
214                    self.targets.insert(directive.to_owned(), LogLevel::Trace);
215                }
216            },
217        }
218    }
219
220    /// Render the model back into an `EnvFilter` spec.
221    fn render(&self) -> String {
222        let mut parts = Vec::with_capacity(self.targets.len() + 1);
223        parts.push(self.root.directive().to_owned());
224        parts.extend(
225            self.targets
226                .iter()
227                .map(|(target, level)| format!("{target}={}", level.directive())),
228        );
229        parts.join(",")
230    }
231
232    /// The level `target` resolves to: the longest directive that prefixes
233    /// it, or the root level.
234    fn effective(&self, target: &str) -> LogLevel {
235        self.targets
236            .iter()
237            .filter(|(name, _)| target.starts_with(name.as_str()))
238            .max_by_key(|(name, _)| name.len())
239            .map_or(self.root, |(_, level)| *level)
240    }
241}
242
243/// The state a [`LogLevelController`] and its [`LogLevelFilter`]s share.
244#[derive(Debug)]
245struct Shared {
246    /// The editable filter. Replaced wholesale on a level change.
247    filter: RwLock<EnvFilter>,
248    /// The span and field directives the starting spec carried, in a filter
249    /// of their own that no level change replaces. `EnvFilter` holds the
250    /// state of the spans its dynamic directives match, and a replacement
251    /// would start with none of it, so the events inside a span that was
252    /// already open would stop matching.
253    carried: EnvFilter,
254    /// The parts [`Shared::filter`] was rendered from.
255    model: RwLock<Model>,
256    /// Every logger name this node has: the targets the starting spec named
257    /// and the targets `tracing` has offered a callsite for.
258    known: RwLock<BTreeSet<String>>,
259}
260
261/// Read a lock, taking the guard even when a panic poisoned it.
262///
263/// A poisoned lock must not take the log filter down with it: the state
264/// behind it is a filter and a name map, and a torn write leaves neither in a
265/// shape that can harm a reader.
266fn read<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
267    lock.read().unwrap_or_else(PoisonError::into_inner)
268}
269
270/// Write a lock, taking the guard even when a panic poisoned it. See
271/// [`read`].
272fn write<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
273    lock.write().unwrap_or_else(PoisonError::into_inner)
274}
275
276/// Record `name` as a logger this node has.
277///
278/// The read comes first because the common case is a name already known, and
279/// a callsite registration must not queue behind a writer to find that out.
280fn remember(shared: &Shared, name: &str) {
281    if !read(&shared.known).contains(name) {
282        write(&shared.known).insert(name.to_owned());
283    }
284}
285
286/// The handle that reads and retargets the live log filter.
287///
288/// Cloning shares one filter: every clone reads and writes the same state,
289/// which is what lets the broker hold one in its config while the installed
290/// subscriber holds the [`LogLevelFilter`] side.
291#[derive(Clone, Debug)]
292pub struct LogLevelController {
293    shared: Arc<Shared>,
294}
295
296/// The [`Filter`] side of a [`LogLevelController`].
297///
298/// Attach it to the layer whose verbosity `BROKER_LOGGER` should control:
299/// `layer.with_filter(filter)`.
300#[derive(Debug)]
301pub struct LogLevelFilter {
302    shared: Arc<Shared>,
303}
304
305impl LogLevelController {
306    /// Build a controller over the `EnvFilter` spec `spec`, and the filter it
307    /// drives.
308    ///
309    /// `spec` is the ordinary `RUST_LOG` syntax. Its target directives seed
310    /// the logger list, the way the loggers named in a `log4j2.properties`
311    /// are listed before anything has logged through them.
312    #[must_use]
313    pub fn new(spec: &str) -> (Self, LogLevelFilter) {
314        let ParsedSpec { model, carried } = Model::parse(spec);
315        let known = model.targets.keys().cloned().collect();
316        let shared = Arc::new(Shared {
317            filter: RwLock::new(EnvFilter::new(model.render())),
318            // The builder and not `EnvFilter::new`: an empty carried spec
319            // must enable nothing, and `EnvFilter::new` would read it as the
320            // default `ERROR` directive and enable everything at that level.
321            carried: EnvFilter::builder().parse_lossy(carried.join(",")),
322            model: RwLock::new(model),
323            known: RwLock::new(known),
324        });
325        (
326            Self {
327                shared: Arc::clone(&shared),
328            },
329            LogLevelFilter { shared },
330        )
331    }
332
333    /// Another filter over the same state, for a second layer.
334    #[must_use]
335    pub fn filter(&self) -> LogLevelFilter {
336        LogLevelFilter {
337            shared: Arc::clone(&self.shared),
338        }
339    }
340
341    /// Every logger this node knows, with its effective level.
342    ///
343    /// That is every target a callsite has been registered under, every
344    /// target the starting spec named, and [`ROOT_LOGGER`].
345    #[must_use]
346    pub fn loggers(&self) -> BTreeMap<String, LogLevel> {
347        let model = read(&self.shared.model);
348        let mut out: BTreeMap<String, LogLevel> = read(&self.shared.known)
349            .iter()
350            .map(|target| (target.clone(), model.effective(target)))
351            .collect();
352        out.insert(ROOT_LOGGER.to_owned(), model.root);
353        out
354    }
355
356    /// The effective level of one logger, or `None` when no such logger
357    /// exists. Mirrors log4j2's `loggerExists` plus `getLevel`.
358    #[must_use]
359    pub fn level(&self, name: &str) -> Option<LogLevel> {
360        let model = read(&self.shared.model);
361        if name == ROOT_LOGGER {
362            Some(model.root)
363        } else {
364            read(&self.shared.known)
365                .contains(name)
366                .then(|| model.effective(name))
367        }
368    }
369
370    /// Whether a logger by this name exists on this node.
371    #[must_use]
372    pub fn contains(&self, name: &str) -> bool { self.level(name).is_some() }
373
374    /// Pin `name` to `level`, in this process only.
375    ///
376    /// [`ROOT_LOGGER`] sets the level every unmatched target falls back to.
377    /// Any other name becomes a target directive, which also covers the
378    /// targets below it: `krabka_broker` at `DEBUG` puts
379    /// `krabka_broker::handlers::produce` at `DEBUG` too.
380    pub fn set_level(&self, name: &str, level: LogLevel) {
381        if name != ROOT_LOGGER {
382            remember(&self.shared, name);
383        }
384        self.edit(|model| {
385            if name == ROOT_LOGGER {
386                model.root = level;
387            } else {
388                model.targets.insert(name.to_owned(), level);
389            }
390        });
391    }
392
393    /// Drop `name`'s own level so it inherits again, and report whether it
394    /// had one.
395    ///
396    /// [`ROOT_LOGGER`] never has a level of its own to drop — it *is* the
397    /// level everything else inherits — so it always reports `false`. Kafka
398    /// refuses a DELETE of the root logger for that reason.
399    #[must_use]
400    pub fn clear_level(&self, name: &str) -> bool {
401        if name == ROOT_LOGGER {
402            return false;
403        }
404        let mut removed = false;
405        self.edit(|model| removed = model.targets.remove(name).is_some());
406        removed
407    }
408
409    /// Apply `mutate` to the model, re-render the filter, and make `tracing`
410    /// re-ask every callsite.
411    ///
412    /// The rebuild is what makes a *raise* work: `tracing` caches a callsite
413    /// the old filter disabled and never consults the filter for it again
414    /// until the cache is dropped.
415    fn edit(&self, mutate: impl FnOnce(&mut Model)) {
416        {
417            // The model lock is held across the install, so one alteration
418            // at a time mutates, renders and swaps. Two that overlapped
419            // could otherwise render `A` and `A+B` and install them in the
420            // opposite order, leaving the filter at `A` while every describe
421            // reported `A+B`.
422            let mut model = write(&self.shared.model);
423            mutate(&mut model);
424            *write(&self.shared.filter) = EnvFilter::new(model.render());
425        }
426        // Outside the locks: the rebuild calls back into
427        // `LogLevelFilter::callsite_enabled`, which reads them.
428        callsite::rebuild_interest_cache();
429    }
430}
431
432/// The interest of a callsite either filter may enable.
433///
434/// `tracing` caches an `always` and never asks again, which is right only
435/// when a filter admits the callsite unconditionally; it drops a `never`
436/// callsite outright, which is right only when neither filter can admit it.
437/// Everything in between is `sometimes`, and [`Filter::enabled`] settles it
438/// per event.
439fn either(a: &Interest, b: &Interest) -> Interest {
440    if a.is_always() || b.is_always() {
441        Interest::always()
442    } else if a.is_never() && b.is_never() {
443        Interest::never()
444    } else {
445        Interest::sometimes()
446    }
447}
448
449impl<S> Filter<S> for LogLevelFilter {
450    fn enabled(&self, meta: &Metadata<'_>, cx: &Context<'_, S>) -> bool {
451        Filter::<S>::enabled(&self.shared.carried, meta, cx)
452            || Filter::<S>::enabled(&*read(&self.shared.filter), meta, cx)
453    }
454
455    fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
456        remember(&self.shared, meta.target());
457        either(
458            &Filter::<S>::callsite_enabled(&self.shared.carried, meta),
459            &Filter::<S>::callsite_enabled(&*read(&self.shared.filter), meta),
460        )
461    }
462
463    fn max_level_hint(&self) -> Option<LevelFilter> {
464        // `None` is `EnvFilter` declining to bound the level, so it wins
465        // over any bound the other filter offers.
466        let carried = Filter::<S>::max_level_hint(&self.shared.carried)?;
467        let editable = Filter::<S>::max_level_hint(&*read(&self.shared.filter))?;
468        Some(carried.max(editable))
469    }
470
471    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, cx: Context<'_, S>) {
472        Filter::<S>::on_new_span(&self.shared.carried, attrs, id, cx.clone());
473        Filter::<S>::on_new_span(&*read(&self.shared.filter), attrs, id, cx);
474    }
475
476    fn on_record(&self, id: &span::Id, values: &span::Record<'_>, cx: Context<'_, S>) {
477        Filter::<S>::on_record(&self.shared.carried, id, values, cx.clone());
478        Filter::<S>::on_record(&*read(&self.shared.filter), id, values, cx);
479    }
480
481    fn on_enter(&self, id: &span::Id, cx: Context<'_, S>) {
482        Filter::<S>::on_enter(&self.shared.carried, id, cx.clone());
483        Filter::<S>::on_enter(&*read(&self.shared.filter), id, cx);
484    }
485
486    fn on_exit(&self, id: &span::Id, cx: Context<'_, S>) {
487        Filter::<S>::on_exit(&self.shared.carried, id, cx.clone());
488        Filter::<S>::on_exit(&*read(&self.shared.filter), id, cx);
489    }
490
491    fn on_close(&self, id: span::Id, cx: Context<'_, S>) {
492        Filter::<S>::on_close(&self.shared.carried, id.clone(), cx.clone());
493        Filter::<S>::on_close(&*read(&self.shared.filter), id, cx);
494    }
495}