1use parking_lot::Mutex;
14use std::collections::VecDeque;
15use std::io::Write;
16use std::sync::Arc;
17
18pub const MAX_IN_MEMORY_LOG_RECORDS: usize = 4_096;
20pub const MAX_IN_MEMORY_LOG_BYTES: usize = 8 * 1024 * 1024;
22pub const MAX_LOG_TARGET_BYTES: usize = 128;
24const LOG_RECORD_FIXED_BYTES: usize = std::mem::size_of::<LogRecord>()
25 + std::mem::size_of::<Arc<LogRecord>>()
26 + std::mem::size_of::<usize>() * 2;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum LogLevel {
31 Trace,
33 Debug,
35 Info,
37 Warn,
39 Error,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct LogRecord {
46 pub level: LogLevel,
48 pub target: String,
50 pub message: String,
52 pub timestamp_ms: u64,
54}
55
56pub trait RuntimeLogger: Send + Sync {
58 fn log(&self, record: LogRecord);
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct LogSnapshot {
65 records: Arc<VecDeque<Arc<LogRecord>>>,
66}
67
68impl LogSnapshot {
69 pub fn len(&self) -> usize {
71 self.records.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
76 self.records.is_empty()
77 }
78
79 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &LogRecord> {
81 self.records.iter().map(AsRef::as_ref)
82 }
83
84 pub fn recent(&self, limit: usize) -> impl Iterator<Item = &LogRecord> {
86 self.records
87 .iter()
88 .skip(self.records.len().saturating_sub(limit))
89 .map(AsRef::as_ref)
90 }
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95pub struct InMemoryLogPressure {
96 pub entries: usize,
98 pub max_entries: usize,
100 pub used_bytes: usize,
102 pub peak_bytes: usize,
104 pub max_bytes: usize,
106 pub evictions: u64,
108 pub oversized_rejections: u64,
110}
111
112#[derive(Debug)]
113struct LogState {
114 records: Arc<VecDeque<Arc<LogRecord>>>,
115 pressure: InMemoryLogPressure,
116}
117
118#[derive(Debug, Default, Clone, Copy)]
120pub struct StdoutLogger;
121
122impl StdoutLogger {
123 pub fn new() -> Self {
125 Self
126 }
127}
128
129impl RuntimeLogger for StdoutLogger {
130 fn log(&self, record: LogRecord) {
131 let message = appcore_core::redact_text(&record.message);
132 let target = appcore_core::redact_text_with_limit(&record.target, 128);
133 let _ = writeln!(
134 std::io::stdout().lock(),
135 "[{:?}] {} {} {}",
136 record.level,
137 target,
138 message,
139 record.timestamp_ms
140 );
141 }
142}
143
144#[derive(Debug)]
146pub struct InMemoryLogger {
147 state: Mutex<LogState>,
148}
149
150impl InMemoryLogger {
151 pub fn new() -> Self {
153 Self::with_limits(MAX_IN_MEMORY_LOG_RECORDS, MAX_IN_MEMORY_LOG_BYTES)
154 }
155
156 pub fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
158 let max_entries = max_entries.clamp(1, MAX_IN_MEMORY_LOG_RECORDS);
159 let max_bytes = max_bytes.clamp(1, MAX_IN_MEMORY_LOG_BYTES);
160 Self {
161 state: Mutex::new(LogState {
162 records: Arc::new(VecDeque::new()),
163 pressure: InMemoryLogPressure {
164 max_entries,
165 max_bytes,
166 ..InMemoryLogPressure::default()
167 },
168 }),
169 }
170 }
171
172 pub fn len(&self) -> usize {
174 self.state.lock().pressure.entries
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.len() == 0
180 }
181
182 pub fn records(&self) -> Vec<LogRecord> {
184 self.shared_records().iter().cloned().collect()
185 }
186
187 pub fn shared_records(&self) -> LogSnapshot {
189 LogSnapshot {
190 records: Arc::clone(&self.state.lock().records),
191 }
192 }
193
194 pub fn pressure(&self) -> InMemoryLogPressure {
196 self.state.lock().pressure
197 }
198}
199
200impl Default for InMemoryLogger {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl RuntimeLogger for InMemoryLogger {
207 fn log(&self, record: LogRecord) {
208 let mut record = record;
209 record.message = appcore_core::redact_text(&record.message);
210 record.target = appcore_core::redact_text_with_limit(&record.target, MAX_LOG_TARGET_BYTES);
211 record.message.shrink_to_fit();
212 record.target.shrink_to_fit();
213 let record = Arc::new(record);
214 let retained_bytes = log_record_retained_bytes(&record);
215 let mut state = self.state.lock();
216 if retained_bytes > state.pressure.max_bytes {
217 state.pressure.oversized_rejections =
218 state.pressure.oversized_rejections.saturating_add(1);
219 return;
220 }
221 while state.pressure.entries >= state.pressure.max_entries
222 || state.pressure.used_bytes.saturating_add(retained_bytes) > state.pressure.max_bytes
223 {
224 let Some(removed) = Arc::make_mut(&mut state.records).pop_front() else {
225 break;
226 };
227 state.pressure.entries = state.pressure.entries.saturating_sub(1);
228 state.pressure.used_bytes = state
229 .pressure
230 .used_bytes
231 .saturating_sub(log_record_retained_bytes(&removed));
232 state.pressure.evictions = state.pressure.evictions.saturating_add(1);
233 }
234 Arc::make_mut(&mut state.records).push_back(record);
235 state.pressure.entries = state.pressure.entries.saturating_add(1);
236 state.pressure.used_bytes = state.pressure.used_bytes.saturating_add(retained_bytes);
237 state.pressure.peak_bytes = state.pressure.peak_bytes.max(state.pressure.used_bytes);
238 }
239}
240
241fn log_record_retained_bytes(record: &LogRecord) -> usize {
242 LOG_RECORD_FIXED_BYTES
243 .saturating_add(record.target.capacity())
244 .saturating_add(record.message.capacity())
245}
246
247#[cfg(test)]
248#[path = "log_tests.rs"]
249mod tests;