Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pup <domain> <subgroup> <action> [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 | ✅ |
Expand Down Expand Up @@ -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"
```

Expand Down
107 changes: 102 additions & 5 deletions src/commands/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i64>) -> 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::*;
Expand Down Expand Up @@ -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();
}
}
41 changes: 39 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["<org-uuid>"]}],
/// "to": 1700003600000
/// },
/// "type": "timeseries_request"
/// }
/// }
Timeseries {
#[arg(long, help = "JSON file with TimeseriesFormulaQueryRequest body")]
file: String,
},
}

#[derive(Subcommand)]
Expand All @@ -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<i64>,
},
}

Expand Down Expand Up @@ -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 ---
Expand Down
Loading