use parking_lot::Mutex;
use std::collections::VecDeque;
use std::io::Write;
use std::sync::Arc;
pub const MAX_IN_MEMORY_LOG_RECORDS: usize = 4_096;
pub const MAX_IN_MEMORY_LOG_BYTES: usize = 8 * 1024 * 1024;
pub const MAX_LOG_TARGET_BYTES: usize = 128;
const LOG_RECORD_FIXED_BYTES: usize = std::mem::size_of::<LogRecord>()
+ std::mem::size_of::<Arc<LogRecord>>()
+ std::mem::size_of::<usize>() * 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
pub level: LogLevel,
pub target: String,
pub message: String,
pub timestamp_ms: u64,
}
pub trait RuntimeLogger: Send + Sync {
fn log(&self, record: LogRecord);
}
#[derive(Debug, Clone, Default)]
pub struct LogSnapshot {
records: Arc<VecDeque<Arc<LogRecord>>>,
}
impl LogSnapshot {
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &LogRecord> {
self.records.iter().map(AsRef::as_ref)
}
pub fn recent(&self, limit: usize) -> impl Iterator<Item = &LogRecord> {
self.records
.iter()
.skip(self.records.len().saturating_sub(limit))
.map(AsRef::as_ref)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct InMemoryLogPressure {
pub entries: usize,
pub max_entries: usize,
pub used_bytes: usize,
pub peak_bytes: usize,
pub max_bytes: usize,
pub evictions: u64,
pub oversized_rejections: u64,
}
#[derive(Debug)]
struct LogState {
records: Arc<VecDeque<Arc<LogRecord>>>,
pressure: InMemoryLogPressure,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct StdoutLogger;
impl StdoutLogger {
pub fn new() -> Self {
Self
}
}
impl RuntimeLogger for StdoutLogger {
fn log(&self, record: LogRecord) {
let message = appcore_core::redact_text(&record.message);
let target = appcore_core::redact_text_with_limit(&record.target, 128);
let _ = writeln!(
std::io::stdout().lock(),
"[{:?}] {} {} {}",
record.level,
target,
message,
record.timestamp_ms
);
}
}
#[derive(Debug)]
pub struct InMemoryLogger {
state: Mutex<LogState>,
}
impl InMemoryLogger {
pub fn new() -> Self {
Self::with_limits(MAX_IN_MEMORY_LOG_RECORDS, MAX_IN_MEMORY_LOG_BYTES)
}
pub fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
let max_entries = max_entries.clamp(1, MAX_IN_MEMORY_LOG_RECORDS);
let max_bytes = max_bytes.clamp(1, MAX_IN_MEMORY_LOG_BYTES);
Self {
state: Mutex::new(LogState {
records: Arc::new(VecDeque::new()),
pressure: InMemoryLogPressure {
max_entries,
max_bytes,
..InMemoryLogPressure::default()
},
}),
}
}
pub fn len(&self) -> usize {
self.state.lock().pressure.entries
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn records(&self) -> Vec<LogRecord> {
self.shared_records().iter().cloned().collect()
}
pub fn shared_records(&self) -> LogSnapshot {
LogSnapshot {
records: Arc::clone(&self.state.lock().records),
}
}
pub fn pressure(&self) -> InMemoryLogPressure {
self.state.lock().pressure
}
}
impl Default for InMemoryLogger {
fn default() -> Self {
Self::new()
}
}
impl RuntimeLogger for InMemoryLogger {
fn log(&self, record: LogRecord) {
let mut record = record;
record.message = appcore_core::redact_text(&record.message);
record.target = appcore_core::redact_text_with_limit(&record.target, MAX_LOG_TARGET_BYTES);
record.message.shrink_to_fit();
record.target.shrink_to_fit();
let record = Arc::new(record);
let retained_bytes = log_record_retained_bytes(&record);
let mut state = self.state.lock();
if retained_bytes > state.pressure.max_bytes {
state.pressure.oversized_rejections =
state.pressure.oversized_rejections.saturating_add(1);
return;
}
while state.pressure.entries >= state.pressure.max_entries
|| state.pressure.used_bytes.saturating_add(retained_bytes) > state.pressure.max_bytes
{
let Some(removed) = Arc::make_mut(&mut state.records).pop_front() else {
break;
};
state.pressure.entries = state.pressure.entries.saturating_sub(1);
state.pressure.used_bytes = state
.pressure
.used_bytes
.saturating_sub(log_record_retained_bytes(&removed));
state.pressure.evictions = state.pressure.evictions.saturating_add(1);
}
Arc::make_mut(&mut state.records).push_back(record);
state.pressure.entries = state.pressure.entries.saturating_add(1);
state.pressure.used_bytes = state.pressure.used_bytes.saturating_add(retained_bytes);
state.pressure.peak_bytes = state.pressure.peak_bytes.max(state.pressure.used_bytes);
}
}
fn log_record_retained_bytes(record: &LogRecord) -> usize {
LOG_RECORD_FIXED_BYTES
.saturating_add(record.target.capacity())
.saturating_add(record.message.capacity())
}
#[cfg(test)]
#[path = "log_tests.rs"]
mod tests;