1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
use serde_json::Value;
use std::{
sync::{Arc, atomic::AtomicU64},
time::Duration,
};
use crate::{
LogLevel, Logger, RGB,
colors::ColorSettings,
constants::DEFAULT_BUFFER_FULL_LAST_WARN_MS,
globals::GLOBAL_LOGGER,
logger::{LogObject, LoggerContext},
utils::{FormatState, RESERVED_FIELD_NAMES, flush_batch},
};
use super::core::ShutdownHandle;
/// Builder for configuring a [`Logger`] instance.
///
/// Created by calling [`Logger::init()`] and finalized with [`.build()`](LoggerOptions::build).
pub struct LoggerOptions {
pub(crate) buffer_size: usize,
pub(crate) batch_size: usize,
pub(crate) batch_duration_ms: u64,
pub(crate) min_level: LogLevel,
pub(crate) timestamp_format: String,
pub(crate) timestamp_key: String,
pub(crate) color_settings: ColorSettings,
pub(crate) context: LoggerContext,
pub(crate) pretty: bool,
}
impl LoggerOptions {
/// The lowest logging level to print
///
/// Example: [`LogLevel::Info`] will skip Debug logs and show Info, Warning, and Error only
///
/// Default is [`LogLevel::Debug`]
#[must_use]
pub const fn min_level(mut self, log_level: LogLevel) -> Self {
self.min_level = log_level;
self
}
/// How many messages to send down the channel before
/// messages start to be dropped.
///
/// Default is [`DEFAULT_BUFFER_SIZE`] - 1024
///
#[must_use]
pub const fn buffer_size(mut self, buffer_size: usize) -> Self {
self.buffer_size = buffer_size;
self
}
/// How many log messages to batch
///
/// Default is [`DEFAULT_BATCH_SIZE`] - 50
#[must_use]
pub const fn batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size;
self
}
/// For how long to batch messages for
///
/// Default is [`DEFAULT_BATCH_DURATION_MS`] - 50ms
#[must_use]
pub const fn batch_duration_ms(mut self, batch_duration_ms: u64) -> Self {
self.batch_duration_ms = batch_duration_ms;
self
}
/// Formats the combined date and time per the specified format string.
/// See the [chrono::format::strftime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) module for the supported escape sequences.
/// Default is [`DEFAULT_TIMESTAMP_FORMAT`] - "%Y-%m-%dT%H:%M:%S%.3fZ" which outputs: 2025-10-26T22:04:29.412Z
#[must_use]
pub fn timestamp_format(mut self, timestamp_format: impl Into<String>) -> Self {
self.timestamp_format = timestamp_format.into();
self
}
/// Allows changing the name of the `timestamp` default key name to something like `tz`
#[must_use]
pub fn timestamp_key(mut self, timestamp_key: impl Into<String>) -> Self {
self.timestamp_key = timestamp_key.into();
self
}
/// Sets the debug color using [`RGB`]
#[must_use]
pub const fn debug_color(mut self, color: RGB) -> Self {
self.color_settings.debug = color;
self
}
/// Sets the info color using [`RGB`]
#[must_use]
pub const fn info_color(mut self, color: RGB) -> Self {
self.color_settings.info = color;
self
}
/// Sets the warn color using [`RGB`]
#[must_use]
pub const fn warn_color(mut self, color: RGB) -> Self {
self.color_settings.warn = color;
self
}
/// Sets the error color using [`RGB`]
#[must_use]
pub const fn error_color(mut self, color: RGB) -> Self {
self.color_settings.error = color;
self
}
/// Sets global context for every log message
/// For example, environment or service-name
/// # Panics
/// Panics if using a reserved field name. See [`RESERVED_FIELD_NAMES`]
#[must_use]
pub fn context(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.context.insert(key.into(), value.into());
self
}
/// Enables pretty-printing of JSON output with indentation and newlines.
///
/// When enabled, logs will be formatted across multiple lines for easier reading.
/// This is useful for development but should typically be disabled in production
/// for log aggregation systems that expect one log per line.
///
/// Note: Colors will still be applied to the log level, but the output will
/// contain ANSI escape codes that may not parse as valid JSON.
///
/// Default is `false` (compact, single-line output)
#[must_use]
pub const fn pretty(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
fn validate(&self) {
assert!(
!RESERVED_FIELD_NAMES.contains(&self.timestamp_key.as_ref()),
"\n\nCannot use '{}' as the timestamp key - it's a reserved field name along with {}",
&self.timestamp_key,
RESERVED_FIELD_NAMES
.iter()
.filter(|v| *v != &self.timestamp_key)
.map(|v| format!("'{v}'"))
.collect::<Vec<_>>()
.join(", ")
);
for (key, _value) in &self.context {
assert!(
!RESERVED_FIELD_NAMES.contains(&key.as_str()),
"\n\nCannot use '{}' as a context key - it's a reserved field name along with '{}",
key,
RESERVED_FIELD_NAMES
.iter()
.filter(|v| *v != key)
.map(|v| format!("'{v}'"))
.collect::<Vec<_>>()
.join(", ")
);
assert!(
key != &self.timestamp_key,
"\n\nCannot use '{key}' as a context key as it is set as the timestamp key. Either rename the timestamp key from '{key}' to something else with .timestamp_key(new_value) or rename your context key"
);
}
}
/// Build and initialize the logger.
///
/// This spawns a background task that handles batching and writing logs.
/// The logger is ready to use immediately after calling this method.
///
/// When the program exits, the logger will automatically flush all remaining
/// logs before shutting down.
///
/// # Panics
/// Actually does not as we just set the logger. This shouldn't happen.
pub fn build(self) {
// If already initialized, return it
if GLOBAL_LOGGER.get().is_some() {
eprintln!(
"WARNING - LOGGER ALREADY INITIALIZED! ANY NEW SETTINGS WILL NOT BE APPLIED."
);
return;
}
// Do some final validation checks
self.validate();
let (log_sender, log_receiver) = crossbeam_channel::bounded::<LogObject>(self.buffer_size);
let (shutdown_sender, shutdown_receiver) = crossbeam_channel::bounded::<()>(1);
// Move configuration into the worker thread
let colors = self.color_settings;
let batch_size = self.batch_size;
let batch_duration = Duration::from_millis(self.batch_duration_ms);
let mut context_fields: Vec<(String, Value)> = Vec::new();
if self.context.keys().len() > 0 {
// Add context fields
for (k, v) in &self.context {
context_fields.push((k.clone(), v.clone()));
}
}
let format_state = Arc::new(FormatState {
timestamp_format: self.timestamp_format,
timestamp_key: self.timestamp_key,
color_settings: colors,
pretty: self.pretty,
context_fields,
});
// For use in logger thread
let format_state_clone = Arc::clone(&format_state);
let worker_thread = std::thread::spawn(move || {
let mut batch = Vec::<LogObject>::with_capacity(batch_size);
let mut deadline = crossbeam_channel::after(batch_duration);
loop {
crossbeam_channel::select! {
recv(log_receiver) -> msg => if let Ok(log) = msg {
batch.push(log);
if batch.len() >= batch_size {
flush_batch( &batch, &format_state_clone);
batch.clear();
deadline = crossbeam_channel::after(batch_duration);
}
}
else {
// Sender disconnected, flush remaining logs and exit
if !batch.is_empty() {
flush_batch( &batch, &format_state_clone);
}
break;
},
recv(deadline) -> _ => {
if !batch.is_empty() {
flush_batch( &batch, &format_state_clone);
batch.clear();
}
deadline = crossbeam_channel::after(batch_duration);
},
recv(shutdown_receiver) -> _ => {
// Shutdown signal received - drain all remaining logs
// First, drop our receiver handle to stop receiving new messages
drop(shutdown_receiver);
// Drain any remaining messages in the channel
while let Ok(log) = log_receiver.try_recv() {
batch.push(log);
if batch.len() >= batch_size {
flush_batch(&batch, &format_state_clone);
batch.clear();
}
}
// Flush final batch
if !batch.is_empty() {
flush_batch(&batch, &format_state_clone);
}
break;
}
}
}
});
let shutdown_handle = Arc::new(ShutdownHandle::new(shutdown_sender, worker_thread));
let logger = Logger {
log_sender,
min_level: self.min_level,
shutdown_handle,
context: Arc::new(self.context),
format_state,
buffer_full_last_warn_ms: AtomicU64::new(DEFAULT_BUFFER_FULL_LAST_WARN_MS),
};
match GLOBAL_LOGGER.set(logger) {
Ok(()) => {
// Register atexit handler to ensure logs are flushed on shutdown
extern "C" fn shutdown_handler() {
crate::globals::shutdown_global_logger();
}
unsafe {
libc::atexit(shutdown_handler);
}
GLOBAL_LOGGER
.get()
.expect("Logger to be there, but it wasn't.")
}
// Incase of a race condition, return the existing one
Err(_) => GLOBAL_LOGGER.get().unwrap(),
};
}
}