use std::process;
struct Args {
url: String,
custom_message: Option<String>,
severity: String,
service: String,
api_key: Option<String>,
run_lifecycle: bool,
}
fn print_help() {
println!("Void CLI - A helper tool to send test or custom logs to Void ingestion server.");
println!();
println!("Usage:");
println!(" void-cli [options]");
println!(" void-cli test-lifecycle [options]");
println!();
println!("Commands:");
println!(" test-lifecycle Starts the server background process, runs ingestion tests,");
println!(" and then shuts the server down automatically.");
println!();
println!("Options:");
println!(" --url <URL> Ingestion server URL (default: http://127.0.0.1:9180/ingest)");
println!(" --message <MSG> Send a single log line with custom message");
println!(" --severity <SEV> Severity for custom log line (default: INFO)");
println!(" --service <SRV> Service name for custom log line (default: void-cli)");
println!(" --api-key <KEY> API key for authentication (fallback: API_KEY env var)");
println!(" --test-lifecycle Alias command to run the lifecycle test");
println!(" -h, --help Show this help message");
println!();
println!("If no custom --message is provided and not running test-lifecycle,");
println!("3 default test log lines will be sent to the configured URL using the log crate.");
}
fn parse_args() -> Result<Args, pico_args::Error> {
let mut pargs = pico_args::Arguments::from_env();
if pargs.contains(["-h", "--help"]) {
print_help();
process::exit(0);
}
let run_lifecycle = pargs.contains("--test-lifecycle")
|| match pargs.subcommand() {
Ok(Some(sub)) => sub == "test-lifecycle",
_ => false,
};
let args = Args {
url: pargs
.opt_value_from_str("--url")?
.unwrap_or_else(|| "http://127.0.0.1:9180/ingest".to_string()),
custom_message: pargs.opt_value_from_str("--message")?,
severity: pargs
.opt_value_from_str("--severity")?
.unwrap_or_else(|| "INFO".to_string()),
service: pargs
.opt_value_from_str("--service")?
.unwrap_or_else(|| "void-cli".to_string()),
api_key: pargs
.opt_value_from_str("--api-key")?
.or_else(|| std::env::var("API_KEY").ok()),
run_lifecycle,
};
let remaining = pargs.finish();
if !remaining.is_empty() {
eprintln!("Warning: Unused or unrecognized arguments: {:?}", remaining);
}
Ok(args)
}
fn main() {
let args = match parse_args() {
Ok(a) => a,
Err(e) => {
eprintln!("Error parsing arguments: {}", e);
print_help();
process::exit(1);
}
};
if args.run_lifecycle {
run_server_test_lifecycle(args.api_key);
} else {
send_logs_to_server(
args.url,
args.custom_message,
args.severity,
args.service,
args.api_key,
);
}
}
fn send_logs_to_server(
url: String,
custom_message: Option<String>,
severity: String,
service: String,
api_key: Option<String>,
) {
let severity_upper = severity.to_uppercase();
let max_level = match severity_upper.as_str() {
"TRACE" => log::LevelFilter::Trace,
"DEBUG" => log::LevelFilter::Debug,
"INFO" => log::LevelFilter::Info,
"WARN" => log::LevelFilter::Warn,
"ERROR" => log::LevelFilter::Error,
_ => log::LevelFilter::Info,
};
let mut builder = void_logger::VoidLoggerBuilder::new(&url)
.service_name(&service)
.max_level(max_level)
.batch_size(1)
.console_output(void_logger::ConsoleOutput::Split);
if let Some(ref key) = api_key {
builder = builder.api_key(key);
}
if let Err(e) = builder.init() {
eprintln!("Error: Failed to initialize VoidLogger: {}", e);
process::exit(1);
}
if let Some(msg) = custom_message {
let level = match severity_upper.as_str() {
"TRACE" => log::Level::Trace,
"DEBUG" => log::Level::Debug,
"INFO" => log::Level::Info,
"WARN" => log::Level::Warn,
"ERROR" => log::Level::Error,
_ => log::Level::Info,
};
log::log!(level, "{}", msg);
} else {
log::info!(
"FICSIT Pioneer landed. Remember: efficiency is key, and sleep is a non-essential activity."
);
log::warn!("Screws bottleneck detected. Solution: Construct 12 more Constructors.");
log::error!("Lizard Doggo ate nuclear waste. Sector 4 is now highly radioactive!");
}
log::logger().flush();
}
fn run_server_test_lifecycle(api_key: Option<String>) {
let api_key_value = api_key.unwrap_or_else(|| "test-api-key-12345".to_string());
println!("=== Starting Lifecycle Test ===");
println!("API Key for test: {}", api_key_value);
println!("Spawning server background process via cargo run --bin void...");
let mut child = match std::process::Command::new("cargo")
.args(["run", "--bin", "void"])
.env("API_KEY", &api_key_value)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(c) => c,
Err(e) => {
eprintln!("Error: Failed to spawn server process: {}", e);
process::exit(1);
}
};
println!("Waiting for server to become ready at http://127.0.0.1:9180/up...");
let start_time = std::time::Instant::now();
let mut is_ready = false;
while start_time.elapsed() < std::time::Duration::from_secs(15) {
match child.try_wait() {
Ok(Some(status)) => {
eprintln!(
"Error: Server process exited prematurely with status: {}",
status
);
process::exit(1);
}
Ok(None) => {}
Err(e) => {
eprintln!("Error checking server status: {}", e);
}
}
if let Ok(resp) = minreq::get("http://127.0.0.1:9180/up")
.with_timeout(1)
.send()
{
if resp.status_code == 200 && resp.as_str().map_or(false, |s| s == "up") {
is_ready = true;
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
if !is_ready {
eprintln!("Error: Server did not become ready within 15 seconds.");
let _ = child.kill();
process::exit(1);
}
println!("Server is up! Initializing void-logger...");
let builder = void_logger::VoidLoggerBuilder::new("http://127.0.0.1:9180/ingest")
.service_name("test-service")
.max_level(log::LevelFilter::Info)
.batch_size(1)
.console_output(void_logger::ConsoleOutput::Split)
.api_key(&api_key_value);
if let Err(e) = builder.init() {
eprintln!("Error: Failed to initialize VoidLogger: {}", e);
let _ = child.kill();
process::exit(1);
}
println!("Sending test logs through log facade...");
log::info!(
"FICSIT Pioneer landed. Remember: efficiency is key, and sleep is a non-essential activity."
);
log::warn!("Screws bottleneck detected. Solution: Construct 12 more Constructors.");
log::error!("Lizard Doggo ate nuclear waste. Sector 4 is now highly radioactive!");
println!("Flushing void-logger...");
log::logger().flush();
println!("Logs sent and flushed successfully.");
let mut test_success = true;
println!("Verifying authentication is enforced (sending without API key)...");
let unauth_response = minreq::post("http://127.0.0.1:9180/ingest")
.with_header("Content-Type", "application/json")
.with_timeout(5)
.with_body("[]")
.send();
match unauth_response {
Ok(res) => {
if res.status_code == 401 {
println!(
"Enforcement Success! Server correctly rejected request without API key (status 401)."
);
} else {
eprintln!(
"Enforcement Failure! Server returned status {} instead of 401.",
res.status_code
);
test_success = false;
}
}
Err(e) => {
eprintln!("Error during enforcement check: {}", e);
test_success = false;
}
}
println!("Killing server process...");
if let Err(e) = child.kill() {
eprintln!("Warning: Failed to kill server process: {}", e);
} else {
let _ = child.wait();
println!("Server process killed successfully.");
}
if test_success {
println!("=== Lifecycle Test PASSED ===");
} else {
println!("=== Lifecycle Test FAILED ===");
process::exit(1);
}
}