diff --git a/src/main.rs b/src/main.rs index c1d2081..480f9ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,7 +146,7 @@ enum Command { #[arg(long)] until: Option, - /// JSON array of metric names to include + /// JSON array or comma-separated list of metric names to include /// /// Available metrics: requests, requests_per_minute, bytes_received, /// bytes_sent, client_errors, server_errors, error_rate, @@ -161,7 +161,7 @@ enum Command { #[arg(long)] interval: Option, - /// JSON array of field names to group by (in addition to time interval) + /// JSON array or comma-separated list of field names to group by (in addition to time interval) /// /// Available fields: env, consumer_id, method, path, status_code. #[arg(long)] @@ -213,7 +213,7 @@ enum Command { #[arg(long)] until: Option, - /// JSON array of field names to include + /// JSON array or comma-separated list of field names to include /// /// Available fields: timestamp, request_uuid, env, method, path, /// url, consumer_id, request_headers, request_size_bytes, request_body_json, diff --git a/src/metrics.rs b/src/metrics.rs index f74d9fd..f7c6507 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -6,7 +6,7 @@ use duckdb::arrow::ipc::reader::StreamReader; use duckdb::vtab::arrow::{ArrowVTab, arrow_recordbatch_to_query_params}; use crate::auth::{resolve_api_base_url, resolve_api_key}; -use crate::utils::{api_post, input_err, open_db, resolve_relative_datetime}; +use crate::utils::{api_post, input_err, open_db, parse_string_list, resolve_relative_datetime}; pub(crate) fn ensure_metrics_table(conn: &duckdb::Connection) -> Result<()> { conn.execute_batch( @@ -55,13 +55,11 @@ pub fn run( let since = resolve_relative_datetime(since); let until = until.map(resolve_relative_datetime); - let metrics_value: serde_json::Value = serde_json::from_str(metrics) - .map_err(|e| input_err(format!("invalid JSON for --metrics: {e}")))?; let format = if db.is_some() { "arrow" } else { "ndjson" }; let mut body = serde_json::json!({ "format": format, "since": since, - "metrics": metrics_value, + "metrics": parse_string_list(metrics).map_err(|e| input_err(format!("invalid JSON for --metrics: {e}")))?, }); if let Some(ref until) = until { body["until"] = serde_json::json!(until); @@ -70,9 +68,8 @@ pub fn run( body["interval"] = serde_json::json!(interval); } if let Some(group_by) = group_by { - let group_by_value: serde_json::Value = serde_json::from_str(group_by) + body["group_by"] = parse_string_list(group_by) .map_err(|e| input_err(format!("invalid JSON for --group-by: {e}")))?; - body["group_by"] = group_by_value; } if let Some(filters) = filters { let filters_value: serde_json::Value = serde_json::from_str(filters) diff --git a/src/request_logs.rs b/src/request_logs.rs index ae5dcac..a817c15 100644 --- a/src/request_logs.rs +++ b/src/request_logs.rs @@ -6,7 +6,7 @@ use duckdb::arrow::ipc::reader::StreamReader; use duckdb::vtab::arrow::{ArrowVTab, arrow_recordbatch_to_query_params}; use crate::auth::{resolve_api_base_url, resolve_api_key}; -use crate::utils::{api_post, input_err, open_db, resolve_relative_datetime}; +use crate::utils::{api_post, input_err, open_db, parse_string_list, resolve_relative_datetime}; pub(crate) fn ensure_request_logs_table(conn: &duckdb::Connection) -> Result<()> { conn.execute_batch( @@ -69,9 +69,8 @@ pub fn run( body["until"] = serde_json::json!(until); } if let Some(fields) = fields { - let fields_value: serde_json::Value = serde_json::from_str(fields) + body["fields"] = parse_string_list(fields) .map_err(|e| input_err(format!("invalid JSON for --fields: {e}")))?; - body["fields"] = fields_value; } if let Some(filters) = filters { let filters_value: serde_json::Value = serde_json::from_str(filters) diff --git a/src/utils.rs b/src/utils.rs index d8cfc34..0e74363 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -96,6 +96,20 @@ pub fn resolve_relative_datetime(s: &str) -> String { dt.to_rfc3339() } +/// Parses a string as either a JSON array or a comma-separated list of strings. +pub fn parse_string_list(s: &str) -> Result { + let s = s.trim_start(); + if s.starts_with('[') { + return serde_json::from_str::(s).map_err(Into::into); + } + let items: Vec<&str> = s + .split(',') + .map(|item| item.trim()) + .filter(|item| !item.is_empty()) + .collect(); + Ok(serde_json::json!(items)) +} + pub fn api_get(url: &str, api_key: &str, query: &[(&str, &str)]) -> Result> { let mut req = ureq::get(url) .header("Api-Key", api_key) @@ -185,6 +199,31 @@ mod tests { assert_approximately_now_minus(&resolve_relative_datetime("1w"), 604_800); } + #[test] + fn test_parse_string_list() { + assert_eq!( + parse_string_list(r#"["requests","error_rate"]"#).unwrap(), + serde_json::json!(["requests", "error_rate"]) + ); + assert_eq!( + parse_string_list("requests,error_rate").unwrap(), + serde_json::json!(["requests", "error_rate"]) + ); + assert_eq!( + parse_string_list("requests").unwrap(), + serde_json::json!(["requests"]) + ); + assert_eq!( + parse_string_list("requests , error_rate").unwrap(), + serde_json::json!(["requests", "error_rate"]) + ); + assert_eq!( + parse_string_list("requests,,error_rate").unwrap(), + serde_json::json!(["requests", "error_rate"]) + ); + assert!(parse_string_list("[foo]").is_err()); + } + #[test] fn test_check_response() { assert!(check_response(&mut resp(200)).is_ok());