diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 0948138..f5f45f7 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -23,7 +23,7 @@ pup [options] # Nested commands |--------|-------------|------|--------| | acp | serve | src/commands/acp.rs | ✅ | | auth | login, logout, status, refresh | src/commands/auth.rs | ✅ | -| metrics | query, list, get, search | src/commands/metrics.rs | ✅ | +| metrics | query, list, search, timeseries, metadata, tags, submit | src/commands/metrics.rs | ✅ | | logs | search, list, aggregate | src/commands/logs.rs | ✅ | | traces | metrics (list, get, create, update, delete) | src/commands/traces.rs | ✅ | | monitors | list, get, delete, search | src/commands/monitors.rs | ✅ | @@ -113,6 +113,8 @@ pup logs search --query="service:api" --from="7d" --storage="flex" pup dbm samples search --query="dbm_type:activity service:orders env:prod" --from="1h" --limit=10 pup metrics search --query="avg:system.cpu.user{*}" --from="1h" pup metrics query --query="avg:system.cpu.user{*}" --from="1h" +pup metrics tags list system.cpu.user --window-seconds=3600 +pup metrics timeseries --file=request.json pup events search --query="@user.id:12345" ``` diff --git a/src/commands/metrics.rs b/src/commands/metrics.rs index d682c7f..1ba836c 100644 --- a/src/commands/metrics.rs +++ b/src/commands/metrics.rs @@ -136,20 +136,39 @@ pub async fn submit(cfg: &Config, file: &str) -> Result<()> { formatter::output(cfg, &resp) } -pub async fn tags_list(cfg: &Config, metric_name: &str) -> Result<()> { +pub async fn tags_list(cfg: &Config, metric_name: &str, window_seconds: Option) -> Result<()> { use datadog_api_client::datadogV2::api_metrics::ListTagsByMetricNameOptionalParams; let api = crate::make_api!(MetricsV2API, cfg); + let mut params = ListTagsByMetricNameOptionalParams::default(); + if let Some(w) = window_seconds { + params = params.window_seconds(w); + } let resp = api - .list_tags_by_metric_name( - metric_name.to_string(), - ListTagsByMetricNameOptionalParams::default(), - ) + .list_tags_by_metric_name(metric_name.to_string(), params) .await .map_err(|e| anyhow::anyhow!("failed to list tags for metric {metric_name}: {e:?}"))?; formatter::output(cfg, &resp) } +/// Query timeseries data using the v2 metrics API. +/// +/// The request body is provided as a JSON file matching the +/// `TimeseriesFormulaQueryRequest` schema. Use `cross_org_uuids` inside the +/// individual query objects in that file to enable cross-organisation queries +/// (SDK PR #1564). +pub async fn query_timeseries(cfg: &Config, file: &str) -> Result<()> { + use datadog_api_client::datadogV2::model::TimeseriesFormulaQueryRequest; + + let api = crate::make_api!(MetricsV2API, cfg); + let body: TimeseriesFormulaQueryRequest = util::read_json_file(file)?; + let resp = api + .query_timeseries_data(body) + .await + .map_err(|e| anyhow::anyhow!("failed to query timeseries data: {e:?}"))?; + formatter::output(cfg, &resp) +} + #[cfg(test)] mod tests { use crate::test_support::*; @@ -255,4 +274,82 @@ mod tests { ); cleanup_env(); } + + #[tokio::test] + async fn test_metrics_tags_list_no_window() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data": null}"#).await; + + let result = super::tags_list(&cfg, "system.cpu.user", None).await; + assert!( + result.is_ok(), + "metrics tags list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_tags_list_with_window() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data": null}"#).await; + + let result = super::tags_list(&cfg, "system.cpu.user", Some(3600)).await; + assert!( + result.is_ok(), + "metrics tags list with window_seconds failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_query_timeseries() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any( + &mut server, + "POST", + r#"{"data": {"type": "timeseries_response", "attributes": {"times": [], "values": [], "series": []}}}"#, + ) + .await; + + let body = r#"{ + "data": { + "attributes": { + "formulas": [{"formula": "a"}], + "from": 0, + "interval": 5000, + "queries": [{"data_source": "metrics", "query": "avg:system.cpu.user{*}", "name": "a"}], + "to": 3600000 + }, + "type": "timeseries_request" + } + }"#; + let tmp = write_temp_json("timeseries_test.json", body); + let result = super::query_timeseries(&cfg, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "metrics query timeseries failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_query_timeseries_bad_file() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _ = mock_any(&mut server, "POST", r#"{}"#).await; + + let result = super::query_timeseries(&cfg, "/nonexistent/path/request.json").await; + assert!(result.is_err(), "expected error for missing file, got ok"); + cleanup_env(); + } } diff --git a/src/main.rs b/src/main.rs index 8fbdede..21b5beb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3425,6 +3425,31 @@ enum MetricActions { #[command(subcommand)] action: MetricTagActions, }, + /// Query v2 timeseries data via TimeseriesFormulaQueryRequest JSON body (SDK PR #1564) + /// + /// Provide a JSON file matching the TimeseriesFormulaQueryRequest schema. + /// To query across organisations, include `cross_org_uuids` inside each + /// query object in the file (SDK PR #1564 added this field to + /// MetricsTimeseriesQuery and MetricsScalarQuery). + /// + /// Example body (timeseries_request.json): + /// { + /// "data": { + /// "attributes": { + /// "formulas": [{"formula": "a"}], + /// "from": 1700000000000, + /// "interval": 5000, + /// "queries": [{"data_source": "metrics", "query": "avg:system.cpu.user{*}", "name": "a", + /// "cross_org_uuids": [""]}], + /// "to": 1700003600000 + /// }, + /// "type": "timeseries_request" + /// } + /// } + Timeseries { + #[arg(long, help = "JSON file with TimeseriesFormulaQueryRequest body")] + file: String, + }, } #[derive(Subcommand)] @@ -3436,6 +3461,11 @@ enum MetricTagActions { from: String, #[arg(long, default_value = "now", help = "End time")] to: String, + #[arg( + long, + help = "Lookback window in seconds (SDK PR #1593; default 3600, max 2592000)" + )] + window_seconds: Option, }, } @@ -11189,10 +11219,17 @@ async fn main_inner() -> anyhow::Result<()> { } }, MetricActions::Tags { action } => match action { - MetricTagActions::List { metric_name, .. } => { - commands::metrics::tags_list(&cfg, &metric_name).await?; + MetricTagActions::List { + metric_name, + window_seconds, + .. + } => { + commands::metrics::tags_list(&cfg, &metric_name, window_seconds).await?; } }, + MetricActions::Timeseries { file } => { + commands::metrics::query_timeseries(&cfg, &file).await?; + } } } // --- SLOs ---