use crate::timestamps::{is_full_iso, normalize_iso};
use crate::Args;
use bloomfilter::Bloom;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::io::{self, Write};
pub(crate) fn on_write_err(e: io::Error, what: &str) {
if e.kind() == io::ErrorKind::BrokenPipe {
std::process::exit(0);
}
eprintln!("Error {}: {}", what, e);
}
pub(crate) fn emit<W: Write>(w: &mut W, text: &str) {
if let Err(e) = writeln!(w, "{}", text) {
on_write_err(e, "writing output");
std::process::exit(1);
}
}
pub(crate) type Sink<'w> = io::BufWriter<&'w mut io::StdoutLock<'static>>;
pub(crate) fn flush(outbuf: &mut Sink) {
if let Err(e) = outbuf.flush() {
on_write_err(e, "flushing output");
std::process::exit(1);
}
}
#[derive(Serialize, Deserialize)]
pub(crate) struct LogEntry {
index: usize,
content: String,
}
pub(crate) struct Pipeline {
pub(crate) source: crate::source::Source,
pub(crate) re_timestamp: Regex,
pub(crate) re_trim: Regex,
pub(crate) query: Option<String>,
pub(crate) since_valid: Option<String>,
pub(crate) until_valid: Option<String>,
pub(crate) range_filter: bool,
pub(crate) child_tool: Option<&'static str>,
pub(crate) verbatim: bool,
pub(crate) max_field_len: Option<usize>,
pub(crate) ts_norm: String,
pub(crate) warned_non_iso: bool,
pub(crate) read_error: Option<io::Error>,
pub(crate) early_cut: bool,
pub(crate) empty_entries: usize,
pub(crate) line_no: usize,
pub(crate) raw_buf: Vec<u8>,
}
impl Pipeline {
pub(crate) fn next_trimmed(&mut self) -> Option<(usize, String)> {
loop {
let line_cow = match &mut self.source {
crate::source::Source::Lines(r) => {
self.raw_buf.clear();
match r.read_until(b'\n', &mut self.raw_buf) {
Ok(0) => return None,
Ok(_) => {}
Err(e) => {
self.read_error = Some(e);
return None;
}
}
self.line_no += 1;
String::from_utf8_lossy(&self.raw_buf)
}
crate::source::Source::Rows(r) => match r.byte_records().next() {
Some(Ok(rec)) => {
self.line_no += 1;
let mut line = String::new();
for (i, f) in rec.iter().enumerate() {
if i > 0 { line.push('\t'); }
line.push_str(&String::from_utf8_lossy(f));
}
if line.contains(['\n', '\r']) {
line = line.replace("\r\n", " ");
line = line.replace('\n', " ");
}
std::borrow::Cow::Owned(line)
}
None => return None,
Some(Err(e)) => {
self.line_no += 1;
self.read_error = Some(e.into());
return None;
}
},
};
let line = line_cow.trim_end_matches(['\n', '\r']);
if let Some(q) = &self.query
&& !line.contains(q) { continue; }
if self.range_filter {
let lead = self.re_timestamp.find_iter(line)
.find(|m| is_full_iso(m.as_str()))
.map(|m| m.as_str());
if let Some(ts) = lead {
let ts = normalize_iso(ts, &mut self.ts_norm);
if let Some(s) = &self.since_valid
&& ts < s.as_str() { continue; }
if let Some(u) = &self.until_valid
&& ts >= u.as_str() { continue; }
} else if !self.warned_non_iso && self.re_timestamp.is_match(line) {
self.warned_non_iso = true;
eprintln!("Warning: --since/--until needs a leading ISO (YYYY-...) timestamp; lines with non-ISO leading timestamps bypass the range filter.");
}
}
if self.child_tool == Some("bq") && line.starts_with("Waiting on bqjob") {
continue;
}
if self.verbatim {
return Some((self.line_no, line.to_string()));
}
let trimmed = self.re_trim.replace_all(line, "");
let final_line = trimmed.trim();
if final_line.is_empty() {
if self.child_tool.is_some() { self.empty_entries += 1; }
continue;
}
return Some((self.line_no, final_line.to_string()));
}
}
pub(crate) fn clamp_fields(&self, line: &str) -> String {
let Some(n) = self.max_field_len else {
return line.to_string();
};
if line.contains('\t') {
line.split('\t')
.map(|f| clamp_one(f, n))
.collect::<Vec<_>>()
.join("\t")
} else {
clamp_one(line, n)
}
}
}
fn clamp_one(field: &str, n: usize) -> String {
if field.chars().count() <= n {
return field.to_string();
}
let mut out: String = field.chars().take(n).collect();
out.push('…');
out
}
pub(crate) fn run_stream(
proc: &mut Pipeline,
mut bloom: Option<&mut Bloom<String>>,
args: &Args,
outbuf: &mut Sink,
) -> (usize, usize, bool) {
let mut dedup_inserts = 0usize;
let mut dropped_dups = 0usize;
let window_n = args.tail.or(args.max_lines);
let mut tail_buf: VecDeque<(usize, String)> =
window_n.map(VecDeque::with_capacity).unwrap_or_default();
let mut window_full = false;
while let Some((line_no, line)) = proc.next_trimmed() {
if let Some(ref mut b) = bloom {
if b.check_and_set(&line) {
dropped_dups += 1;
continue;
}
dedup_inserts += 1;
}
let line = proc.clamp_fields(&line);
if let Some(n) = window_n {
if tail_buf.len() >= n {
tail_buf.pop_front();
window_full = true;
}
tail_buf.push_back((line_no, line));
continue;
}
if args.json {
let json = serde_json::to_string(&LogEntry { index: line_no, content: line }).unwrap();
emit(outbuf, &json);
} else {
emit(outbuf, &line);
}
}
for (line_no, line) in tail_buf {
if args.json {
let json = serde_json::to_string(&LogEntry { index: line_no, content: line }).unwrap();
emit(outbuf, &json);
} else {
emit(outbuf, &line);
}
}
(dedup_inserts, dropped_dups, window_full)
}