freeswitch_log_parser/stream/
mod.rs1mod block;
6mod collision;
7mod entry;
8mod stats;
9#[cfg(test)]
10mod tests;
11
12use std::collections::VecDeque;
13
14use crate::attached::AttachedLines;
15use crate::chain::SEGMENT_BOUNDARY;
16use crate::fields::FieldLocation;
17use crate::line::{
18 is_date_at, is_log_header_at, is_uuid_at, parse_line, LineKind, RawLine, UUID_PREFIX_LEN,
19};
20use crate::message::{classify_message, MessageKind};
21
22use block::BlockBuilder;
23use collision::{WriteCursor, DECODE_DRIFT};
24
25pub use entry::{Block, LogEntry, ParseWarning, SessionReading};
26pub use stats::{ParseStats, UnclassifiedLine, UnclassifiedReason, UnclassifiedTracking};
27
28const TIMESTAMP_FIELD_LEN: usize = 27;
31
32const FULL_HEADER_LEN: usize = UUID_PREFIX_LEN + TIMESTAMP_FIELD_LEN;
34
35fn header_len(bytes: &[u8]) -> usize {
38 if is_uuid_at(bytes, 0) {
39 if bytes.len() > UUID_PREFIX_LEN && bytes[UUID_PREFIX_LEN].is_ascii_digit() {
40 FULL_HEADER_LEN
41 } else {
42 UUID_PREFIX_LEN
43 }
44 } else if is_date_at(bytes, 0) {
45 TIMESTAMP_FIELD_LEN
46 } else {
47 0
48 }
49}
50
51fn scan_splits(
54 bytes: &[u8],
55 min_scan: usize,
56 first_boundary: Option<usize>,
57) -> (Vec<usize>, Vec<bool>) {
58 let end = bytes.len();
59 let mut next_boundary = first_boundary;
60 let mut splits: Vec<usize> = Vec::new();
61 let mut cut_verdicts: Vec<bool> = Vec::new();
62 let mut chunk_start = 0usize;
63 let mut offset = 0usize;
64 while offset <= end {
65 if let Some(boundary) = next_boundary {
66 if offset >= boundary && offset <= boundary + DECODE_DRIFT {
67 let uuid = is_uuid_at(bytes, offset);
68 if uuid || is_log_header_at(bytes, offset) {
69 splits.push(offset);
70 cut_verdicts.push(true);
71 chunk_start = offset;
72 next_boundary = WriteCursor::boundary_after(uuid, offset, end);
73 offset += UUID_PREFIX_LEN;
74 continue;
75 }
76 }
77 }
78 if offset >= min_scan && is_log_header_at(bytes, offset) {
81 let split_at = if offset >= chunk_start + UUID_PREFIX_LEN
82 && is_uuid_at(bytes, offset - UUID_PREFIX_LEN)
83 {
84 offset - UUID_PREFIX_LEN
85 } else {
86 offset
87 };
88 if split_at > chunk_start {
89 splits.push(split_at);
90 cut_verdicts.push(false);
91 chunk_start = split_at;
92 next_boundary =
93 WriteCursor::boundary_after(is_uuid_at(bytes, split_at), split_at, end);
94 offset += TIMESTAMP_FIELD_LEN;
95 } else {
96 offset = (offset + TIMESTAMP_FIELD_LEN).max(offset + 1);
99 }
100 continue;
101 }
102 offset += 1;
103 }
104 cut_verdicts.push(next_boundary.is_some());
105 (splits, cut_verdicts)
106}
107
108fn split_chunks(line: String, splits: &[usize]) -> (String, Vec<String>) {
111 let mut head = line;
112 let mut chunks: Vec<String> = Vec::with_capacity(splits.len());
113 for &at in splits.iter().rev() {
114 chunks.push(head.split_off(at));
115 }
116 chunks.reverse();
117 (head, chunks)
118}
119
120#[derive(Default)]
123struct LineVerdict {
124 chunks: VecDeque<bool>,
127 prev_cut: bool,
130 cut: bool,
131 warning: Option<ParseWarning>,
132}
133
134impl LineVerdict {
135 fn advance(&mut self) -> bool {
137 self.cut = self.chunks.pop_front().unwrap_or(false);
138 if self.cut {
139 self.warning = Some(ParseWarning::CutLine);
140 }
141 std::mem::replace(&mut self.prev_cut, self.cut)
142 }
143
144 fn claim(&mut self) -> Option<ParseWarning> {
146 self.warning.take()
147 }
148
149 fn clear_cuts(&mut self) {
152 self.chunks.clear();
153 self.prev_cut = false;
154 self.cut = false;
155 }
156}
157
158struct Pending {
161 entry: LogEntry,
162 block: BlockBuilder,
163}
164
165pub struct LogStream<I> {
175 lines: I,
176 last_uuid: String,
177 last_timestamp: String,
178 pending: Option<Pending>,
179 stats: ParseStats,
180 tracking: UnclassifiedTracking,
181 line_number: u64,
182 split_pending: VecDeque<String>,
183 verdict: LineVerdict,
186 cursor: WriteCursor,
188 max_attached: usize,
191}
192
193impl<I: Iterator<Item = String>> LogStream<I> {
194 pub fn new(lines: I) -> Self {
196 LogStream {
197 lines,
198 last_uuid: String::new(),
199 last_timestamp: String::new(),
200 pending: None,
201 stats: ParseStats::default(),
202 tracking: UnclassifiedTracking::CountOnly,
203 line_number: 0,
204 split_pending: VecDeque::new(),
205 verdict: LineVerdict::default(),
206 cursor: WriteCursor::default(),
207 max_attached: usize::MAX,
208 }
209 }
210
211 pub fn unclassified_tracking(mut self, level: UnclassifiedTracking) -> Self {
213 self.tracking = level;
214 self
215 }
216
217 pub fn max_attached_bytes(mut self, max_bytes: usize) -> Self {
224 self.max_attached = max_bytes;
225 self
226 }
227
228 pub fn stats(&self) -> &ParseStats {
230 &self.stats
231 }
232
233 pub fn drain_unclassified(&mut self) -> Vec<UnclassifiedLine> {
237 std::mem::take(&mut self.stats.unclassified_lines)
238 }
239
240 fn record_unclassified(&mut self, reason: UnclassifiedReason, data: Option<&str>) {
241 self.stats.lines_unclassified += 1;
242 match self.tracking {
243 UnclassifiedTracking::CountOnly => {}
244 UnclassifiedTracking::TrackLines => {
245 self.stats.unclassified_lines.push(UnclassifiedLine {
246 line_number: self.line_number,
247 reason,
248 data: None,
249 });
250 }
251 UnclassifiedTracking::CaptureData => {
252 self.stats.unclassified_lines.push(UnclassifiedLine {
253 line_number: self.line_number,
254 reason,
255 data: data.map(|s| s.to_string()),
256 });
257 }
258 }
259 }
260
261 fn take_pending(&mut self) -> Option<LogEntry> {
263 let mut pending = self.pending.take()?;
264 let (block, warnings) = pending.block.finish();
265 pending.entry.block = block;
266 pending.entry.warnings.extend(warnings);
267 self.stats.lines_in_entries += 1 + pending.entry.attached.len() as u64;
268 Some(pending.entry)
269 }
270
271 fn attach(&mut self, line: &str) {
275 let Some(pending) = self.pending.as_mut() else {
276 return;
277 };
278 let over_budget = pending.entry.attached.byte_len() + line.len() + 1 > self.max_attached;
279 if over_budget || pending.entry.attached.push(line).is_err() {
280 pending.entry.warnings.push(ParseWarning::AttachedOverflow {
281 line: ParseWarning::excerpt(line),
282 });
283 self.stats.lines_dropped += 1;
284 } else if self.verdict.cut {
285 let i = pending.entry.attached.len() - 1;
286 pending.entry.cut_texts.push(FieldLocation::Attached(i));
287 }
288 pending.entry.warnings.extend(self.verdict.claim());
289 }
290
291 fn merge_codec_run(&mut self, parsed: &RawLine<'_>, uuid: &str, line: &str) -> bool {
298 let Some(pending) = self.pending.as_mut() else {
299 return false;
300 };
301 let Some(open_media) = pending.block.codec_media() else {
302 return false;
303 };
304 if pending.entry.uuid.as_deref().unwrap_or("") != uuid {
305 return false;
306 }
307 let MessageKind::CodecNegotiation { media } = classify_message(parsed.message) else {
308 return false;
309 };
310 if media != open_media {
311 return false;
312 }
313
314 let warning = pending.block.push_codec_trace(parsed.message);
315 pending.entry.warnings.extend(warning);
316 self.attach(line);
317 true
318 }
319
320 fn accumulate_continuation(&mut self, msg: &str, line: &str, has_uuid: bool, prev_cut: bool) {
323 let Some(pending) = self.pending.as_mut() else {
324 return;
325 };
326 if prev_cut {
327 let warning = pending.block.mark_variable_cut();
328 pending.entry.warnings.extend(warning);
329 }
330 let warning = pending.block.push_continuation(msg, has_uuid);
331 pending.entry.warnings.extend(warning);
332 self.attach(line);
333 }
334
335 fn open_entry(&mut self, parsed: &RawLine<'_>, uuid: String, timestamp: String) {
343 let message_kind = classify_message(parsed.message);
344
345 if !uuid.is_empty() {
346 self.last_uuid = uuid.clone();
347 }
348 if parsed.timestamp.is_some() {
349 self.last_timestamp = timestamp.clone();
350 }
351
352 let mut block = BlockBuilder::open(&message_kind);
353 let opening_warning = block.push_codec_trace(parsed.message);
356
357 let entry = LogEntry {
358 uuid: if uuid.is_empty() { None } else { Some(uuid) },
359 timestamp,
360 message: parsed.message.to_string(),
361 kind: parsed.kind,
362 message_kind,
363 level: parsed.level,
364 idle_pct: parsed.idle_pct.map(|s| s.to_string()),
365 source: parsed.source.map(|s| s.to_string()),
366 block: None,
367 attached: AttachedLines::new(),
368 line_number: self.line_number,
369 warnings: self
370 .verdict
371 .claim()
372 .into_iter()
373 .chain(opening_warning)
374 .collect(),
375 cut_texts: if self.verdict.cut {
376 vec![FieldLocation::Message]
377 } else {
378 Vec::new()
379 },
380 };
381 self.pending = Some(Pending { entry, block });
382 }
383}
384
385impl<I: Iterator<Item = String>> LogStream<I> {
386 fn detect_collision(&mut self, line: String, on_disk_len: usize) -> String {
392 let bytes = line.as_bytes();
393 let prepended = is_uuid_at(bytes, 0);
394
395 if prepended {
398 self.cursor.begin();
399 } else if bytes.is_empty() || is_date_at(bytes, 0) {
400 self.cursor.lose();
401 }
402
403 let boundary = self.cursor.boundary_in(bytes.len());
404 let (splits, cut_verdicts) = scan_splits(bytes, header_len(bytes), boundary);
405 self.verdict.chunks = cut_verdicts.into();
406
407 let last_start = splits.last().copied().unwrap_or(0);
409 if last_start > 0 {
410 if is_uuid_at(bytes, last_start) {
411 self.cursor.begin();
412 } else {
413 self.cursor.lose();
414 }
415 }
416 self.cursor.advance(on_disk_len - last_start - 1);
419
420 if splits.is_empty() {
421 return line;
422 }
423
424 let (head, chunks) = split_chunks(line, &splits);
425 self.split_pending.extend(chunks);
426 head
427 }
428}
429
430enum Sourced {
432 Line(String),
433 Segment,
436 Eof,
437}
438
439impl<I: Iterator<Item = String>> LogStream<I> {
440 fn next_line(&mut self) -> Sourced {
443 if let Some(split) = self.split_pending.pop_front() {
444 self.stats.lines_split += 1;
445 return Sourced::Line(split);
446 }
447
448 let Some(mut line) = self.lines.next() else {
449 return Sourced::Eof;
450 };
451
452 if line == SEGMENT_BOUNDARY {
455 self.last_uuid.clear();
456 self.last_timestamp.clear();
457 self.cursor.lose();
460 self.verdict.clear_cuts();
461 return Sourced::Segment;
462 }
463
464 self.line_number += 1;
465 self.stats.lines_processed += 1;
466
467 let on_disk_len = line.len() + 1;
470 if line.ends_with('\r') {
471 line.pop();
472 }
473 Sourced::Line(self.detect_collision(line, on_disk_len))
474 }
475
476 fn dispatch_primary(&mut self, parsed: &RawLine<'_>, line: &str) -> Option<LogEntry> {
479 let uuid = parsed.uuid.unwrap_or("").to_string();
480 if self.merge_codec_run(parsed, &uuid, line) {
481 return None;
482 }
483
484 let yielded = self.take_pending();
485 let timestamp = parsed
486 .timestamp
487 .map(|t| t.to_string())
488 .unwrap_or_else(|| self.last_timestamp.clone());
489 self.open_entry(parsed, uuid, timestamp);
490 yielded
491 }
492
493 fn dispatch_uuid_continuation(
494 &mut self,
495 parsed: &RawLine<'_>,
496 line: &str,
497 prev_cut: bool,
498 ) -> Option<LogEntry> {
499 let uuid = parsed.uuid.unwrap_or("").to_string();
500 let continues = !parsed.message.starts_with("EXECUTE ")
503 && self
504 .pending
505 .as_ref()
506 .is_some_and(|p| p.entry.uuid.as_deref() == Some(uuid.as_str()));
507
508 if continues {
509 self.accumulate_continuation(parsed.message, line, true, prev_cut);
510 return None;
511 }
512 let yielded = self.take_pending();
513 self.open_entry(parsed, uuid, self.last_timestamp.clone());
514 yielded
515 }
516
517 fn dispatch_bare_continuation(&mut self, parsed: &RawLine<'_>, line: &str, prev_cut: bool) {
520 if self.pending.is_some() {
521 self.accumulate_continuation(parsed.message, line, false, prev_cut);
522 return;
523 }
524 self.record_unclassified(UnclassifiedReason::OrphanContinuation, Some(line));
525 let (uuid, timestamp) = (self.last_uuid.clone(), self.last_timestamp.clone());
526 self.open_entry(parsed, uuid, timestamp);
527 }
528
529 fn dispatch_empty(&mut self, line: &str) {
530 if self.pending.is_some() {
531 self.attach(line);
532 } else {
533 self.stats.lines_empty_orphan += 1;
534 }
535 }
536}
537
538impl<I: Iterator<Item = String>> Iterator for LogStream<I> {
539 type Item = LogEntry;
540
541 fn next(&mut self) -> Option<LogEntry> {
542 loop {
543 let line = match self.next_line() {
544 Sourced::Line(line) => line,
545 Sourced::Segment => match self.take_pending() {
546 Some(entry) => return Some(entry),
547 None => continue,
548 },
549 Sourced::Eof => return self.take_pending(),
550 };
551
552 let prev_cut = self.verdict.advance();
553 let parsed = parse_line(&line);
554
555 let yielded = match parsed.kind {
556 LineKind::Full | LineKind::System | LineKind::Truncated => {
557 self.dispatch_primary(&parsed, &line)
558 }
559 LineKind::UuidContinuation => {
560 self.dispatch_uuid_continuation(&parsed, &line, prev_cut)
561 }
562 LineKind::BareContinuation => {
563 self.dispatch_bare_continuation(&parsed, &line, prev_cut);
564 None
565 }
566 LineKind::Empty => {
567 self.dispatch_empty(&line);
568 None
569 }
570 };
571
572 if yielded.is_some() {
573 return yielded;
574 }
575 }
576 }
577}