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 spawn = |program: &str| {
let mut cmd = Command::new(program);
cmd.args(&argv).stdout(Stdio::piped()).stderr(Stdio::inherit());
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
cmd.spawn()
};
let mut child = match spawn(tool) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
#[cfg(windows)]
let retried = [".cmd", ".bat"].iter().find_map(|ext| spawn(&format!("{tool}{ext}")).ok());
#[cfg(not(windows))]
let retried: Option<Child> = None;
let Some(child) = retried else {
eprintln!("Failed to execute {}: {}. Is the {} CLI installed?", tool, e, tool);
exit(1);
};
child
}
Err(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)
}
fn builtin_bytes_guard() -> Option<u64> {
let spec = std::env::var("CARDIO_MAX_BYTES").ok()?;
let spec = spec.trim();
if spec.is_empty() || spec == "0" {
return None;
}
match parse_byte_size(spec) {
Some(bytes) => Some(bytes),
None => {
eprintln!("Warning: CARDIO_MAX_BYTES={spec:?} is not a size (use like 100GB or 500MB) - this run is NOT bytes-capped.");
None
}
}
}
fn parse_byte_size(spec: &str) -> Option<u64> {
let (digits, mult) = match spec.chars().last()?.to_ascii_lowercase() {
'b' => match spec[..spec.len()-1].trim_end().chars().last()?.to_ascii_lowercase() {
'k' => (&spec[..spec.len()-2], 1024u64),
'm' => (&spec[..spec.len()-2], 1024u64 * 1024),
'g' => (&spec[..spec.len()-2], 1024u64 * 1024 * 1024),
't' => (&spec[..spec.len()-2], 1024u64 * 1024 * 1024 * 1024),
_ => (spec, 1),
},
'k' => (&spec[..spec.len()-1], 1024u64),
'm' => (&spec[..spec.len()-1], 1024u64 * 1024),
'g' => (&spec[..spec.len()-1], 1024u64 * 1024 * 1024),
't' => (&spec[..spec.len()-1], 1024u64 * 1024 * 1024 * 1024),
d if d.is_ascii_digit() => (spec, 1),
_ => return None,
};
parse_digits(digits)?.checked_mul(mult)
}
fn parse_digits(s: &str) -> Option<u64> {
s.trim().parse::<u64>().ok()
}
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 spawn_watchdog(child: &std::sync::Arc<std::sync::Mutex<Child>>, secs: u64) {
if secs == 0 {
return;
}
let watchdog = std::sync::Arc::clone(child);
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(secs));
let Ok(mut c) = watchdog.lock() else { return };
match c.try_wait() {
Ok(Some(_)) | Err(_) => {}
Ok(None) => {
kill_fetcher(&mut c);
eprintln!("Error: fetcher killed after --timeout {}s (its job id is visible in the output above).", secs);
}
}
});
}
pub(crate) fn collect_fetcher(
child: &std::sync::Arc<std::sync::Mutex<Child>>,
tool: Option<&'static str>,
early_cut: bool,
) -> Result<(), crate::errors::Fail> {
if early_cut {
let mut g = child.lock().unwrap();
kill_fetcher(&mut g);
}
let status;
loop {
let probed = { child.lock().unwrap().try_wait() };
match probed {
Ok(Some(s)) => { status = Some(s); break; }
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(e) => {
eprintln!("Warning: failed to wait for {}: {}", tool.unwrap_or("fetcher"), e);
status = None;
break;
}
}
}
let Some(status) = status else { return Ok(()) };
if !status.success() && !early_cut {
return Err(crate::errors::Fail::Fetch(format!(
"{} exited with {}", tool.unwrap_or("fetcher"), status)));
}
Ok(())
}
pub(crate) type ReaderSetup = (
Source,
Option<(&'static str, Child)>,
Option<Vec<String>>,
);
pub(crate) fn make_reader(
args: &Args,
since_valid: &Option<String>,
until_valid: &Option<String>,
) -> ReaderSetup {
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)), None)
} 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}"));
} else if let Some(bytes) = builtin_bytes_guard() {
bq_args.push(format!("--maximum_bytes_billed={bytes}"));
}
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);
let headers: Option<Vec<String>> = rows.headers().ok().map(|h| {
h.iter().map(|s| s.to_string()).collect::<Vec<_>>()
});
if let Some(h) = &headers {
eprintln!("Note: bq columns: {}", h.join(" | "));
}
(Source::Rows(rows), Some(("bq", child)), headers)
} else if let Some(selector) = &args.k8s {
if since_valid.is_none() {
eprintln!("Warning: no --since — this kubectl logs call streams from pod creation. Add --since unless you want everything.");
}
let mut k8s_args: Vec<String> = vec!["logs".into(), selector.clone()];
k8s_args.push("--timestamps".into());
if let Some(c) = &args.k8s_container {
k8s_args.push("-c".into());
k8s_args.push(c.clone());
}
if let Some(s) = since_valid {
k8s_args.push(format!("--since-time={}Z", server_ts(s)));
}
let (stdout, child) = spawn_fetcher("kubectl", k8s_args);
(Source::Lines(Box::new(BufReader::new(stdout))), Some(("kubectl", child)), None)
} 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, None)
} else {
(Source::Lines(Box::new(BufReader::new(io::stdin()))), None, None)
}
}