Skip to content
Draft
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
5 changes: 3 additions & 2 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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, 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 | ✅ |
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
130 changes: 130 additions & 0 deletions src/commands/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 42 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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 ---
Expand Down
28 changes: 28 additions & 0 deletions src/test_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading