use crate::{
InternalError,
dto::{
log::LogEntry,
page::{Page, PageRequest},
},
log::{Level, Topic},
ops::{config::ConfigOps, runtime::RuntimeOpsError},
storage::{
StorageError,
stable::log::{Log, LogEntryRecord, RetentionSummary, apply_retention},
},
};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum LogOpsError {
#[error(transparent)]
Storage(#[from] StorageError),
}
impl From<LogOpsError> for InternalError {
fn from(err: LogOpsError) -> Self {
RuntimeOpsError::LogOps(err).into()
}
}
pub struct LogOps;
impl LogOps {
pub fn append_runtime_log(
crate_name: &str,
topic: Option<Topic>,
level: Level,
message: &str,
created_at: u64,
) -> Result<u64, InternalError> {
if !crate::log::is_ready() {
return Ok(0);
}
let cfg = ConfigOps::log_config()?;
let max_entries = usize::try_from(cfg.max_entries).unwrap_or(usize::MAX);
let entry = LogEntryRecord {
crate_name: crate_name.to_string(),
created_at,
level,
topic,
message: message.to_string(),
};
let id = Log::append(max_entries, cfg.max_entry_bytes, entry).map_err(LogOpsError::from)?;
Ok(id)
}
pub fn apply_retention(
cutoff: Option<u64>,
max_entries: usize,
max_entry_bytes: u32,
) -> Result<RetentionSummary, InternalError> {
let summary =
apply_retention(cutoff, max_entries, max_entry_bytes).map_err(LogOpsError::from)?;
Ok(summary)
}
#[must_use]
pub fn page_filtered(
crate_name: Option<&str>,
topic: Option<&str>,
min_level: Option<Level>,
page: PageRequest,
) -> Page<LogEntry> {
let mut entries = Vec::new();
let mut total = 0u64;
let offset = page.offset;
let limit = page.limit.min(1_000);
for entry in Log::snapshot().into_iter().rev() {
if !record_matches(&entry, crate_name, topic, min_level) {
continue;
}
if total >= offset && (entries.len() as u64) < limit {
entries.push(record_to_entry(entry));
}
total = total.saturating_add(1);
}
Page { entries, total }
}
}
fn record_to_entry(entry: LogEntryRecord) -> LogEntry {
LogEntry {
crate_name: entry.crate_name,
created_at: entry.created_at,
level: entry.level,
topic: entry.topic.map(|topic| topic.log_label().to_string()),
message: entry.message,
}
}
fn record_matches(
entry: &LogEntryRecord,
crate_name: Option<&str>,
topic: Option<&str>,
min_level: Option<Level>,
) -> bool {
crate_name.is_none_or(|name| entry.crate_name == name)
&& topic.is_none_or(|needle| topic_matches(entry.topic, needle))
&& min_level.is_none_or(|min| entry.level >= min)
}
fn topic_matches(topic: Option<Topic>, needle: &str) -> bool {
topic.is_some_and(|topic| topic.log_label() == needle)
}