mod block;
mod collision;
mod entry;
mod stats;
#[cfg(test)]
mod tests;
use std::collections::VecDeque;
use crate::attached::AttachedLines;
use crate::chain::SEGMENT_BOUNDARY;
use crate::fields::FieldLocation;
use crate::line::{
is_date_at, is_log_header_at, is_uuid_at, parse_line, LineKind, RawLine, UUID_PREFIX_LEN,
};
use crate::message::{classify_message, MessageKind};
use block::BlockBuilder;
use collision::{WriteCursor, DECODE_DRIFT};
pub use entry::{Block, LogEntry, ParseWarning, SessionReading};
pub use stats::{ParseStats, UnclassifiedLine, UnclassifiedReason, UnclassifiedTracking};
const TIMESTAMP_FIELD_LEN: usize = 27;
const FULL_HEADER_LEN: usize = UUID_PREFIX_LEN + TIMESTAMP_FIELD_LEN;
fn header_len(bytes: &[u8]) -> usize {
if is_uuid_at(bytes, 0) {
if bytes.len() > UUID_PREFIX_LEN && bytes[UUID_PREFIX_LEN].is_ascii_digit() {
FULL_HEADER_LEN
} else {
UUID_PREFIX_LEN
}
} else if is_date_at(bytes, 0) {
TIMESTAMP_FIELD_LEN
} else {
0
}
}
fn scan_splits(
bytes: &[u8],
min_scan: usize,
first_boundary: Option<usize>,
) -> (Vec<usize>, Vec<bool>) {
let end = bytes.len();
let mut next_boundary = first_boundary;
let mut splits: Vec<usize> = Vec::new();
let mut cut_verdicts: Vec<bool> = Vec::new();
let mut chunk_start = 0usize;
let mut offset = 0usize;
while offset <= end {
if let Some(boundary) = next_boundary {
if offset >= boundary && offset <= boundary + DECODE_DRIFT {
let uuid = is_uuid_at(bytes, offset);
if uuid || is_log_header_at(bytes, offset) {
splits.push(offset);
cut_verdicts.push(true);
chunk_start = offset;
next_boundary = WriteCursor::boundary_after(uuid, offset, end);
offset += UUID_PREFIX_LEN;
continue;
}
}
}
if offset >= min_scan && is_log_header_at(bytes, offset) {
let split_at = if offset >= chunk_start + UUID_PREFIX_LEN
&& is_uuid_at(bytes, offset - UUID_PREFIX_LEN)
{
offset - UUID_PREFIX_LEN
} else {
offset
};
if split_at > chunk_start {
splits.push(split_at);
cut_verdicts.push(false);
chunk_start = split_at;
next_boundary =
WriteCursor::boundary_after(is_uuid_at(bytes, split_at), split_at, end);
offset += TIMESTAMP_FIELD_LEN;
} else {
offset = (offset + TIMESTAMP_FIELD_LEN).max(offset + 1);
}
continue;
}
offset += 1;
}
cut_verdicts.push(next_boundary.is_some());
(splits, cut_verdicts)
}
fn split_chunks(line: String, splits: &[usize]) -> (String, Vec<String>) {
let mut head = line;
let mut chunks: Vec<String> = Vec::with_capacity(splits.len());
for &at in splits.iter().rev() {
chunks.push(head.split_off(at));
}
chunks.reverse();
(head, chunks)
}
#[derive(Default)]
struct LineVerdict {
chunks: VecDeque<bool>,
prev_cut: bool,
cut: bool,
warning: Option<ParseWarning>,
}
impl LineVerdict {
fn advance(&mut self) -> bool {
self.cut = self.chunks.pop_front().unwrap_or(false);
if self.cut {
self.warning = Some(ParseWarning::CutLine);
}
std::mem::replace(&mut self.prev_cut, self.cut)
}
fn claim(&mut self) -> Option<ParseWarning> {
self.warning.take()
}
fn clear_cuts(&mut self) {
self.chunks.clear();
self.prev_cut = false;
self.cut = false;
}
}
struct Pending {
entry: LogEntry,
block: BlockBuilder,
}
pub struct LogStream<I> {
lines: I,
last_uuid: String,
last_timestamp: String,
pending: Option<Pending>,
stats: ParseStats,
tracking: UnclassifiedTracking,
line_number: u64,
split_pending: VecDeque<String>,
verdict: LineVerdict,
cursor: WriteCursor,
max_attached: usize,
}
impl<I: Iterator<Item = String>> LogStream<I> {
pub fn new(lines: I) -> Self {
LogStream {
lines,
last_uuid: String::new(),
last_timestamp: String::new(),
pending: None,
stats: ParseStats::default(),
tracking: UnclassifiedTracking::CountOnly,
line_number: 0,
split_pending: VecDeque::new(),
verdict: LineVerdict::default(),
cursor: WriteCursor::default(),
max_attached: usize::MAX,
}
}
pub fn unclassified_tracking(mut self, level: UnclassifiedTracking) -> Self {
self.tracking = level;
self
}
pub fn max_attached_bytes(mut self, max_bytes: usize) -> Self {
self.max_attached = max_bytes;
self
}
pub fn stats(&self) -> &ParseStats {
&self.stats
}
pub fn drain_unclassified(&mut self) -> Vec<UnclassifiedLine> {
std::mem::take(&mut self.stats.unclassified_lines)
}
fn record_unclassified(&mut self, reason: UnclassifiedReason, data: Option<&str>) {
self.stats.lines_unclassified += 1;
match self.tracking {
UnclassifiedTracking::CountOnly => {}
UnclassifiedTracking::TrackLines => {
self.stats.unclassified_lines.push(UnclassifiedLine {
line_number: self.line_number,
reason,
data: None,
});
}
UnclassifiedTracking::CaptureData => {
self.stats.unclassified_lines.push(UnclassifiedLine {
line_number: self.line_number,
reason,
data: data.map(|s| s.to_string()),
});
}
}
}
fn take_pending(&mut self) -> Option<LogEntry> {
let mut pending = self.pending.take()?;
let (block, warnings) = pending.block.finish();
pending.entry.block = block;
pending.entry.warnings.extend(warnings);
self.stats.lines_in_entries += 1 + pending.entry.attached.len() as u64;
Some(pending.entry)
}
fn attach(&mut self, line: &str) {
let Some(pending) = self.pending.as_mut() else {
return;
};
let over_budget = pending.entry.attached.byte_len() + line.len() + 1 > self.max_attached;
if over_budget || pending.entry.attached.push(line).is_err() {
pending.entry.warnings.push(ParseWarning::AttachedOverflow {
line: ParseWarning::excerpt(line),
});
self.stats.lines_dropped += 1;
} else if self.verdict.cut {
let i = pending.entry.attached.len() - 1;
pending.entry.cut_texts.push(FieldLocation::Attached(i));
}
pending.entry.warnings.extend(self.verdict.claim());
}
fn merge_codec_run(&mut self, parsed: &RawLine<'_>, uuid: &str, line: &str) -> bool {
let Some(pending) = self.pending.as_mut() else {
return false;
};
let Some(open_media) = pending.block.codec_media() else {
return false;
};
if pending.entry.uuid.as_deref().unwrap_or("") != uuid {
return false;
}
let MessageKind::CodecNegotiation { media } = classify_message(parsed.message) else {
return false;
};
if media != open_media {
return false;
}
let warning = pending.block.push_codec_trace(parsed.message);
pending.entry.warnings.extend(warning);
self.attach(line);
true
}
fn accumulate_continuation(&mut self, msg: &str, line: &str, has_uuid: bool, prev_cut: bool) {
let Some(pending) = self.pending.as_mut() else {
return;
};
if prev_cut {
let warning = pending.block.mark_variable_cut();
pending.entry.warnings.extend(warning);
}
let warning = pending.block.push_continuation(msg, has_uuid);
pending.entry.warnings.extend(warning);
self.attach(line);
}
fn open_entry(&mut self, parsed: &RawLine<'_>, uuid: String, timestamp: String) {
let message_kind = classify_message(parsed.message);
if !uuid.is_empty() {
self.last_uuid = uuid.clone();
}
if parsed.timestamp.is_some() {
self.last_timestamp = timestamp.clone();
}
let mut block = BlockBuilder::open(&message_kind);
let opening_warning = block.push_codec_trace(parsed.message);
let entry = LogEntry {
uuid: if uuid.is_empty() { None } else { Some(uuid) },
timestamp,
message: parsed.message.to_string(),
kind: parsed.kind,
message_kind,
level: parsed.level,
idle_pct: parsed.idle_pct.map(|s| s.to_string()),
source: parsed.source.map(|s| s.to_string()),
block: None,
attached: AttachedLines::new(),
line_number: self.line_number,
warnings: self
.verdict
.claim()
.into_iter()
.chain(opening_warning)
.collect(),
cut_texts: if self.verdict.cut {
vec![FieldLocation::Message]
} else {
Vec::new()
},
};
self.pending = Some(Pending { entry, block });
}
}
impl<I: Iterator<Item = String>> LogStream<I> {
fn detect_collision(&mut self, line: String, on_disk_len: usize) -> String {
let bytes = line.as_bytes();
let prepended = is_uuid_at(bytes, 0);
if prepended {
self.cursor.begin();
} else if bytes.is_empty() || is_date_at(bytes, 0) {
self.cursor.lose();
}
let boundary = self.cursor.boundary_in(bytes.len());
let (splits, cut_verdicts) = scan_splits(bytes, header_len(bytes), boundary);
self.verdict.chunks = cut_verdicts.into();
let last_start = splits.last().copied().unwrap_or(0);
if last_start > 0 {
if is_uuid_at(bytes, last_start) {
self.cursor.begin();
} else {
self.cursor.lose();
}
}
self.cursor.advance(on_disk_len - last_start - 1);
if splits.is_empty() {
return line;
}
let (head, chunks) = split_chunks(line, &splits);
self.split_pending.extend(chunks);
head
}
}
enum Sourced {
Line(String),
Segment,
Eof,
}
impl<I: Iterator<Item = String>> LogStream<I> {
fn next_line(&mut self) -> Sourced {
if let Some(split) = self.split_pending.pop_front() {
self.stats.lines_split += 1;
return Sourced::Line(split);
}
let Some(mut line) = self.lines.next() else {
return Sourced::Eof;
};
if line == SEGMENT_BOUNDARY {
self.last_uuid.clear();
self.last_timestamp.clear();
self.cursor.lose();
self.verdict.clear_cuts();
return Sourced::Segment;
}
self.line_number += 1;
self.stats.lines_processed += 1;
let on_disk_len = line.len() + 1;
if line.ends_with('\r') {
line.pop();
}
Sourced::Line(self.detect_collision(line, on_disk_len))
}
fn dispatch_primary(&mut self, parsed: &RawLine<'_>, line: &str) -> Option<LogEntry> {
let uuid = parsed.uuid.unwrap_or("").to_string();
if self.merge_codec_run(parsed, &uuid, line) {
return None;
}
let yielded = self.take_pending();
let timestamp = parsed
.timestamp
.map(|t| t.to_string())
.unwrap_or_else(|| self.last_timestamp.clone());
self.open_entry(parsed, uuid, timestamp);
yielded
}
fn dispatch_uuid_continuation(
&mut self,
parsed: &RawLine<'_>,
line: &str,
prev_cut: bool,
) -> Option<LogEntry> {
let uuid = parsed.uuid.unwrap_or("").to_string();
let continues = !parsed.message.starts_with("EXECUTE ")
&& self
.pending
.as_ref()
.is_some_and(|p| p.entry.uuid.as_deref() == Some(uuid.as_str()));
if continues {
self.accumulate_continuation(parsed.message, line, true, prev_cut);
return None;
}
let yielded = self.take_pending();
self.open_entry(parsed, uuid, self.last_timestamp.clone());
yielded
}
fn dispatch_bare_continuation(&mut self, parsed: &RawLine<'_>, line: &str, prev_cut: bool) {
if self.pending.is_some() {
self.accumulate_continuation(parsed.message, line, false, prev_cut);
return;
}
self.record_unclassified(UnclassifiedReason::OrphanContinuation, Some(line));
let (uuid, timestamp) = (self.last_uuid.clone(), self.last_timestamp.clone());
self.open_entry(parsed, uuid, timestamp);
}
fn dispatch_empty(&mut self, line: &str) {
if self.pending.is_some() {
self.attach(line);
} else {
self.stats.lines_empty_orphan += 1;
}
}
}
impl<I: Iterator<Item = String>> Iterator for LogStream<I> {
type Item = LogEntry;
fn next(&mut self) -> Option<LogEntry> {
loop {
let line = match self.next_line() {
Sourced::Line(line) => line,
Sourced::Segment => match self.take_pending() {
Some(entry) => return Some(entry),
None => continue,
},
Sourced::Eof => return self.take_pending(),
};
let prev_cut = self.verdict.advance();
let parsed = parse_line(&line);
let yielded = match parsed.kind {
LineKind::Full | LineKind::System | LineKind::Truncated => {
self.dispatch_primary(&parsed, &line)
}
LineKind::UuidContinuation => {
self.dispatch_uuid_continuation(&parsed, &line, prev_cut)
}
LineKind::BareContinuation => {
self.dispatch_bare_continuation(&parsed, &line, prev_cut);
None
}
LineKind::Empty => {
self.dispatch_empty(&line);
None
}
};
if yielded.is_some() {
return yielded;
}
}
}
}