From 8206a60338172138cee4f3ee6f43cacafce4746f Mon Sep 17 00:00:00 2001 From: datadog-bits <263423550+datadog-bits@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:18:27 +0000 Subject: [PATCH] Add logs saved views commands --- docs/COMMANDS.md | 5 +- src/commands/logs.rs | 130 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 43 +++++++++++++- src/test_commands.rs | 28 ++++++++++ 4 files changed, 203 insertions(+), 3 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index b70acfaa..748cc818 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -24,7 +24,7 @@ pup [options] # Nested commands | acp | serve | src/commands/acp.rs | ✅ | | auth | login, logout, status, refresh | src/commands/auth.rs | ✅ | | metrics | query, list, search, timeseries, metadata, tags, submit | src/commands/metrics.rs | ✅ | -| logs | search, list, aggregate | src/commands/logs.rs | ✅ | +| logs | search, list, aggregate, saved-views (list, get, create, delete) | src/commands/logs.rs | ✅ | | traces | metrics (list, get, create, update, delete) | src/commands/traces.rs | ✅ | | monitors | list, get, create, update, delete, search, diff | src/commands/monitors.rs | ✅ | | dashboards | list, get, delete, url, annotations (list, get-page, create, update, delete) | src/commands/dashboards.rs, src/commands/annotations.rs | ✅ | @@ -112,6 +112,7 @@ pup slos get abc-123-def pup logs search --query="status:error" --from="1h" pup logs search --query="service:api" --from="7d" --storage="flex" pup logs query --query="service:api" --index="main,security" --from="1h" +pup logs saved-views create --file=saved-view.json 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" @@ -141,7 +142,7 @@ pup infrastructure hosts list ### Data & Observability - **metrics** - Time-series metrics (query, list, get, search) -- **logs** - Log search and analysis (search, list, aggregate) +- **logs** - Log search and analysis (search, list, aggregate, saved views) - **dbm** - Database Monitoring query samples (samples search) - **traces** - APM spans metrics (list, get, create, update, delete) - **rum** - Real User Monitoring (apps, metrics, retention-filters, sessions) diff --git a/src/commands/logs.rs b/src/commands/logs.rs index c23c87c7..180a97ba 100644 --- a/src/commands/logs.rs +++ b/src/commands/logs.rs @@ -10,8 +10,11 @@ use datadog_api_client::datadogV2::model::{ use crate::config::Config; use crate::formatter; use crate::raw_client; +use crate::util; use crate::util_ext; +const SAVED_VIEWS_PATH: &str = "/api/v2/logs/config/saved_views"; + pub struct AggregateArgs { pub query: String, pub from: String, @@ -404,6 +407,34 @@ pub async fn restriction_queries_get(cfg: &Config, query_id: &str) -> Result<()> formatter::output(cfg, &data) } +// --------------------------------------------------------------------------- +// Saved Views (raw HTTP - not available in typed client) +// --------------------------------------------------------------------------- + +pub async fn saved_views_list(cfg: &Config) -> Result<()> { + let data = raw_client::raw_get(cfg, SAVED_VIEWS_PATH, &[]).await?; + formatter::output(cfg, &data) +} + +pub async fn saved_views_get(cfg: &Config, view_id: &str) -> Result<()> { + let path = format!("{SAVED_VIEWS_PATH}/{view_id}"); + let data = raw_client::raw_get(cfg, &path, &[]).await?; + formatter::output(cfg, &data) +} + +pub async fn saved_views_create(cfg: &Config, file: &str) -> Result<()> { + let body: serde_json::Value = util::read_json_file(file)?; + let data = raw_client::raw_post(cfg, SAVED_VIEWS_PATH, body).await?; + formatter::output(cfg, &data) +} + +pub async fn saved_views_delete(cfg: &Config, view_id: &str) -> Result<()> { + let path = format!("{SAVED_VIEWS_PATH}/{view_id}"); + raw_client::raw_delete(cfg, &path).await?; + println!("Log saved view {view_id} deleted."); + Ok(()) +} + #[cfg(test)] mod tests { use crate::config::{Config, OutputFormat}; @@ -754,6 +785,105 @@ mod tests { cleanup_env(); } + #[tokio::test] + async fn test_saved_views_list() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = server + .mock("GET", "/api/v2/logs/config/saved_views") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"data": []}"#) + .create_async() + .await; + + let result = super::saved_views_list(&cfg).await; + assert!( + result.is_ok(), + "saved views list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_saved_views_get() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = server + .mock("GET", "/api/v2/logs/config/saved_views/123") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"data": {"id": "123"}}"#) + .create_async() + .await; + + let result = super::saved_views_get(&cfg, "123").await; + assert!(result.is_ok(), "saved views get failed: {:?}", result.err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_saved_views_create() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let tmp = TempDir::new("saved_views_create"); + let file = tmp.path().join("view.json"); + std::fs::write(&file, r#"{"data":{"attributes":{"name":"Errors"}}}"#).unwrap(); + let _mock = server + .mock("POST", "/api/v2/logs/config/saved_views") + .match_body(mockito::Matcher::Regex(r#""name":"Errors""#.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"data": {"id": "123"}}"#) + .create_async() + .await; + + let result = super::saved_views_create(&cfg, file.to_str().unwrap()).await; + assert!( + result.is_ok(), + "saved views create failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_saved_views_create_missing_file_errors() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let result = super::saved_views_create(&cfg, "/tmp/__pup_missing_saved_view__.json").await; + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("failed to read")); + cleanup_env(); + } + + #[tokio::test] + async fn test_saved_views_delete() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = server + .mock("DELETE", "/api/v2/logs/config/saved_views/123") + .with_status(204) + .create_async() + .await; + + let result = super::saved_views_delete(&cfg, "123").await; + assert!( + result.is_ok(), + "saved views delete failed: {:?}", + result.err() + ); + cleanup_env(); + } + #[tokio::test] async fn test_logs_search_with_oauth() { let _lock = lock_env().await; diff --git a/src/main.rs b/src/main.rs index 6964cb1f..af1f3a85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1713,7 +1713,7 @@ enum Commands { /// /// The logs command provides comprehensive access to Datadog's log management capabilities /// including search, querying, aggregation, archives management, custom destinations, - /// log-based metrics, and restriction queries. + /// log-based metrics, restriction queries, and saved views. /// /// CAPABILITIES: /// • Search logs with flexible queries (v1 API) @@ -1724,6 +1724,7 @@ enum Commands { /// • Manage custom destinations for logs /// • Create and manage log-based metrics /// • Configure restriction queries for access control + /// • Manage logs saved views (experimental raw API) /// /// STORAGE TIERS: /// Datadog logs can be stored in different tiers with different performance and cost characteristics: @@ -1790,6 +1791,11 @@ enum Commands { /// # List restriction queries /// pup logs restriction-queries list /// + /// # Manage saved views + /// pup logs saved-views list + /// pup logs saved-views get 123456 + /// pup logs saved-views create --file saved-view.json + /// /// AUTHENTICATION: /// Requires either OAuth2 authentication (pup auth login) or API keys /// (DD_API_KEY and DD_APP_KEY environment variables). @@ -3217,6 +3223,27 @@ enum LogActions { #[command(subcommand)] action: LogRestrictionQueryActions, }, + /// Manage logs saved views (experimental raw API) + #[command(name = "saved-views")] + SavedViews { + #[command(subcommand)] + action: LogSavedViewActions, + }, +} + +#[derive(Subcommand)] +enum LogSavedViewActions { + /// List logs saved views + List, + /// Get logs saved view details + Get { view_id: String }, + /// Create a logs saved view from JSON + Create { + #[arg(long, help = "JSON file with saved view request body (required)")] + file: String, + }, + /// Delete a logs saved view + Delete { view_id: String }, } #[derive(Subcommand)] @@ -12031,6 +12058,20 @@ async fn main_inner() -> anyhow::Result<()> { commands::logs::restriction_queries_get(&cfg, &query_id).await?; } }, + LogActions::SavedViews { action } => match action { + LogSavedViewActions::List => { + commands::logs::saved_views_list(&cfg).await?; + } + LogSavedViewActions::Get { view_id } => { + commands::logs::saved_views_get(&cfg, &view_id).await?; + } + LogSavedViewActions::Create { file } => { + commands::logs::saved_views_create(&cfg, &file).await?; + } + LogSavedViewActions::Delete { view_id } => { + commands::logs::saved_views_delete(&cfg, &view_id).await?; + } + }, } } // --- Incidents --- diff --git a/src/test_commands.rs b/src/test_commands.rs index 489dc161..03dc0fbc 100644 --- a/src/test_commands.rs +++ b/src/test_commands.rs @@ -522,6 +522,34 @@ fn test_logs_list_sort_accepts_hyphen_timestamp() { } } +#[test] +fn test_logs_saved_views_create_parses() { + use clap::Parser; + + let cli = crate::Cli::try_parse_from([ + "pup", + "logs", + "saved-views", + "create", + "--file", + "view.json", + ]) + .expect("logs saved-views create --file should parse"); + + match cli.command { + crate::Commands::Logs { action } => match action { + crate::LogActions::SavedViews { action } => match action { + crate::LogSavedViewActions::Create { file } => { + assert_eq!(file, "view.json"); + } + _ => panic!("expected LogSavedViewActions::Create"), + }, + _ => panic!("expected LogActions::SavedViews"), + }, + _ => panic!("expected Commands::Logs"), + } +} + #[test] fn test_traces_search_sort_accepts_hyphen_timestamp() { use clap::Parser;