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}