use bytes::Bytes;
use chrono::{DateTime, Utc};
use super::cursor::LogCursor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogSource {
Stdout,
Stderr,
Output,
System,
}
impl LogSource {
pub(crate) fn effective(requested: &[Self]) -> Vec<Self> {
if requested.is_empty() {
vec![Self::Stdout, Self::Stderr, Self::Output]
} else {
let mut s = requested.to_vec();
s.sort_by_key(|src| match src {
Self::Stdout => 0,
Self::Stderr => 1,
Self::Output => 2,
Self::System => 3,
});
s.dedup();
s
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: DateTime<Utc>,
pub source: LogSource,
pub session_id: Option<u64>,
pub data: Bytes,
pub cursor: LogCursor,
}
#[derive(Debug, Clone, Default)]
pub struct LogOptions {
pub tail: Option<usize>,
pub since: Option<DateTime<Utc>>,
pub until: Option<DateTime<Utc>>,
pub sources: Vec<LogSource>,
}
impl LogOptions {
pub(crate) fn apply_to(&self, entries: &mut Vec<LogEntry>) {
if let Some(s) = self.since {
entries.retain(|e| e.timestamp >= s);
}
if let Some(u) = self.until {
entries.retain(|e| e.timestamp < u);
}
if let Some(n) = self.tail
&& entries.len() > n
{
let drop = entries.len() - n;
entries.drain(0..drop);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_sources_are_user_program_output() {
let s = LogSource::effective(&[]);
assert_eq!(
s,
vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
);
}
#[test]
fn explicit_sources_used_when_set() {
let s = LogSource::effective(&[LogSource::System]);
assert_eq!(s, vec![LogSource::System]);
}
#[test]
fn apply_filters_tail() {
let mut entries = (0..5)
.map(|i| LogEntry {
timestamp: DateTime::parse_from_rfc3339(&format!("2026-04-30T00:00:0{i}.000Z"))
.unwrap()
.with_timezone(&Utc),
source: LogSource::Stdout,
session_id: Some(1u64),
data: Bytes::from(format!("line {i}").into_bytes()),
cursor: LogCursor::empty(),
})
.collect::<Vec<_>>();
LogOptions {
tail: Some(2),
..Default::default()
}
.apply_to(&mut entries);
assert_eq!(entries.len(), 2);
}
}