1use std::error::Error;
2use std::fmt::{Display, Formatter, Result as FmtResult};
3use std::ptr;
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
6use std::time::Instant;
7
8use crate::tracing::{TraceContext, current_trace_context};
9
10static LOG_EMITTER_MARKER: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());
15static LOG_EMITTER_TOKEN: () = ();
16static GLOBAL_LOG_EMITTER: OnceLock<Box<dyn LogEmitter>> = OnceLock::new();
17static MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(MIN_LOG_LEVEL_UNSET);
18
19const MIN_LOG_LEVEL_UNSET: u8 = 0;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
22pub enum LogLevel {
23 Trace,
24 Debug,
25 Info,
26 Warn,
27 Error,
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub enum LogValue {
32 String(String),
33 I64(i64),
34 U64(u64),
35 F64(f64),
36 Bool(bool),
37}
38
39#[derive(Clone, Debug, PartialEq)]
40pub struct LogEntry {
41 pub timestamp: Instant,
42 pub level: LogLevel,
43 pub target: &'static str,
44 pub message: String,
45 pub trace_context: Option<TraceContext>,
46 pub fields: Vec<(String, LogValue)>,
47}
48
49impl LogEntry {
50 #[must_use]
51 pub const fn trace_id(&self) -> Option<u128> {
52 match self.trace_context {
53 Some(context) => Some(context.trace_id()),
54 None => None,
55 }
56 }
57
58 #[must_use]
59 pub const fn span_id(&self) -> Option<u64> {
60 match self.trace_context {
61 Some(context) => Some(context.span_id()),
62 None => None,
63 }
64 }
65}
66
67pub trait LogEmitter: std::fmt::Debug + Send + Sync + 'static {
68 fn emit(&self, entry: &LogEntry);
69}
70
71#[derive(Debug, Clone, Copy, Default)]
72pub struct NoopEmitter;
73
74impl LogEmitter for NoopEmitter {
75 fn emit(&self, entry: &LogEntry) {
76 let _ = entry;
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum LogEmitterInstallError {
82 AlreadyInstalled,
83}
84
85impl Display for LogEmitterInstallError {
86 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
87 match self {
88 Self::AlreadyInstalled => {
89 formatter.write_str("global log emitter is already installed")
90 }
91 }
92 }
93}
94
95impl Error for LogEmitterInstallError {}
96
97#[derive(Debug, Clone, PartialEq)]
98pub struct LogEntryBuilder {
99 level: LogLevel,
100 target: &'static str,
101 message: String,
102 fields: Vec<(String, LogValue)>,
103}
104
105impl LogEntryBuilder {
106 #[must_use]
107 pub fn new(level: LogLevel, target: &'static str, message: impl Into<String>) -> Self {
108 Self {
109 level,
110 target,
111 message: message.into(),
112 fields: Vec::new(),
113 }
114 }
115
116 #[must_use]
117 pub fn field(mut self, key: impl Into<String>, value: LogValue) -> Self {
118 self.fields.push((key.into(), value));
119 self
120 }
121
122 #[must_use]
123 pub fn build(self) -> LogEntry {
124 LogEntry {
125 timestamp: Instant::now(),
126 level: self.level,
127 target: self.target,
128 message: self.message,
129 trace_context: current_trace_context(),
130 fields: self.fields,
131 }
132 }
133}
134
135pub fn install_log_emitter<Emitter>(emitter: Emitter) -> Result<(), LogEmitterInstallError>
139where
140 Emitter: LogEmitter,
141{
142 install_boxed_log_emitter(Box::new(emitter))
143}
144
145pub fn install_boxed_log_emitter(
149 emitter: Box<dyn LogEmitter>,
150) -> Result<(), LogEmitterInstallError> {
151 match GLOBAL_LOG_EMITTER.set(emitter) {
152 Ok(()) => {
153 LOG_EMITTER_MARKER.store(
156 ptr::addr_of!(LOG_EMITTER_TOKEN).cast_mut(),
157 Ordering::Release,
158 );
159 Ok(())
160 }
161 Err(emitter) => {
162 drop(emitter);
163 Err(LogEmitterInstallError::AlreadyInstalled)
164 }
165 }
166}
167
168#[must_use]
169pub fn log_emitter_enabled() -> bool {
170 !LOG_EMITTER_MARKER.load(Ordering::Acquire).is_null()
171}
172
173#[must_use]
174pub fn global_log_emitter() -> Option<&'static dyn LogEmitter> {
175 if log_emitter_enabled() {
176 GLOBAL_LOG_EMITTER.get().map(Box::as_ref)
177 } else {
178 None
179 }
180}
181
182pub fn set_min_log_level(level: LogLevel) {
183 MIN_LOG_LEVEL.store(log_level_filter_value(level), Ordering::Release);
184}
185
186pub fn clear_min_log_level() {
187 MIN_LOG_LEVEL.store(MIN_LOG_LEVEL_UNSET, Ordering::Release);
188}
189
190#[must_use]
191pub fn min_log_level() -> Option<LogLevel> {
192 log_level_from_filter_value(MIN_LOG_LEVEL.load(Ordering::Acquire))
193}
194
195#[must_use]
196pub fn level_enabled(level: LogLevel) -> bool {
197 let minimum = MIN_LOG_LEVEL.load(Ordering::Acquire);
198 minimum == MIN_LOG_LEVEL_UNSET || log_level_filter_value(level) >= minimum
199}
200
201#[must_use]
202pub fn log_enabled(level: LogLevel) -> bool {
203 log_emitter_enabled() && level_enabled(level)
204}
205
206pub fn emit_entry(entry: &LogEntry) {
207 if let Some(emitter) = enabled_log_emitter(entry.level) {
208 emitter.emit(entry);
209 }
210}
211
212pub fn log(level: LogLevel, target: &'static str, message: impl Into<String>) {
213 log_with_fields(level, target, message, |builder| builder);
214}
215
216pub fn log_with_fields<Fields>(
217 level: LogLevel,
218 target: &'static str,
219 message: impl Into<String>,
220 fields: Fields,
221) where
222 Fields: FnOnce(LogEntryBuilder) -> LogEntryBuilder,
223{
224 let Some(emitter) = enabled_log_emitter(level) else {
225 return;
226 };
227
228 let entry = fields(LogEntryBuilder::new(level, target, message)).build();
229 emitter.emit(&entry);
230}
231
232fn enabled_log_emitter(level: LogLevel) -> Option<&'static dyn LogEmitter> {
233 if !log_emitter_enabled() || !level_enabled(level) {
234 return None;
235 }
236
237 GLOBAL_LOG_EMITTER.get().map(Box::as_ref)
238}
239
240const fn log_level_filter_value(level: LogLevel) -> u8 {
241 match level {
242 LogLevel::Trace => 1,
243 LogLevel::Debug => 2,
244 LogLevel::Info => 3,
245 LogLevel::Warn => 4,
246 LogLevel::Error => 5,
247 }
248}
249
250const fn log_level_from_filter_value(value: u8) -> Option<LogLevel> {
251 match value {
252 MIN_LOG_LEVEL_UNSET => None,
253 1 => Some(LogLevel::Trace),
254 2 => Some(LogLevel::Debug),
255 3 => Some(LogLevel::Info),
256 4 => Some(LogLevel::Warn),
257 5 => Some(LogLevel::Error),
258 _ => None,
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use std::cell::Cell;
265
266 use std::sync::{Arc, Mutex, PoisonError};
267
268 use super::{
269 LogEmitter, LogEntry, LogEntryBuilder, LogLevel, LogValue, NoopEmitter,
270 clear_min_log_level, install_log_emitter, level_enabled, log, min_log_level,
271 set_min_log_level,
272 };
273 use crate::tracing::{SpanGuard, TraceContext};
274
275 #[test]
276 fn log_levels_are_ordered_by_severity() {
277 assert!(LogLevel::Trace < LogLevel::Debug);
278 assert!(LogLevel::Debug < LogLevel::Info);
279 assert!(LogLevel::Info < LogLevel::Warn);
280 assert!(LogLevel::Warn < LogLevel::Error);
281 }
282
283 #[test]
284 fn public_logging_types_are_debug() {
285 fn assert_debug<T: std::fmt::Debug>() {}
286
287 assert_debug::<super::LogLevel>();
288 assert_debug::<super::LogValue>();
289 assert_debug::<super::LogEntry>();
290 assert_debug::<super::LogEntryBuilder>();
291 assert_debug::<super::NoopEmitter>();
292 assert_debug::<super::LogEmitterInstallError>();
293 }
294
295 #[test]
296 fn builder_constructs_structured_entries() {
297 let entry = LogEntryBuilder::new(LogLevel::Info, module_path!(), "user login")
298 .field("user_id", LogValue::String("alice".to_owned()))
299 .field("attempt", LogValue::I64(-1))
300 .field("count", LogValue::U64(2))
301 .field("ratio", LogValue::F64(0.5))
302 .field("success", LogValue::Bool(true))
303 .build();
304
305 assert_eq!(entry.level, LogLevel::Info);
306 assert_eq!(entry.target, module_path!());
307 assert_eq!(entry.message, "user login");
308 assert_eq!(entry.trace_context, None);
309 assert_eq!(entry.fields.len(), 5);
310 assert_eq!(
311 entry.fields[0],
312 ("user_id".to_owned(), LogValue::String("alice".to_owned()))
313 );
314 assert_eq!(entry.fields[1], ("attempt".to_owned(), LogValue::I64(-1)));
315 assert_eq!(entry.fields[2], ("count".to_owned(), LogValue::U64(2)));
316 assert!(
317 matches!(entry.fields[3], (ref key, LogValue::F64(value)) if key == "ratio" && (value - 0.5).abs() < f64::EPSILON)
318 );
319 assert_eq!(
320 entry.fields[4],
321 ("success".to_owned(), LogValue::Bool(true))
322 );
323 }
324
325 #[test]
326 fn builder_captures_active_trace_context_at_build_time() {
327 let builder = LogEntryBuilder::new(LogLevel::Debug, module_path!(), "inside span");
328 let guard = SpanGuard::start_conversation("conversation");
329
330 let entry = builder.build();
331
332 assert_eq!(entry.trace_context, Some(guard.context()));
333 assert_eq!(entry.trace_id(), Some(guard.context().trace_id()));
334 assert_eq!(entry.span_id(), Some(guard.context().span_id()));
335 }
336
337 #[test]
338 fn builder_records_no_context_after_span_leaves_scope() {
339 {
340 let guard = SpanGuard::start_conversation("conversation");
341 let entry = LogEntryBuilder::new(LogLevel::Info, module_path!(), "inside span").build();
342 assert_eq!(entry.trace_context, Some(guard.context()));
343 }
344
345 let entry = LogEntryBuilder::new(LogLevel::Info, module_path!(), "outside span").build();
346 assert_eq!(entry.trace_context, None);
347 assert_eq!(entry.trace_id(), None);
348 assert_eq!(entry.span_id(), None);
349 }
350
351 #[test]
352 fn noop_emitter_discards_entries() {
353 let emitter = NoopEmitter;
354 let entry = LogEntryBuilder::new(LogLevel::Warn, module_path!(), "discarded").build();
355
356 emitter.emit(&entry);
357 }
358
359 #[test]
360 fn minimum_level_filter_discards_lower_severity_entries() {
361 clear_min_log_level();
362 assert_eq!(min_log_level(), None);
363 assert!(level_enabled(LogLevel::Trace));
364 assert!(level_enabled(LogLevel::Debug));
365
366 set_min_log_level(LogLevel::Info);
367
368 assert_eq!(min_log_level(), Some(LogLevel::Info));
369 assert!(!level_enabled(LogLevel::Trace));
370 assert!(!level_enabled(LogLevel::Debug));
371 assert!(level_enabled(LogLevel::Info));
372 assert!(level_enabled(LogLevel::Warn));
373 assert!(level_enabled(LogLevel::Error));
374
375 clear_min_log_level();
376 }
377
378 type RecordedLog = Arc<Mutex<Vec<(Option<TraceContext>, String)>>>;
380
381 #[derive(Debug)]
384 struct RecordingEmitter(RecordedLog);
385
386 impl LogEmitter for RecordingEmitter {
387 fn emit(&self, entry: &LogEntry) {
388 self.0
389 .lock()
390 .unwrap_or_else(PoisonError::into_inner)
391 .push((entry.trace_context, entry.message.clone()));
392 }
393 }
394
395 #[test]
399 fn global_log_path_is_no_op_until_installed_then_emits_with_trace_context() {
400 clear_min_log_level();
403 let probed = Cell::new(false);
404 let mut fields_called = false;
405 super::log_with_fields(
406 LogLevel::Info,
407 module_path!(),
408 MessageProbe::new(&probed),
409 |builder| {
410 fields_called = true;
411 builder.field("called", LogValue::Bool(true))
412 },
413 );
414 assert!(
415 !fields_called,
416 "no-op log must not invoke the fields closure"
417 );
418 assert!(!probed.get(), "no-op log must not build the message");
419
420 let recorded = Arc::new(Mutex::new(Vec::new()));
422 assert!(
423 install_log_emitter(RecordingEmitter(Arc::clone(&recorded))).is_ok(),
424 "no emitter installed yet in this test binary"
425 );
426
427 set_min_log_level(LogLevel::Info);
430 let filtered_probe = Cell::new(false);
431 let mut filtered_fields = false;
432 super::log_with_fields(
433 LogLevel::Debug,
434 module_path!(),
435 MessageProbe::new(&filtered_probe),
436 |builder| {
437 filtered_fields = true;
438 builder.field("called", LogValue::Bool(true))
439 },
440 );
441 assert!(
442 !filtered_fields,
443 "filtered log must not invoke the fields closure"
444 );
445 assert!(
446 !filtered_probe.get(),
447 "filtered log must not build the message"
448 );
449 assert!(
450 recorded
451 .lock()
452 .unwrap_or_else(PoisonError::into_inner)
453 .is_empty(),
454 "filtered log must not reach the emitter"
455 );
456
457 clear_min_log_level();
460 let context = {
461 let guard = SpanGuard::start_conversation("conversation");
462 log(LogLevel::Info, module_path!(), "inside span");
463 guard.context()
464 };
465
466 let entries = recorded.lock().unwrap_or_else(PoisonError::into_inner);
467 assert_eq!(
468 entries.len(),
469 1,
470 "exactly one entry emitted via the global path"
471 );
472 assert_eq!(
473 entries[0].0,
474 Some(context),
475 "emitted entry must carry the active trace context"
476 );
477 assert_eq!(entries[0].1, "inside span");
478 drop(entries);
479 clear_min_log_level();
480 }
481
482 #[derive(Debug)]
483 struct MessageProbe<'a>(&'a Cell<bool>);
484
485 impl<'a> MessageProbe<'a> {
486 const fn new(converted: &'a Cell<bool>) -> Self {
487 Self(converted)
488 }
489 }
490
491 impl From<MessageProbe<'_>> for String {
492 fn from(probe: MessageProbe<'_>) -> Self {
493 probe.0.set(true);
494 Self::new()
495 }
496 }
497}