reifydb_core/interface/logging/
mod.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4use std::{
5	collections::HashMap,
6	fmt,
7	fmt::Debug,
8	sync::{Mutex, OnceLock},
9	thread::current,
10};
11
12use crossbeam_channel::{SendError, Sender};
13use reifydb_type::{IntoValue, Value, value::DateTime};
14use serde::{Deserialize, Serialize};
15
16use crate::util;
17
18mod macros;
19pub mod mock;
20pub mod timed;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
23pub enum LogLevel {
24	Off = 0,
25	Trace = 1,
26	Debug = 2,
27	Info = 3,
28	Warn = 4,
29	Error = 5,
30	Critical = 6,
31}
32
33impl LogLevel {
34	pub fn as_str(&self) -> &'static str {
35		match self {
36			LogLevel::Off => "off",
37			LogLevel::Trace => "trace",
38			LogLevel::Debug => "debug",
39			LogLevel::Info => "info",
40			LogLevel::Warn => "warn",
41			LogLevel::Error => "error",
42			LogLevel::Critical => "critical",
43		}
44	}
45}
46
47impl fmt::Display for LogLevel {
48	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49		write!(f, "{}", self.as_str())
50	}
51}
52
53/// Structured log record
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Record {
56	/// Timestamp when the log was created
57	pub timestamp: DateTime,
58	/// Log severity level
59	pub level: LogLevel,
60	/// Source module/crate (with reifydb- prefix stripped)
61	pub module: String,
62	/// Log message
63	pub message: String,
64	/// Structured fields (key-value pairs using ReifyDB Values)
65	pub fields: HashMap<Value, Value>,
66	/// File location where log was generated
67	pub file: Option<String>,
68	/// Line number where log was generated
69	pub line: Option<u32>,
70	/// Thread ID that generated the log
71	pub thread_id: String,
72}
73
74impl Record {
75	pub fn new(level: LogLevel, module: impl Into<String>, message: impl Into<String>) -> Self {
76		Self {
77			timestamp: util::now(),
78			level,
79			module: module.into(),
80			message: message.into(),
81			fields: HashMap::new(),
82			file: None,
83			line: None,
84			thread_id: format!("{:?}", current().id()),
85		}
86	}
87
88	pub fn with_field(mut self, key: impl IntoValue, value: impl IntoValue) -> Self {
89		self.fields.insert(key.into_value(), value.into_value());
90		self
91	}
92
93	pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
94		self.file = Some(file.into());
95		self.line = Some(line);
96		self
97	}
98}
99
100pub trait LogBackend: Send + Sync + Debug {
101	fn name(&self) -> &str;
102
103	fn write(&self, records: &[Record]) -> crate::Result<()>;
104
105	fn flush(&self) -> crate::Result<()> {
106		Ok(())
107	}
108}
109
110pub struct Logger {
111	sender: Sender<Record>,
112}
113
114impl Logger {
115	/// Create a new logger with a channel to the subsystem
116	pub fn new(sender: Sender<Record>) -> Self {
117		Self {
118			sender,
119		}
120	}
121
122	/// Log a record by sending it through the channel
123	pub fn log(&self, record: Record) -> Result<(), SendError<Record>> {
124		self.sender.send(record)
125	}
126}
127
128/// Global logger instance - lightweight, only holds a channel sender
129static LOGGER: OnceLock<Logger> = OnceLock::new();
130
131/// Maximum number of log records to buffer before logger is initialized
132const MAX_BUFFERED_RECORDS: usize = 10_000;
133
134/// Buffer for log records that arrive before logger initialization
135static LOG_BUFFER: OnceLock<Mutex<Vec<Record>>> = OnceLock::new();
136
137/// Get or create the log buffer
138fn get_log_buffer() -> &'static Mutex<Vec<Record>> {
139	LOG_BUFFER.get_or_init(|| Mutex::new(Vec::with_capacity(1000)))
140}
141
142/// Initialize the global logger with a sender channel
143/// This can only be called once - subsequent calls will be ignored
144pub fn init_logger(sender: Sender<Record>) {
145	let logger = Logger::new(sender);
146
147	// Set the logger first
148	if LOGGER.set(logger).is_ok() {
149		if let Ok(mut buffer) = get_log_buffer().lock() {
150			if !buffer.is_empty() {
151				if let Some(logger) = LOGGER.get() {
152					for record in buffer.drain(..) {
153						let _ = logger.log(record);
154					}
155				}
156			}
157		}
158	}
159}
160
161/// Get the global logger
162pub fn logger() -> Option<&'static Logger> {
163	LOGGER.get()
164}
165
166/// Send a log record through the global logger
167/// In debug builds, checks for a thread-local mock logger first
168pub fn log(record: Record) {
169	// Check for mock logger in debug builds
170	#[cfg(debug_assertions)]
171	{
172		if let Some(sender) = mock::get_mock_logger() {
173			// Send to mock logger instead of global logger
174			let _ = sender.send(record);
175			return;
176		}
177	}
178
179	// Normal path: use global logger if available
180	if let Some(logger) = logger() {
181		// Ignore send errors - logging should not crash the application
182		let _ = logger.log(record);
183	} else {
184		// No logger yet, buffer the record
185		if let Ok(mut buffer) = get_log_buffer().lock() {
186			// Only buffer up to MAX_BUFFERED_RECORDS to prevent
187			// unbounded memory growth
188			if buffer.len() < MAX_BUFFERED_RECORDS {
189				buffer.push(record);
190			}
191			// If we've reached the limit, we silently drop new
192			// records This prevents memory exhaustion if logging
193			// is misconfigured
194		}
195	}
196}