use crate::timestamps::{until_server_bound, server_ts};
use crate::Args;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::process::{Child, ChildStdout, Command, Stdio, exit};
pub(crate) enum Source {
Lines(Box<dyn BufRead>),
Rows(csv::Reader<ChildStdout>),
}
pub(crate) fn spawn_fetcher(tool: &'static str, argv: Vec<String>) -> (ChildStdout, Child) {
let mut cmd = Command::new(tool);
cmd.args(&argv)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
let mut child = cmd.spawn().unwrap_or_else(|e| {
eprintln!("Failed to execute {}: {}. Is the {} CLI installed?", tool, e, tool);
exit(1);
});
let stdout: ChildStdout = child.stdout.take().expect("child stdout");
(stdout, child)
}
pub(crate) fn kill_fetcher(child: &mut Child) {
let pid = child.id();
#[cfg(unix)]
let _ = Command::new("kill").arg("-9").arg(format!("-{pid}")).status();
let _ = child.kill();
}
pub(crate) fn make_reader(
args: &Args,
since_valid: &Option<String>,
until_valid: &Option<String>,
) -> (Source, Option<(&'static str, Child)>) {
if let Some(filter) = &args.gcloud {
if since_valid.is_none() {
eprintln!("Warning: no --since — this gcloud call streams the WHOLE matching range, oldest first. Add --since unless you truly want everything.");
}
let mut gc_filter = format!("({})", filter);
if let Some(s) = since_valid {
gc_filter.push_str(&format!(" AND timestamp>=\"{}\"", server_ts(s)));
}
if let Some(next) = until_valid.as_ref().and_then(|u| until_server_bound(u)) {
gc_filter.push_str(&format!(" AND timestamp<\"{}\"", next));
}
let gc_args = vec![
"logging".to_string(), "read".to_string(), gc_filter,
"--format".into(), "value(timestamp,textPayload,jsonPayload.message)".into(),
"--order=asc".into(),
];
let (stdout, child) = spawn_fetcher("gcloud", gc_args);
(Source::Lines(Box::new(BufReader::new(stdout))), Some(("gcloud", child)))
} else if let Some(sql) = &args.bq {
let mut bq_args: Vec<String> = Vec::new();
if let Some(p) = &args.bq_project {
bq_args.push(format!("--project_id={p}"));
}
bq_args.push("--quiet".to_string());
bq_args.push("query".to_string());
bq_args.push("--use_legacy_sql=false".to_string());
bq_args.push("--format=csv".to_string());
bq_args.push("--max_rows=10000000".to_string());
if args.dry_run {
bq_args.push("--dry_run".to_string());
}
if let Some(mb) = args.max_bytes_billed {
bq_args.push(format!("--maximum_bytes_billed={mb}"));
}
if let Some(extra) = &args.bq_args {
bq_args.extend(extra.split_whitespace().map(str::to_string));
}
bq_args.push(sql.clone());
let (stdout, child) = spawn_fetcher("bq", bq_args);
let mut rows = csv::ReaderBuilder::new().flexible(true).from_reader(stdout);
if let Ok(h) = rows.headers() {
eprintln!("Note: bq columns: {}", h.iter().collect::<Vec<_>>().join(" | "));
}
(Source::Rows(rows), Some(("bq", child)))
} else if let Some(path) = &args.file_path {
let file = File::open(path).unwrap_or_else(|e| {
eprintln!("Error opening file {}: {}", path, e);
exit(1);
});
(Source::Lines(Box::new(BufReader::new(file))), None)
} else {
(Source::Lines(Box::new(BufReader::new(io::stdin()))), None)
}
}