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
//! Synchronized bounded log records with explicit flush and close.
use std::io::Write;
use std::sync::{Mutex, MutexGuard};
use std::time::SystemTime;
use crate::{Result, invalid};
/// Severity attached to a production log record.
#[derive(Clone, Copy, Debug)]
pub enum Level {
/// Routine progress.
Info,
/// Recoverable issue.
Warning,
/// Failed operation.
Error,
/// Diagnostic detail.
Debug,
}
struct State<W> {
writer: Option<W>,
timestamps: bool,
prefix: bool,
}
/// A writer shared by concurrent runtime workers.
///
/// Record writes and format changes serialize on one mutex; close flushes the
/// writer and rejects later operations. The sink owns any persistent storage.
pub struct Logger<W: Write + Send> {
state: Mutex<State<W>>,
}
impl<W: Write + Send> Logger<W> {
/// Owns a writer with timestamps and severity prefixes initially disabled.
pub const fn new(writer: W) -> Self {
Self {
state: Mutex::new(State {
writer: Some(writer),
timestamps: false,
prefix: false,
}),
}
}
fn state(&self) -> Result<MutexGuard<'_, State<W>>> {
let state = self
.state
.lock()
.map_err(|_| invalid("logger worker poisoned the writer"))?;
if state.writer.is_none() {
return Err(invalid("logger is closed"));
}
Ok(state)
}
/// Changes timestamp emission atomically with respect to record writes.
///
/// # Errors
/// Rejects a closed or poisoned writer.
pub fn set_timestamps(&self, enabled: bool) -> Result<()> {
self.state()?.timestamps = enabled;
Ok(())
}
/// Changes severity prefix emission atomically with respect to writes.
///
/// # Errors
/// Rejects a closed or poisoned writer.
pub fn set_prefix(&self, enabled: bool) -> Result<()> {
self.state()?.prefix = enabled;
Ok(())
}
/// Writes a complete UTF-8 record while excluding other workers.
///
/// # Errors
/// Rejects NUL, records larger than 1024 bytes, closed writers and I/O errors.
pub fn emit(&self, level: Level, text: &str) -> Result<()> {
if text.len() > 1024 || text.contains('\0') {
return Err(invalid("log record exceeds bounds"));
}
let mut state = self.state()?;
let timestamps = state.timestamps;
let prefix = state.prefix;
let writer = state
.writer
.as_mut()
.ok_or_else(|| invalid("logger is closed"))?;
if timestamps {
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
write!(
writer,
"[{}.{:03}] ",
timestamp.as_secs(),
timestamp.subsec_millis()
)?;
}
if prefix {
write!(writer, "{level:?}: ")?;
}
writer.write_all(text.as_bytes())?;
drop(state);
Ok(())
}
/// Flushes all completed records to the owned writer.
///
/// # Errors
/// Rejects a closed/poisoned writer and propagates flush failures.
pub fn flush(&self) -> Result<()> {
self.state()?
.writer
.as_mut()
.ok_or_else(|| invalid("logger is closed"))?
.flush()?;
Ok(())
}
/// Flushes and destroys the writer, making all subsequent calls fail.
///
/// # Errors
/// Rejects repeated close, poisoning and flush errors. On a flush error the
/// writer remains owned so the caller can retry or inspect its sink.
pub fn close(&self) -> Result<()> {
let mut state = self.state()?;
state
.writer
.as_mut()
.ok_or_else(|| invalid("logger is closed"))?
.flush()?;
state.writer.take();
drop(state);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closing_is_visible_to_all_worker_calls() -> Result<()> {
let log = Logger::new(Vec::new());
std::thread::scope(|scope| {
let worker = scope.spawn(|| log.emit(Level::Info, "worker\n"));
worker.join().map_err(|_| invalid("log worker panicked"))?
})?;
log.flush()?;
log.close()?;
assert!(log.emit(Level::Info, "late").is_err());
assert!(log.set_prefix(true).is_err());
assert!(log.close().is_err());
Ok(())
}
}