From ff53f3e9242afd1a7193a9f98d507cbfb01abce7 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Wed, 30 Jul 2025 11:11:24 +0200 Subject: [PATCH 01/20] feat(logs): Introduce logs command --- src/api/mod.rs | 90 +++++++++++++++- src/commands/derive_parser.rs | 2 + src/commands/logs/common_args.rs | 13 +++ src/commands/logs/list.rs | 100 ++++++++++++++++++ src/commands/logs/mod.rs | 43 ++++++++ src/commands/mod.rs | 3 + src/commands/send_metric/mod.rs | 10 +- .../_cases/help/help-windows.trycmd | 1 + tests/integration/_cases/help/help.trycmd | 1 + .../integration/_cases/logs/logs-help.trycmd | 38 +++++++ .../_cases/logs/logs-list-basic.trycmd | 3 + .../_cases/logs/logs-list-help.trycmd | 43 ++++++++ .../_cases/logs/logs-list-no-defaults.trycmd | 3 + .../_cases/logs/logs-list-with-data.trycmd | 9 ++ .../integration/_responses/logs/get-logs.json | 40 +++++++ tests/integration/logs.rs | 37 +++++++ tests/integration/mod.rs | 1 + 17 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 src/commands/logs/common_args.rs create mode 100644 src/commands/logs/list.rs create mode 100644 src/commands/logs/mod.rs create mode 100644 tests/integration/_cases/logs/logs-help.trycmd create mode 100644 tests/integration/_cases/logs/logs-list-basic.trycmd create mode 100644 tests/integration/_cases/logs/logs-list-help.trycmd create mode 100644 tests/integration/_cases/logs/logs-list-no-defaults.trycmd create mode 100644 tests/integration/_cases/logs/logs-list-with-data.trycmd create mode 100644 tests/integration/_responses/logs/get-logs.json create mode 100644 tests/integration/logs.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index bbba47f0ba..79e341c756 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1225,6 +1225,75 @@ impl<'a> AuthenticatedApi<'a> { Ok(rv) } +} + +/// Options for fetching organization events +#[derive(Debug, Default)] +pub struct FetchEventsOptions<'a> { + /// Project ID to filter events by + pub project_id: Option<&'a str>, + /// Cursor for pagination + pub cursor: Option<&'a str>, + /// Query string to filter events + pub query: Option<&'a str>, + /// Number of events per page (default: 100) + pub per_page: Option, + /// Time period for stats (default: "1h") + pub stats_period: Option<&'a str>, + /// Sort order (default: "-timestamp") + pub sort: Option<&'a str>, +} + +impl<'a> AuthenticatedApi<'a> { + /// Fetch organization events from the specified dataset + pub fn fetch_organization_events( + &self, + org: &str, + dataset: &str, + fields: &[&str], + options: FetchEventsOptions, + ) -> ApiResult> { + let mut params = vec![format!("dataset={}", QueryArg(dataset))]; + + for field in fields { + params.push(format!("field={}", QueryArg(field))); + } + + if let Some(cursor) = options.cursor { + params.push(format!("cursor={}", QueryArg(cursor))); + } + + if let Some(project_id) = options.project_id { + params.push(format!("project={}", QueryArg(project_id))); + } + + if let Some(query) = options.query { + params.push(format!("query={}", QueryArg(query))); + } + + params.push(format!("per_page={}", options.per_page.unwrap_or(100))); + params.push(format!( + "statsPeriod={}", + options.stats_period.unwrap_or("1h") + )); + params.push("referrer=sentry-cli-tail".to_owned()); + params.push(format!("sort={}", options.sort.unwrap_or("-timestamp"))); + + let url = format!( + "/organizations/{}/events/?{}", + PathArg(org), + params.join("&") + ); + + let resp = self.get(&url)?; + + if resp.status() == 404 { + return Err(ApiErrorKind::OrganizationNotFound.into()); + } + + let logs_response: LogsResponse = resp.convert()?; + Ok(logs_response.data) + } /// List all issues associated with an organization and a project pub fn list_organization_project_issues( @@ -2343,7 +2412,7 @@ pub struct ProcessedEvent { pub tags: Option>, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ProcessedEventUser { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, @@ -2377,7 +2446,7 @@ impl fmt::Display for ProcessedEventUser { } } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ProcessedEventTag { pub key: String, pub value: String, @@ -2401,3 +2470,20 @@ pub struct Region { pub struct RegionResponse { pub regions: Vec, } + +/// Response structure for logs API +#[derive(Debug, Deserialize)] +struct LogsResponse { + data: Vec, +} + +/// Log entry structure from the logs API +#[derive(Debug, Deserialize)] +pub struct LogEntry { + #[serde(rename = "sentry.item_id")] + pub item_id: String, + pub trace: Option, + pub severity: Option, + pub timestamp: String, + pub message: Option, +} diff --git a/src/commands/derive_parser.rs b/src/commands/derive_parser.rs index ee6c3a389e..3d81b94733 100644 --- a/src/commands/derive_parser.rs +++ b/src/commands/derive_parser.rs @@ -2,6 +2,7 @@ use crate::utils::auth_token::AuthToken; use crate::utils::value_parsers::{auth_token_parser, kv_parser}; use clap::{command, ArgAction::SetTrue, Parser, Subcommand}; +use super::logs::LogsArgs; use super::send_metric::SendMetricArgs; #[derive(Parser)] @@ -32,5 +33,6 @@ pub(super) struct SentryCLI { #[derive(Subcommand)] pub(super) enum SentryCLICommand { + Logs(LogsArgs), SendMetric(SendMetricArgs), } diff --git a/src/commands/logs/common_args.rs b/src/commands/logs/common_args.rs new file mode 100644 index 0000000000..12a4b697d2 --- /dev/null +++ b/src/commands/logs/common_args.rs @@ -0,0 +1,13 @@ +use clap::Args; + +/// Common arguments for all logs subcommands. +#[derive(Args)] +pub(super) struct CommonLogsArgs { + #[arg(short = 'o', long = "org")] + #[arg(help = "The organization ID or slug.")] + pub(super) org: Option, + + #[arg(short = 'p', long = "project")] + #[arg(help = "The project ID or slug.")] + pub(super) project: Option, +} diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs new file mode 100644 index 0000000000..86014bc0c1 --- /dev/null +++ b/src/commands/logs/list.rs @@ -0,0 +1,100 @@ +use anyhow::Result; +use clap::Args; + +use crate::api::{Api, FetchEventsOptions}; +use crate::config::Config; +use crate::utils::formatting::Table; + +use super::common_args::CommonLogsArgs; + +/// Arguments for listing logs +#[derive(Args)] +pub(super) struct ListLogsArgs { + #[command(flatten)] + pub(super) common: CommonLogsArgs, + + #[arg(long = "max-rows")] + #[arg(help = "Maximum number of rows to print.")] + pub(super) max_rows: Option, + + #[arg(long = "per-page", default_value = "100")] + #[arg(help = "Number of log entries per request (max 1000).")] + pub(super) per_page: usize, + + #[arg(long = "query", default_value = "")] + #[arg(help = "Query to filter logs. Example: \"level:error\"")] + pub(super) query: String, + + #[arg(long = "live")] + #[arg(help = "Live-tail logs (not implemented yet).")] + pub(super) live: bool, +} + +pub(super) fn execute(args: ListLogsArgs) -> Result<()> { + let config = Config::current(); + let (default_org, default_project) = config.get_org_and_project_defaults(); + + let org = args.common.org.or(default_org).ok_or_else(|| { + anyhow::anyhow!("No organization specified. Use --org or set a default in config.") + })?; + let project = args.common.project.or(default_project).ok_or_else(|| { + anyhow::anyhow!("No project specified. Use --project or set a default in config.") + })?; + + let api = Api::current(); + + let query = if args.query.is_empty() { + None + } else { + Some(args.query.as_str()) + }; + let fields = [ + "sentry.item_id", + "trace", + "severity", + "timestamp", + "message", + ]; + + let options = FetchEventsOptions { + project_id: Some(&project), + query, + per_page: Some(args.per_page), + stats_period: Some("1h"), + ..Default::default() + }; + + let logs = api + .authenticated()? + .fetch_organization_events(&org, "ourlogs", &fields, options)?; + + let mut table = Table::new(); + table + .title_row() + .add("Item ID") + .add("Timestamp") + .add("Severity") + .add("Message") + .add("Trace"); + + let max_rows = std::cmp::min(logs.len(), args.max_rows.unwrap_or(usize::MAX)); + + if let Some(logs) = logs.get(..max_rows) { + for log in logs { + let row = table.add_row(); + row.add(&log.item_id) + .add(&log.timestamp) + .add(log.severity.as_deref().unwrap_or("")) + .add(log.message.as_deref().unwrap_or("")) + .add(log.trace.as_deref().unwrap_or("")); + } + } + + if table.is_empty() { + println!("No logs found"); + } else { + table.print(); + } + + Ok(()) +} diff --git a/src/commands/logs/mod.rs b/src/commands/logs/mod.rs new file mode 100644 index 0000000000..62aa730545 --- /dev/null +++ b/src/commands/logs/mod.rs @@ -0,0 +1,43 @@ +pub mod common_args; + +mod list; + +use self::list::ListLogsArgs; +use super::derive_parser::{SentryCLI, SentryCLICommand}; +use anyhow::Result; +use clap::ArgMatches; +use clap::{Args, Command, Parser as _, Subcommand}; + +const LIST_ABOUT: &str = "List logs from your organization"; + +#[derive(Args)] +pub(super) struct LogsArgs { + #[command(subcommand)] + subcommand: LogsSubcommand, +} + +#[derive(Subcommand)] +#[command(about = "Manage logs in Sentry")] +#[command(long_about = "Manage and query logs in Sentry. \ +This command provides access to log entries and supports live-tailing functionality.")] +enum LogsSubcommand { + #[command(about = LIST_ABOUT)] + #[command(long_about = format!("{LIST_ABOUT}. \ +Query and filter log entries from your Sentry projects. \ +Supports filtering by time period, log level, and custom queries."))] + List(ListLogsArgs), +} + +pub(super) fn make_command(command: Command) -> Command { + LogsSubcommand::augment_subcommands(command) +} + +pub(super) fn execute(_: &ArgMatches) -> Result<()> { + let SentryCLICommand::Logs(LogsArgs { subcommand }) = SentryCLI::parse().command else { + unreachable!("expected logs subcommand"); + }; + + match subcommand { + LogsSubcommand::List(args) => list::execute(args), + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ecb7f95691..a894619d9f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -28,6 +28,7 @@ mod files; mod info; mod issues; mod login; +mod logs; mod mobile_app; mod monitors; mod organizations; @@ -57,6 +58,7 @@ macro_rules! each_subcommand { $mac!(info); $mac!(issues); $mac!(login); + $mac!(logs); #[cfg(feature = "unstable-mobile-app")] $mac!(mobile_app); $mac!(monitors); @@ -95,6 +97,7 @@ const UPDATE_NAGGER_CMDS: &[&str] = &[ "info", "issues", "login", + "logs", "organizations", "projects", "releases", diff --git a/src/commands/send_metric/mod.rs b/src/commands/send_metric/mod.rs index c745da47c2..82615b237d 100644 --- a/src/commands/send_metric/mod.rs +++ b/src/commands/send_metric/mod.rs @@ -59,12 +59,10 @@ pub(super) fn make_command(command: Command) -> Command { } pub(super) fn execute(_: &ArgMatches) -> Result<()> { - // When adding a new subcommand to the derive_parser SentryCLI, replace the line below with the following: - // let subcommand = match SentryCLI::parse().command { - // SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) => subcommand, - // _ => panic!("expected send-metric subcommand"), - // }; - let SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) = SentryCLI::parse().command; + let subcommand = match SentryCLI::parse().command { + SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) => subcommand, + _ => unreachable!("expected send-metric subcommand"), + }; log::warn!("{DEPRECATION_MESSAGE}"); diff --git a/tests/integration/_cases/help/help-windows.trycmd b/tests/integration/_cases/help/help-windows.trycmd index b7eea1433f..9f44742a13 100644 --- a/tests/integration/_cases/help/help-windows.trycmd +++ b/tests/integration/_cases/help/help-windows.trycmd @@ -17,6 +17,7 @@ Commands: info Print information about the configuration and verify authentication. issues Manage issues in Sentry. login Authenticate with the Sentry server. + logs Manage logs in Sentry monitors Manage cron monitors on Sentry. organizations Manage organizations on Sentry. projects Manage projects on Sentry. diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index 00a2978dd7..03ac6a02d4 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -17,6 +17,7 @@ Commands: info Print information about the configuration and verify authentication. issues Manage issues in Sentry. login Authenticate with the Sentry server. + logs Manage logs in Sentry monitors Manage cron monitors on Sentry. organizations Manage organizations on Sentry. projects Manage projects on Sentry. diff --git a/tests/integration/_cases/logs/logs-help.trycmd b/tests/integration/_cases/logs/logs-help.trycmd new file mode 100644 index 0000000000..cc2cc0eebd --- /dev/null +++ b/tests/integration/_cases/logs/logs-help.trycmd @@ -0,0 +1,38 @@ +$ sentry-cli logs --help +? success +Manage logs in Sentry. + +Usage: sentry-cli[EXE] logs [OPTIONS] + +Commands: + list List logs from your organization. + help Print this message or the help of the given subcommand(s) + +Options: + -o, --org + The organization ID or slug. + + -p, --project + The project ID or slug. + + --live + Live-tail logs (not implemented yet). + + --header + Custom headers that should be attached to all requests + in key:value format. + + --auth-token + Use the given Sentry auth token. + + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. + + [aliases: silent] + + -h, --help + Print help \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-basic.trycmd new file mode 100644 index 0000000000..537e78f063 --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-basic.trycmd @@ -0,0 +1,3 @@ +$ sentry-cli logs list --org wat-org --project wat-project --max-rows 0 +? success +No logs found \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd new file mode 100644 index 0000000000..2083ee073b --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -0,0 +1,43 @@ +$ sentry-cli logs list --help +? success +List logs from your organization. + +Usage: sentry-cli[EXE] logs list [OPTIONS] + +Options: + --max-rows + Maximum number of rows to print. + + --per-page + Number of log entries per request (max 1000). [default: 100] + + --query + Query to filter logs. Example: "level:error" [default: ] + + -o, --org + The organization ID or slug. + + -p, --project + The project ID or slug. + + --live + Live-tail logs (not implemented yet). + + --header + Custom headers that should be attached to all requests + in key:value format. + + --auth-token + Use the given Sentry auth token. + + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. + + [aliases: silent] + + -h, --help + Print help \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-no-defaults.trycmd b/tests/integration/_cases/logs/logs-list-no-defaults.trycmd new file mode 100644 index 0000000000..91290cb99d --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-no-defaults.trycmd @@ -0,0 +1,3 @@ +$ sentry-cli logs list +? 1 +[ERROR] No organization specified. Use --org or set a default in config. \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd new file mode 100644 index 0000000000..b93167383c --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -0,0 +1,9 @@ +$ sentry-cli logs list --org wat-org --project wat-project +? success ++------------------+---------------------+----------+----------------------+---------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------+----------+----------------------+---------------------+ +| test-item-id-001 | 2025-01-15T10:30:00 | info | test_log_message_001 | test-trace-id-abc123 | +| test-item-id-002 | 2025-01-15T10:31:00 | error | test_error_message_002 | test-trace-id-def456 | +| test-item-id-003 | 2025-01-15T10:32:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | ++------------------+---------------------+----------+----------------------+---------------------+ \ No newline at end of file diff --git a/tests/integration/_responses/logs/get-logs.json b/tests/integration/_responses/logs/get-logs.json new file mode 100644 index 0000000000..b8b4851084 --- /dev/null +++ b/tests/integration/_responses/logs/get-logs.json @@ -0,0 +1,40 @@ +{ + "data": [ + { + "sentry.item_id": "test-item-id-001", + "project.id": 1, + "trace": "test-trace-id-abc123", + "severity_number": 9, + "severity": "info", + "timestamp": "2025-01-15T10:30:00+00:00", + "tags[sentry.timestamp_precise,number]": 1.7051238000000000e+18, + "sentry.observed_timestamp_nanos": "1705123800000000000", + "message": "test_log_message_001", + "key": null + }, + { + "sentry.item_id": "test-item-id-002", + "project.id": 1, + "trace": "test-trace-id-def456", + "severity_number": 13, + "severity": "error", + "timestamp": "2025-01-15T10:31:00+00:00", + "tags[sentry.timestamp_precise,number]": 1.7051238600000000e+18, + "sentry.observed_timestamp_nanos": "1705123860000000000", + "message": "test_error_message_002", + "key": null + }, + { + "sentry.item_id": "test-item-id-003", + "project.id": 1, + "trace": "test-trace-id-ghi789", + "severity_number": 11, + "severity": "warning", + "timestamp": "2025-01-15T10:32:00+00:00", + "tags[sentry.timestamp_precise,number]": 1.7051239200000000e+18, + "sentry.observed_timestamp_nanos": "1705123920000000000", + "message": "test_warning_message_003", + "key": null + } + ] +} \ No newline at end of file diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs new file mode 100644 index 0000000000..3734e3ee22 --- /dev/null +++ b/tests/integration/logs.rs @@ -0,0 +1,37 @@ +use trycmd::TestCases; + +use crate::integration::{MockEndpointBuilder, TestManager}; + +#[test] +fn command_logs_help() { + TestCases::new().case("tests/integration/_cases/logs/logs-help.trycmd"); +} + +#[test] +fn command_logs_list_help() { + TestCases::new().case("tests/integration/_cases/logs/logs-list-help.trycmd"); +} + +#[test] +fn command_logs_list_no_defaults() { + TestCases::new().case("tests/integration/_cases/logs/logs-list-no-defaults.trycmd"); +} + +#[test] +fn command_logs_list_basic() { + TestCases::new().case("tests/integration/_cases/logs/logs-list-basic.trycmd"); +} + +#[test] +fn command_logs_list_with_data() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new( + "GET", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=1h&referrer=sentry-cli-tail&sort=-timestamp", + ) + .with_response_file("logs/get-logs.json"), + ) + .register_trycmd_test("logs/logs-list-with-data.trycmd") + .with_default_token(); +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index ab6e5fba6b..62b68ea816 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -7,6 +7,7 @@ mod info; mod invalid_env; mod issues; mod login; +mod logs; mod mobile_app; mod monitors; mod org_tokens; From 57ca614ed8fa1c47c5353eaf4df88aab6ca72e20 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Wed, 30 Jul 2025 17:45:59 +0200 Subject: [PATCH 02/20] fixes --- src/api/mod.rs | 115 ++++++++++-------- src/commands/logs/common_args.rs | 13 -- src/commands/logs/list.rs | 89 ++++++++------ src/commands/logs/mod.rs | 8 +- .../integration/_cases/logs/logs-help.trycmd | 5 +- .../_cases/logs/logs-list-basic.trycmd | 2 +- .../_cases/logs/logs-list-help.trycmd | 10 +- .../logs-list-with-custom-max-rows.trycmd | 5 + .../_cases/logs/logs-list-with-data.trycmd | 14 +-- .../logs/logs-list-with-zero-max-rows.trycmd | 3 + tests/integration/logs.rs | 42 +++---- 11 files changed, 160 insertions(+), 146 deletions(-) delete mode 100644 src/commands/logs/common_args.rs create mode 100644 tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd create mode 100644 tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd diff --git a/src/api/mod.rs b/src/api/mod.rs index 79e341c756..095f694974 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1225,60 +1225,14 @@ impl<'a> AuthenticatedApi<'a> { Ok(rv) } -} - -/// Options for fetching organization events -#[derive(Debug, Default)] -pub struct FetchEventsOptions<'a> { - /// Project ID to filter events by - pub project_id: Option<&'a str>, - /// Cursor for pagination - pub cursor: Option<&'a str>, - /// Query string to filter events - pub query: Option<&'a str>, - /// Number of events per page (default: 100) - pub per_page: Option, - /// Time period for stats (default: "1h") - pub stats_period: Option<&'a str>, - /// Sort order (default: "-timestamp") - pub sort: Option<&'a str>, -} -impl<'a> AuthenticatedApi<'a> { /// Fetch organization events from the specified dataset pub fn fetch_organization_events( &self, org: &str, - dataset: &str, - fields: &[&str], - options: FetchEventsOptions, + options: &FetchEventsOptions, ) -> ApiResult> { - let mut params = vec![format!("dataset={}", QueryArg(dataset))]; - - for field in fields { - params.push(format!("field={}", QueryArg(field))); - } - - if let Some(cursor) = options.cursor { - params.push(format!("cursor={}", QueryArg(cursor))); - } - - if let Some(project_id) = options.project_id { - params.push(format!("project={}", QueryArg(project_id))); - } - - if let Some(query) = options.query { - params.push(format!("query={}", QueryArg(query))); - } - - params.push(format!("per_page={}", options.per_page.unwrap_or(100))); - params.push(format!( - "statsPeriod={}", - options.stats_period.unwrap_or("1h") - )); - params.push("referrer=sentry-cli-tail".to_owned()); - params.push(format!("sort={}", options.sort.unwrap_or("-timestamp"))); - + let params = options.to_query_params(); let url = format!( "/organizations/{}/events/?{}", PathArg(org), @@ -1459,6 +1413,71 @@ impl<'a> AuthenticatedApi<'a> { } } +/// Available datasets for fetching organization events +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Dataset { + /// Our logs dataset + OurLogs, +} + +impl Dataset { + /// Returns the string representation of the dataset + pub fn as_str(&self) -> &'static str { + match self { + Dataset::OurLogs => "ourlogs", + } + } +} +/// Options for fetching organization events +pub struct FetchEventsOptions<'a> { + /// Dataset to fetch events from + pub dataset: Dataset, + /// Fields to include in the response + pub fields: &'a [&'a str], + /// Project ID to filter events by + pub project_id: Option<&'a str>, + /// Cursor for pagination + pub cursor: Option<&'a str>, + /// Query string to filter events + pub query: Option<&'a str>, + /// Number of events per page (default: 100) + pub per_page: Option, + /// Time period for stats (default: "1h") + pub stats_period: Option<&'a str>, + /// Sort order (default: "-timestamp") + pub sort: Option<&'a str>, +} + +impl<'a> FetchEventsOptions<'a> { + /// Generate query parameters as a vector of strings + pub fn to_query_params(&self) -> Vec { + let mut params = vec![format!("dataset={}", QueryArg(self.dataset.as_str()))]; + + for field in self.fields { + params.push(format!("field={}", QueryArg(field))); + } + + if let Some(cursor) = self.cursor { + params.push(format!("cursor={}", QueryArg(cursor))); + } + + if let Some(project_id) = self.project_id { + params.push(format!("project={}", QueryArg(project_id))); + } + + if let Some(query) = self.query { + params.push(format!("query={}", QueryArg(query))); + } + + params.push(format!("per_page={}", self.per_page.unwrap_or(100))); + params.push(format!("statsPeriod={}", self.stats_period.unwrap_or("1h"))); + + params.push(format!("sort={}", self.sort.unwrap_or("-timestamp"))); + + params + } +} + impl RegionSpecificApi<'_> { fn request(&self, method: Method, url: &str) -> ApiResult { self.api diff --git a/src/commands/logs/common_args.rs b/src/commands/logs/common_args.rs deleted file mode 100644 index 12a4b697d2..0000000000 --- a/src/commands/logs/common_args.rs +++ /dev/null @@ -1,13 +0,0 @@ -use clap::Args; - -/// Common arguments for all logs subcommands. -#[derive(Args)] -pub(super) struct CommonLogsArgs { - #[arg(short = 'o', long = "org")] - #[arg(help = "The organization ID or slug.")] - pub(super) org: Option, - - #[arg(short = 'p', long = "project")] - #[arg(help = "The project ID or slug.")] - pub(super) project: Option, -} diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 86014bc0c1..294de1e834 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -1,45 +1,59 @@ use anyhow::Result; use clap::Args; -use crate::api::{Api, FetchEventsOptions}; +use crate::api::{Api, Dataset, FetchEventsOptions}; use crate::config::Config; use crate::utils::formatting::Table; -use super::common_args::CommonLogsArgs; +/// Fields to fetch from the logs API +const LOG_FIELDS: &[&str] = &[ + "sentry.item_id", + "trace", + "severity", + "timestamp", + "message", +]; /// Arguments for listing logs #[derive(Args)] pub(super) struct ListLogsArgs { - #[command(flatten)] - pub(super) common: CommonLogsArgs, + #[arg(short = 'o', long = "org")] + #[arg(help = "The organization ID or slug.")] + org: Option, - #[arg(long = "max-rows")] - #[arg(help = "Maximum number of rows to print.")] - pub(super) max_rows: Option, + #[arg(short = 'p', long = "project")] + #[arg(help = "The project ID (slug not supported).")] + project: Option, - #[arg(long = "per-page", default_value = "100")] - #[arg(help = "Number of log entries per request (max 1000).")] - pub(super) per_page: usize, + #[arg(long = "max-rows", default_value = "100")] + #[arg(help = "Maximum number of log entries to fetch and display (max 1000).")] + max_rows: usize, #[arg(long = "query", default_value = "")] #[arg(help = "Query to filter logs. Example: \"level:error\"")] - pub(super) query: String, - - #[arg(long = "live")] - #[arg(help = "Live-tail logs (not implemented yet).")] - pub(super) live: bool, + query: String, } pub(super) fn execute(args: ListLogsArgs) -> Result<()> { let config = Config::current(); let (default_org, default_project) = config.get_org_and_project_defaults(); - let org = args.common.org.or(default_org).ok_or_else(|| { - anyhow::anyhow!("No organization specified. Use --org or set a default in config.") - })?; - let project = args.common.project.or(default_project).ok_or_else(|| { - anyhow::anyhow!("No project specified. Use --project or set a default in config.") - })?; + let org = args + .org + .as_ref() + .or(default_org.as_ref()) + .ok_or_else(|| { + anyhow::anyhow!("No organization specified. Use --org or set a default in config.") + })? + .to_owned(); + let project = args + .project + .as_ref() + .or(default_project.as_ref()) + .ok_or_else(|| { + anyhow::anyhow!("No project specified. Use --project or set a default in config.") + })? + .to_owned(); let api = Api::current(); @@ -48,25 +62,32 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { } else { Some(args.query.as_str()) }; - let fields = [ - "sentry.item_id", - "trace", - "severity", - "timestamp", - "message", - ]; + execute_single_fetch(&api, &org, &project, query, LOG_FIELDS, &args) +} + +fn execute_single_fetch( + api: &Api, + org: &str, + project: &str, + query: Option<&str>, + fields: &[&str], + args: &ListLogsArgs, +) -> Result<()> { let options = FetchEventsOptions { - project_id: Some(&project), + dataset: Dataset::OurLogs, + fields, + project_id: Some(project), + cursor: None, query, - per_page: Some(args.per_page), + per_page: Some(args.max_rows), stats_period: Some("1h"), - ..Default::default() + sort: Some("-timestamp"), }; let logs = api .authenticated()? - .fetch_organization_events(&org, "ourlogs", &fields, options)?; + .fetch_organization_events(org, &options)?; let mut table = Table::new(); table @@ -77,9 +98,7 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { .add("Message") .add("Trace"); - let max_rows = std::cmp::min(logs.len(), args.max_rows.unwrap_or(usize::MAX)); - - if let Some(logs) = logs.get(..max_rows) { + if let Some(logs) = logs.get(..args.max_rows) { for log in logs { let row = table.add_row(); row.add(&log.item_id) diff --git a/src/commands/logs/mod.rs b/src/commands/logs/mod.rs index 62aa730545..10ad5e0d10 100644 --- a/src/commands/logs/mod.rs +++ b/src/commands/logs/mod.rs @@ -1,5 +1,3 @@ -pub mod common_args; - mod list; use self::list::ListLogsArgs; @@ -19,12 +17,12 @@ pub(super) struct LogsArgs { #[derive(Subcommand)] #[command(about = "Manage logs in Sentry")] #[command(long_about = "Manage and query logs in Sentry. \ -This command provides access to log entries and supports live-tailing functionality.")] + This command provides access to log entries.")] enum LogsSubcommand { #[command(about = LIST_ABOUT)] #[command(long_about = format!("{LIST_ABOUT}. \ -Query and filter log entries from your Sentry projects. \ -Supports filtering by time period, log level, and custom queries."))] + Query and filter log entries from your Sentry projects. \ + Supports filtering by time period, log level, and custom queries."))] List(ListLogsArgs), } diff --git a/tests/integration/_cases/logs/logs-help.trycmd b/tests/integration/_cases/logs/logs-help.trycmd index cc2cc0eebd..7f5d05949d 100644 --- a/tests/integration/_cases/logs/logs-help.trycmd +++ b/tests/integration/_cases/logs/logs-help.trycmd @@ -13,10 +13,7 @@ Options: The organization ID or slug. -p, --project - The project ID or slug. - - --live - Live-tail logs (not implemented yet). + The project ID (slug not supported). --header Custom headers that should be attached to all requests diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-basic.trycmd index 537e78f063..c5a8f2e046 100644 --- a/tests/integration/_cases/logs/logs-list-basic.trycmd +++ b/tests/integration/_cases/logs/logs-list-basic.trycmd @@ -1,3 +1,3 @@ -$ sentry-cli logs list --org wat-org --project wat-project --max-rows 0 +$ sentry-cli logs list --org wat-org --project 12345 --max-rows 0 ? success No logs found \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index 2083ee073b..a73b680dce 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -6,10 +6,7 @@ Usage: sentry-cli[EXE] logs list [OPTIONS] Options: --max-rows - Maximum number of rows to print. - - --per-page - Number of log entries per request (max 1000). [default: 100] + Maximum number of log entries to fetch and display (max 1000). [default: 100] --query Query to filter logs. Example: "level:error" [default: ] @@ -18,10 +15,7 @@ Options: The organization ID or slug. -p, --project - The project ID or slug. - - --live - Live-tail logs (not implemented yet). + The project ID (slug not supported). --header Custom headers that should be attached to all requests diff --git a/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd b/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd new file mode 100644 index 0000000000..eb4481295d --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd @@ -0,0 +1,5 @@ +$ sentry-cli logs list --org wat-org --project 12345 --max-rows 50 +? success ++------------------+---------------------+----------+----------------------+---------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------+----------+----------------------+---------------------+ \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd index b93167383c..c3c8ac159b 100644 --- a/tests/integration/_cases/logs/logs-list-with-data.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -1,9 +1,9 @@ -$ sentry-cli logs list --org wat-org --project wat-project +$ sentry-cli logs list --org wat-org --project 12345 ? success -+------------------+---------------------+----------+----------------------+---------------------+ -| Item ID | Timestamp | Severity | Message | Trace | -+------------------+---------------------+----------+----------------------+---------------------+ -| test-item-id-001 | 2025-01-15T10:30:00 | info | test_log_message_001 | test-trace-id-abc123 | -| test-item-id-002 | 2025-01-15T10:31:00 | error | test_error_message_002 | test-trace-id-def456 | ++------------------+---------------------+----------+--------------------------+---------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------+----------+--------------------------+---------------------+ +| test-item-id-001 | 2025-01-15T10:30:00 | info | test_log_message_001 | test-trace-id-abc123 | +| test-item-id-002 | 2025-01-15T10:31:00 | error | test_error_message_002 | test-trace-id-def456 | | test-item-id-003 | 2025-01-15T10:32:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | -+------------------+---------------------+----------+----------------------+---------------------+ \ No newline at end of file ++------------------+---------------------+----------+--------------------------+---------------------+ \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd new file mode 100644 index 0000000000..c5a8f2e046 --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd @@ -0,0 +1,3 @@ +$ sentry-cli logs list --org wat-org --project 12345 --max-rows 0 +? success +No logs found \ No newline at end of file diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 3734e3ee22..4a39865f19 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -1,37 +1,29 @@ -use trycmd::TestCases; - use crate::integration::{MockEndpointBuilder, TestManager}; #[test] -fn command_logs_help() { - TestCases::new().case("tests/integration/_cases/logs/logs-help.trycmd"); -} - -#[test] -fn command_logs_list_help() { - TestCases::new().case("tests/integration/_cases/logs/logs-list-help.trycmd"); -} - -#[test] -fn command_logs_list_no_defaults() { - TestCases::new().case("tests/integration/_cases/logs/logs-list-no-defaults.trycmd"); -} - -#[test] -fn command_logs_list_basic() { - TestCases::new().case("tests/integration/_cases/logs/logs-list-basic.trycmd"); -} - -#[test] -fn command_logs_list_with_data() { +fn command_logs_with_api_calls() { TestManager::new() .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=1h&referrer=sentry-cli-tail&sort=-timestamp", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=1h&sort=-timestamp", + ) + .with_response_file("logs/get-logs.json"), + ) + .mock_endpoint( + MockEndpointBuilder::new( + "GET", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=50&statsPeriod=1h&sort=-timestamp", ) .with_response_file("logs/get-logs.json"), ) - .register_trycmd_test("logs/logs-list-with-data.trycmd") + .mock_endpoint( + MockEndpointBuilder::new( + "GET", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=0&statsPeriod=1h&sort=-timestamp", + ) + .with_response_body("{\"data\": []}"), + ) + .register_trycmd_test("logs/*.trycmd") .with_default_token(); } From 2ea949b65104096a30e5cf36a993489b2d7c57a6 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Fri, 1 Aug 2025 11:48:52 +0200 Subject: [PATCH 03/20] fixes x2 --- src/api/mod.rs | 9 +++- src/commands/logs/list.rs | 32 +++++++++---- .../_cases/logs/logs-help-windows.trycmd | 32 +++++++++++++ .../integration/_cases/logs/logs-help.trycmd | 17 +++---- .../_cases/logs/logs-list-basic.trycmd | 7 ++- .../_cases/logs/logs-list-help.trycmd | 30 +++++++----- .../_cases/logs/logs-list-no-defaults.trycmd | 3 -- .../logs-list-with-custom-max-rows.trycmd | 5 -- .../_cases/logs/logs-list-with-data.trycmd | 19 ++++---- .../logs/logs-list-with-zero-max-rows.trycmd | 9 +++- tests/integration/logs.rs | 46 +++++++++++++------ 11 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 tests/integration/_cases/logs/logs-help-windows.trycmd delete mode 100644 tests/integration/_cases/logs/logs-list-no-defaults.trycmd delete mode 100644 tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd diff --git a/src/api/mod.rs b/src/api/mod.rs index 095f694974..5b83769532 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1422,12 +1422,19 @@ pub enum Dataset { impl Dataset { /// Returns the string representation of the dataset - pub fn as_str(&self) -> &'static str { + fn as_str(&self) -> &'static str { match self { Dataset::OurLogs => "ourlogs", } } } + +impl fmt::Display for Dataset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + /// Options for fetching organization events pub struct FetchEventsOptions<'a> { /// Dataset to fetch events from diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 294de1e834..c11de2e2a8 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -5,6 +5,18 @@ use crate::api::{Api, Dataset, FetchEventsOptions}; use crate::config::Config; use crate::utils::formatting::Table; +/// Validate that max_rows is greater than 0 +fn validate_max_rows(s: &str) -> Result { + let value = s + .parse::() + .map_err(|_| "invalid number".to_owned())?; + if value == 0 { + Err("max-rows must be greater than 0".to_owned()) + } else { + Ok(value) + } +} + /// Fields to fetch from the logs API const LOG_FIELDS: &[&str] = &[ "sentry.item_id", @@ -26,6 +38,7 @@ pub(super) struct ListLogsArgs { project: Option, #[arg(long = "max-rows", default_value = "100")] + #[arg(value_parser = validate_max_rows)] #[arg(help = "Maximum number of log entries to fetch and display (max 1000).")] max_rows: usize, @@ -43,7 +56,7 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { .as_ref() .or(default_org.as_ref()) .ok_or_else(|| { - anyhow::anyhow!("No organization specified. Use --org or set a default in config.") + anyhow::anyhow!("No organization specified. Please specify an organization using the --org argument.") })? .to_owned(); let project = args @@ -98,15 +111,14 @@ fn execute_single_fetch( .add("Message") .add("Trace"); - if let Some(logs) = logs.get(..args.max_rows) { - for log in logs { - let row = table.add_row(); - row.add(&log.item_id) - .add(&log.timestamp) - .add(log.severity.as_deref().unwrap_or("")) - .add(log.message.as_deref().unwrap_or("")) - .add(log.trace.as_deref().unwrap_or("")); - } + let logs_to_show = &logs[..args.max_rows.min(logs.len())]; + for log in logs_to_show { + let row = table.add_row(); + row.add(&log.item_id) + .add(&log.timestamp) + .add(log.severity.as_deref().unwrap_or("")) + .add(log.message.as_deref().unwrap_or("")) + .add(log.trace.as_deref().unwrap_or("")); } if table.is_empty() { diff --git a/tests/integration/_cases/logs/logs-help-windows.trycmd b/tests/integration/_cases/logs/logs-help-windows.trycmd new file mode 100644 index 0000000000..04279797ae --- /dev/null +++ b/tests/integration/_cases/logs/logs-help-windows.trycmd @@ -0,0 +1,32 @@ +``` +$ sentry-cli logs --help +? success +Manage and query logs in Sentry. This command provides access to log entries. + +Usage: sentry-cli[EXE] logs [OPTIONS] [COMMAND] + +Commands: + list List logs from your organization + help Print this message or the help of the given subcommand(s) + +Options: + --header + Custom headers that should be attached to all requests + in key:value format. + + --auth-token + Use the given Sentry auth token. + + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. + + [aliases: silent] + + -h, --help + Print help (see a summary with '-h') + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-help.trycmd b/tests/integration/_cases/logs/logs-help.trycmd index 7f5d05949d..43d5b16c2a 100644 --- a/tests/integration/_cases/logs/logs-help.trycmd +++ b/tests/integration/_cases/logs/logs-help.trycmd @@ -1,20 +1,15 @@ +``` $ sentry-cli logs --help ? success -Manage logs in Sentry. +Manage and query logs in Sentry. This command provides access to log entries. -Usage: sentry-cli[EXE] logs [OPTIONS] +Usage: sentry-cli logs [OPTIONS] [COMMAND] Commands: - list List logs from your organization. + list List logs from your organization help Print this message or the help of the given subcommand(s) Options: - -o, --org - The organization ID or slug. - - -p, --project - The project ID (slug not supported). - --header Custom headers that should be attached to all requests in key:value format. @@ -32,4 +27,6 @@ Options: [aliases: silent] -h, --help - Print help \ No newline at end of file + Print help (see a summary with '-h') + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-basic.trycmd index c5a8f2e046..da6f07b998 100644 --- a/tests/integration/_cases/logs/logs-list-basic.trycmd +++ b/tests/integration/_cases/logs/logs-list-basic.trycmd @@ -1,3 +1,6 @@ -$ sentry-cli logs list --org wat-org --project 12345 --max-rows 0 +``` +$ sentry-cli logs list --org wat-org --project 12345 --max-rows 1 ? success -No logs found \ No newline at end of file +No logs found + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index a73b680dce..e1824925d5 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -1,29 +1,35 @@ +``` $ sentry-cli logs list --help ? success -List logs from your organization. +List logs from your organization. Query and filter log entries from your Sentry projects. Supports +filtering by time period, log level, and custom queries. Usage: sentry-cli[EXE] logs list [OPTIONS] Options: - --max-rows - Maximum number of log entries to fetch and display (max 1000). [default: 100] - - --query - Query to filter logs. Example: "level:error" [default: ] - -o, --org The organization ID or slug. - -p, --project - The project ID (slug not supported). - --header Custom headers that should be attached to all requests in key:value format. + -p, --project + The project ID (slug not supported). + --auth-token Use the given Sentry auth token. + --max-rows + Maximum number of log entries to fetch and display (max 1000). + + [default: 100] + + --query + Query to filter logs. Example: "level:error" + + [default: ] + --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] @@ -34,4 +40,6 @@ Options: [aliases: silent] -h, --help - Print help \ No newline at end of file + Print help (see a summary with '-h') + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-no-defaults.trycmd b/tests/integration/_cases/logs/logs-list-no-defaults.trycmd deleted file mode 100644 index 91290cb99d..0000000000 --- a/tests/integration/_cases/logs/logs-list-no-defaults.trycmd +++ /dev/null @@ -1,3 +0,0 @@ -$ sentry-cli logs list -? 1 -[ERROR] No organization specified. Use --org or set a default in config. \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd b/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd deleted file mode 100644 index eb4481295d..0000000000 --- a/tests/integration/_cases/logs/logs-list-with-custom-max-rows.trycmd +++ /dev/null @@ -1,5 +0,0 @@ -$ sentry-cli logs list --org wat-org --project 12345 --max-rows 50 -? success -+------------------+---------------------+----------+----------------------+---------------------+ -| Item ID | Timestamp | Severity | Message | Trace | -+------------------+---------------------+----------+----------------------+---------------------+ \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd index c3c8ac159b..90d26ea33d 100644 --- a/tests/integration/_cases/logs/logs-list-with-data.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -1,9 +1,12 @@ -$ sentry-cli logs list --org wat-org --project 12345 +``` +$ sentry-cli logs list ? success -+------------------+---------------------+----------+--------------------------+---------------------+ -| Item ID | Timestamp | Severity | Message | Trace | -+------------------+---------------------+----------+--------------------------+---------------------+ -| test-item-id-001 | 2025-01-15T10:30:00 | info | test_log_message_001 | test-trace-id-abc123 | -| test-item-id-002 | 2025-01-15T10:31:00 | error | test_error_message_002 | test-trace-id-def456 | -| test-item-id-003 | 2025-01-15T10:32:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | -+------------------+---------------------+----------+--------------------------+---------------------+ \ No newline at end of file ++------------------+---------------------------+----------+--------------------------+----------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------------+----------+--------------------------+----------------------+ +| test-item-id-001 | 2025-01-15T10:30:00+00:00 | info | test_log_message_001 | test-trace-id-abc123 | +| test-item-id-002 | 2025-01-15T10:31:00+00:00 | error | test_error_message_002 | test-trace-id-def456 | +| test-item-id-003 | 2025-01-15T10:32:00+00:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | ++------------------+---------------------------+----------+--------------------------+----------------------+ + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd index c5a8f2e046..74199157df 100644 --- a/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd @@ -1,3 +1,8 @@ +``` $ sentry-cli logs list --org wat-org --project 12345 --max-rows 0 -? success -No logs found \ No newline at end of file +? failed +error: invalid value '0' for '--max-rows ': max-rows must be greater than 0 + +For more information, try '--help'. + +``` \ No newline at end of file diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 4a39865f19..9d5be570b4 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -5,25 +5,45 @@ fn command_logs_with_api_calls() { TestManager::new() .mock_endpoint( MockEndpointBuilder::new( - "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=1h&sort=-timestamp", - ) - .with_response_file("logs/get-logs.json"), - ) - .mock_endpoint( - MockEndpointBuilder::new( - "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=50&statsPeriod=1h&sort=-timestamp", + "GET", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=1h&sort=-timestamp" ) .with_response_file("logs/get-logs.json"), ) + .register_trycmd_test("logs/logs-list-with-data.trycmd") + .with_default_token(); +} + +#[test] +fn command_logs_basic() { + TestManager::new() .mock_endpoint( MockEndpointBuilder::new( - "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=0&statsPeriod=1h&sort=-timestamp", + "GET", + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=1h&sort=-timestamp" ) - .with_response_body("{\"data\": []}"), + .with_response_body(r#"{"data": []}"#), ) - .register_trycmd_test("logs/*.trycmd") + .register_trycmd_test("logs/logs-list-basic.trycmd") .with_default_token(); } + +#[test] +fn command_logs_zero_max_rows() { + TestManager::new().register_trycmd_test("logs/logs-list-with-zero-max-rows.trycmd"); +} + +#[test] +fn command_logs_help() { + let manager = TestManager::new(); + + #[cfg(not(windows))] + manager.register_trycmd_test("logs/logs-help.trycmd"); + #[cfg(windows)] + manager.register_trycmd_test("logs/logs-help-windows.trycmd"); +} + +#[test] +fn command_logs_list_help() { + TestManager::new().register_trycmd_test("logs/logs-list-help.trycmd"); +} From 49ed90032dd990fdb58370c2bf7a6ed2b4fa75cf Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 15:46:24 +0200 Subject: [PATCH 04/20] address review comments --- src/commands/logs/list.rs | 5 ++--- src/commands/logs/mod.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index c11de2e2a8..5d80d59921 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -94,7 +94,7 @@ fn execute_single_fetch( cursor: None, query, per_page: Some(args.max_rows), - stats_period: Some("1h"), + stats_period: Some("90d"), sort: Some("-timestamp"), }; @@ -111,8 +111,7 @@ fn execute_single_fetch( .add("Message") .add("Trace"); - let logs_to_show = &logs[..args.max_rows.min(logs.len())]; - for log in logs_to_show { + for log in logs.iter().take(args.max_rows) { let row = table.add_row(); row.add(&log.item_id) .add(&log.timestamp) diff --git a/src/commands/logs/mod.rs b/src/commands/logs/mod.rs index 10ad5e0d10..4d14a9823a 100644 --- a/src/commands/logs/mod.rs +++ b/src/commands/logs/mod.rs @@ -22,7 +22,7 @@ enum LogsSubcommand { #[command(about = LIST_ABOUT)] #[command(long_about = format!("{LIST_ABOUT}. \ Query and filter log entries from your Sentry projects. \ - Supports filtering by time period, log level, and custom queries."))] + Supports filtering by log level and custom queries."))] List(ListLogsArgs), } From c8e60c0b3f1415bc2fc5a28b22e28405f774231c Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 15:49:02 +0200 Subject: [PATCH 05/20] unify logs help tests --- .../_cases/logs/logs-help-windows.trycmd | 32 ------------------- .../integration/_cases/logs/logs-help.trycmd | 2 +- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 tests/integration/_cases/logs/logs-help-windows.trycmd diff --git a/tests/integration/_cases/logs/logs-help-windows.trycmd b/tests/integration/_cases/logs/logs-help-windows.trycmd deleted file mode 100644 index 04279797ae..0000000000 --- a/tests/integration/_cases/logs/logs-help-windows.trycmd +++ /dev/null @@ -1,32 +0,0 @@ -``` -$ sentry-cli logs --help -? success -Manage and query logs in Sentry. This command provides access to log entries. - -Usage: sentry-cli[EXE] logs [OPTIONS] [COMMAND] - -Commands: - list List logs from your organization - help Print this message or the help of the given subcommand(s) - -Options: - --header - Custom headers that should be attached to all requests - in key:value format. - - --auth-token - Use the given Sentry auth token. - - --log-level - Set the log output verbosity. [possible values: trace, debug, info, warn, error] - - --quiet - Do not print any output while preserving correct exit code. This flag is currently - implemented only for selected subcommands. - - [aliases: silent] - - -h, --help - Print help (see a summary with '-h') - -``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-help.trycmd b/tests/integration/_cases/logs/logs-help.trycmd index 43d5b16c2a..04279797ae 100644 --- a/tests/integration/_cases/logs/logs-help.trycmd +++ b/tests/integration/_cases/logs/logs-help.trycmd @@ -3,7 +3,7 @@ $ sentry-cli logs --help ? success Manage and query logs in Sentry. This command provides access to log entries. -Usage: sentry-cli logs [OPTIONS] [COMMAND] +Usage: sentry-cli[EXE] logs [OPTIONS] [COMMAND] Commands: list List logs from your organization From 519d55c4692091d4379ffd0ef1c2774ff07a3fa6 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 16:18:03 +0200 Subject: [PATCH 06/20] update help test --- .../_cases/logs/logs-list-basic.trycmd | 10 ++++++++-- .../_cases/logs/logs-list-help.trycmd | 2 +- .../_cases/logs/logs-list-with-data.trycmd | 16 ++++++++-------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-basic.trycmd index da6f07b998..75194393a2 100644 --- a/tests/integration/_cases/logs/logs-list-basic.trycmd +++ b/tests/integration/_cases/logs/logs-list-basic.trycmd @@ -1,6 +1,12 @@ ``` $ sentry-cli logs list --org wat-org --project 12345 --max-rows 1 -? success -No logs found +? 1 +error: API request failed + +Caused by: + sentry reported an error: unknown error (http status: 501) + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. ``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index e1824925d5..d0a9e08ed3 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -2,7 +2,7 @@ $ sentry-cli logs list --help ? success List logs from your organization. Query and filter log entries from your Sentry projects. Supports -filtering by time period, log level, and custom queries. +filtering by log level and custom queries. Usage: sentry-cli[EXE] logs list [OPTIONS] diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd index 90d26ea33d..4d1f94be77 100644 --- a/tests/integration/_cases/logs/logs-list-with-data.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -1,12 +1,12 @@ ``` $ sentry-cli logs list -? success -+------------------+---------------------------+----------+--------------------------+----------------------+ -| Item ID | Timestamp | Severity | Message | Trace | -+------------------+---------------------------+----------+--------------------------+----------------------+ -| test-item-id-001 | 2025-01-15T10:30:00+00:00 | info | test_log_message_001 | test-trace-id-abc123 | -| test-item-id-002 | 2025-01-15T10:31:00+00:00 | error | test_error_message_002 | test-trace-id-def456 | -| test-item-id-003 | 2025-01-15T10:32:00+00:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | -+------------------+---------------------------+----------+--------------------------+----------------------+ +? 1 +error: API request failed + +Caused by: + sentry reported an error: unknown error (http status: 501) + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. ``` \ No newline at end of file From a015ab183fcbb4bde27262ad1905b5cf63e3c341 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 16:18:37 +0200 Subject: [PATCH 07/20] Revert "update help test" This reverts commit 519d55c4692091d4379ffd0ef1c2774ff07a3fa6. --- .../_cases/logs/logs-list-basic.trycmd | 10 ++-------- .../_cases/logs/logs-list-help.trycmd | 2 +- .../_cases/logs/logs-list-with-data.trycmd | 16 ++++++++-------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-basic.trycmd index 75194393a2..da6f07b998 100644 --- a/tests/integration/_cases/logs/logs-list-basic.trycmd +++ b/tests/integration/_cases/logs/logs-list-basic.trycmd @@ -1,12 +1,6 @@ ``` $ sentry-cli logs list --org wat-org --project 12345 --max-rows 1 -? 1 -error: API request failed - -Caused by: - sentry reported an error: unknown error (http status: 501) - -Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. -Please attach the full debug log to all bug reports. +? success +No logs found ``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index d0a9e08ed3..e1824925d5 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -2,7 +2,7 @@ $ sentry-cli logs list --help ? success List logs from your organization. Query and filter log entries from your Sentry projects. Supports -filtering by log level and custom queries. +filtering by time period, log level, and custom queries. Usage: sentry-cli[EXE] logs list [OPTIONS] diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd index 4d1f94be77..90d26ea33d 100644 --- a/tests/integration/_cases/logs/logs-list-with-data.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -1,12 +1,12 @@ ``` $ sentry-cli logs list -? 1 -error: API request failed - -Caused by: - sentry reported an error: unknown error (http status: 501) - -Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. -Please attach the full debug log to all bug reports. +? success ++------------------+---------------------------+----------+--------------------------+----------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------------+----------+--------------------------+----------------------+ +| test-item-id-001 | 2025-01-15T10:30:00+00:00 | info | test_log_message_001 | test-trace-id-abc123 | +| test-item-id-002 | 2025-01-15T10:31:00+00:00 | error | test_error_message_002 | test-trace-id-def456 | +| test-item-id-003 | 2025-01-15T10:32:00+00:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | ++------------------+---------------------------+----------+--------------------------+----------------------+ ``` \ No newline at end of file From d836bf47c3c203d39b9749650d0dcbc59fd047ac Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 16:20:48 +0200 Subject: [PATCH 08/20] update help test --- tests/integration/_cases/logs/logs-list-help.trycmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index e1824925d5..d0a9e08ed3 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -2,7 +2,7 @@ $ sentry-cli logs list --help ? success List logs from your organization. Query and filter log entries from your Sentry projects. Supports -filtering by time period, log level, and custom queries. +filtering by log level and custom queries. Usage: sentry-cli[EXE] logs list [OPTIONS] From 43e98c81fe56018784124ee9fb3b5677522d949e Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Mon, 4 Aug 2025 16:32:48 +0200 Subject: [PATCH 09/20] fix tests --- tests/integration/logs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 9d5be570b4..b3245fb108 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -6,7 +6,7 @@ fn command_logs_with_api_calls() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=1h&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_file("logs/get-logs.json"), ) @@ -20,7 +20,7 @@ fn command_logs_basic() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=1h&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=90d&sort=-timestamp" ) .with_response_body(r#"{"data": []}"#), ) From 1b5d554f2cde5c779148034623601178c1795e48 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 5 Aug 2025 09:15:23 +0200 Subject: [PATCH 10/20] remove extra test file for windows --- tests/integration/logs.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index b3245fb108..1b9087dd32 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -39,8 +39,6 @@ fn command_logs_help() { #[cfg(not(windows))] manager.register_trycmd_test("logs/logs-help.trycmd"); - #[cfg(windows)] - manager.register_trycmd_test("logs/logs-help-windows.trycmd"); } #[test] From a5b1bb0964a24cf47bc1fa6ff9d089a5bd835cdb Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 5 Aug 2025 09:31:08 +0200 Subject: [PATCH 11/20] remove extra test code --- tests/integration/logs.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 1b9087dd32..ed660fdb15 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -33,14 +33,6 @@ fn command_logs_zero_max_rows() { TestManager::new().register_trycmd_test("logs/logs-list-with-zero-max-rows.trycmd"); } -#[test] -fn command_logs_help() { - let manager = TestManager::new(); - - #[cfg(not(windows))] - manager.register_trycmd_test("logs/logs-help.trycmd"); -} - #[test] fn command_logs_list_help() { TestManager::new().register_trycmd_test("logs/logs-list-help.trycmd"); From b7d5973ec6f66cacd211f1468962d7caf56dbf13 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 5 Aug 2025 14:09:39 +0200 Subject: [PATCH 12/20] address review comments --- src/commands/logs/list.rs | 30 ++++++++++--------- .../logs/logs-list-with-zero-max-rows.trycmd | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 5d80d59921..8b25de4fe8 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -5,15 +5,18 @@ use crate::api::{Api, Dataset, FetchEventsOptions}; use crate::config::Config; use crate::utils::formatting::Table; -/// Validate that max_rows is greater than 0 -fn validate_max_rows(s: &str) -> Result { - let value = s - .parse::() - .map_err(|_| "invalid number".to_owned())?; - if value == 0 { - Err("max-rows must be greater than 0".to_owned()) - } else { +const MAX_ROWS_RANGE: std::ops::RangeInclusive = 1..=1000; +/// Validate that max_rows is in the allowed range +fn validate_max_rows(s: &str) -> Result { + let value = s.parse()?; + if MAX_ROWS_RANGE.contains(&value) { Ok(value) + } else { + Err(anyhow::anyhow!( + "max-rows must be between {} and {}", + MAX_ROWS_RANGE.start(), + MAX_ROWS_RANGE.end() + )) } } @@ -39,7 +42,7 @@ pub(super) struct ListLogsArgs { #[arg(long = "max-rows", default_value = "100")] #[arg(value_parser = validate_max_rows)] - #[arg(help = "Maximum number of log entries to fetch and display (max 1000).")] + #[arg(help = format!("Maximum number of log entries to fetch and display (max {}).", MAX_ROWS_RANGE.end()))] max_rows: usize, #[arg(long = "query", default_value = "")] @@ -57,16 +60,15 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { .or(default_org.as_ref()) .ok_or_else(|| { anyhow::anyhow!("No organization specified. Please specify an organization using the --org argument.") - })? - .to_owned(); + })?; + let project = args .project .as_ref() .or(default_project.as_ref()) .ok_or_else(|| { anyhow::anyhow!("No project specified. Use --project or set a default in config.") - })? - .to_owned(); + })?; let api = Api::current(); @@ -76,7 +78,7 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { Some(args.query.as_str()) }; - execute_single_fetch(&api, &org, &project, query, LOG_FIELDS, &args) + execute_single_fetch(&api, org, project, query, LOG_FIELDS, &args) } fn execute_single_fetch( diff --git a/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd index 74199157df..f188288ff3 100644 --- a/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-zero-max-rows.trycmd @@ -1,7 +1,7 @@ ``` $ sentry-cli logs list --org wat-org --project 12345 --max-rows 0 ? failed -error: invalid value '0' for '--max-rows ': max-rows must be greater than 0 +error: invalid value '0' for '--max-rows ': max-rows must be between 1 and 1000 For more information, try '--help'. From 10240ce998994247037ed8c34076ccf51af1c711 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 5 Aug 2025 14:13:25 +0200 Subject: [PATCH 13/20] formatting --- src/commands/logs/list.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 8b25de4fe8..3da6f89f37 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -54,13 +54,11 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { let config = Config::current(); let (default_org, default_project) = config.get_org_and_project_defaults(); - let org = args - .org - .as_ref() - .or(default_org.as_ref()) - .ok_or_else(|| { - anyhow::anyhow!("No organization specified. Please specify an organization using the --org argument.") - })?; + let org = args.org.as_ref().or(default_org.as_ref()).ok_or_else(|| { + anyhow::anyhow!( + "No organization specified. Please specify an organization using the --org argument." + ) + })?; let project = args .project From e7f98059303fd8983fd83d20c0e457547ebe143d Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 10:14:58 +0200 Subject: [PATCH 14/20] address review comments --- src/api/mod.rs | 13 ++++++------- src/commands/logs/list.rs | 2 +- ...-basic.trycmd => logs-list-no-logs-found.trycmd} | 0 tests/integration/logs.rs | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) rename tests/integration/_cases/logs/{logs-list-basic.trycmd => logs-list-no-logs-found.trycmd} (100%) diff --git a/src/api/mod.rs b/src/api/mod.rs index 5b83769532..96afc18196 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1417,14 +1417,14 @@ impl<'a> AuthenticatedApi<'a> { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Dataset { /// Our logs dataset - OurLogs, + Logs, } impl Dataset { /// Returns the string representation of the dataset fn as_str(&self) -> &'static str { match self { - Dataset::OurLogs => "ourlogs", + Dataset::Logs => "logs", } } } @@ -1447,13 +1447,12 @@ pub struct FetchEventsOptions<'a> { pub cursor: Option<&'a str>, /// Query string to filter events pub query: Option<&'a str>, - /// Number of events per page (default: 100) + /// Number of events per page pub per_page: Option, - /// Time period for stats (default: "1h") + /// Time period for stats pub stats_period: Option<&'a str>, - /// Sort order (default: "-timestamp") - pub sort: Option<&'a str>, -} + /// Sort order + pub sort: Option<&'a str>,} impl<'a> FetchEventsOptions<'a> { /// Generate query parameters as a vector of strings diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 3da6f89f37..28f90a1d86 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -88,7 +88,7 @@ fn execute_single_fetch( args: &ListLogsArgs, ) -> Result<()> { let options = FetchEventsOptions { - dataset: Dataset::OurLogs, + dataset: Dataset::Logs, fields, project_id: Some(project), cursor: None, diff --git a/tests/integration/_cases/logs/logs-list-basic.trycmd b/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd similarity index 100% rename from tests/integration/_cases/logs/logs-list-basic.trycmd rename to tests/integration/_cases/logs/logs-list-no-logs-found.trycmd diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index ed660fdb15..f8cfd7bb3e 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -6,7 +6,7 @@ fn command_logs_with_api_calls() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_file("logs/get-logs.json"), ) @@ -20,11 +20,11 @@ fn command_logs_basic() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=ourlogs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=90d&sort=-timestamp" ) .with_response_body(r#"{"data": []}"#), ) - .register_trycmd_test("logs/logs-list-basic.trycmd") + .register_trycmd_test("logs/logs-list-no-logs-found.trycmd") .with_default_token(); } From 4d4f99ae2b37aba3debe4f33d356dca2b8e9c7ab Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 10:34:35 +0200 Subject: [PATCH 15/20] fix tests --- src/api/mod.rs | 2 -- tests/integration/_cases/logs/logs-list-no-logs-found.trycmd | 2 +- tests/integration/logs.rs | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 96afc18196..5e4dd3c4db 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1703,7 +1703,6 @@ impl ApiRequest { pipeline_env: Option, global_headers: Option>, ) -> ApiResult { - debug!("request {} {}", method, url); let mut headers = curl::easy::List::new(); headers.append("Expect:").ok(); @@ -1834,7 +1833,6 @@ impl ApiRequest { let body = self.body.as_deref(); let (status, headers) = send_req(&mut self.handle, out, body, self.progress_bar_mode.clone())?; - debug!("response status: {}", status); Ok(ApiResponse { status, headers, diff --git a/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd b/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd index da6f07b998..04e607f63c 100644 --- a/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd +++ b/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd @@ -1,5 +1,5 @@ ``` -$ sentry-cli logs list --org wat-org --project 12345 --max-rows 1 +$ sentry-cli logs list --org wat-org --project 12345 ? success No logs found diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index f8cfd7bb3e..4cfa4eab0f 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -15,12 +15,12 @@ fn command_logs_with_api_calls() { } #[test] -fn command_logs_basic() { +fn command_logs_no_logs_found() { TestManager::new() .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=1&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_body(r#"{"data": []}"#), ) From ccef59d5685bb6fbfa2c28aa3af634fb53462bf5 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 10:39:12 +0200 Subject: [PATCH 16/20] fmt --- src/api/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 5e4dd3c4db..8eceb7f27a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1452,7 +1452,8 @@ pub struct FetchEventsOptions<'a> { /// Time period for stats pub stats_period: Option<&'a str>, /// Sort order - pub sort: Option<&'a str>,} + pub sort: Option<&'a str>, +} impl<'a> FetchEventsOptions<'a> { /// Generate query parameters as a vector of strings @@ -1703,7 +1704,6 @@ impl ApiRequest { pipeline_env: Option, global_headers: Option>, ) -> ApiResult { - let mut headers = curl::easy::List::new(); headers.append("Expect:").ok(); From 74f859624e684896b1b94451ac765a846221778b Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 10:58:13 +0200 Subject: [PATCH 17/20] require project parameter --- src/api/mod.rs | 6 ++---- src/commands/logs/list.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 8eceb7f27a..c3dd77522f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1442,7 +1442,7 @@ pub struct FetchEventsOptions<'a> { /// Fields to include in the response pub fields: &'a [&'a str], /// Project ID to filter events by - pub project_id: Option<&'a str>, + pub project_id: &'a str, /// Cursor for pagination pub cursor: Option<&'a str>, /// Query string to filter events @@ -1468,9 +1468,7 @@ impl<'a> FetchEventsOptions<'a> { params.push(format!("cursor={}", QueryArg(cursor))); } - if let Some(project_id) = self.project_id { - params.push(format!("project={}", QueryArg(project_id))); - } + params.push(format!("project={}", QueryArg(self.project_id))); if let Some(query) = self.query { params.push(format!("query={}", QueryArg(query))); diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 28f90a1d86..487647d1e4 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -90,7 +90,7 @@ fn execute_single_fetch( let options = FetchEventsOptions { dataset: Dataset::Logs, fields, - project_id: Some(project), + project_id: project, cursor: None, query, per_page: Some(args.max_rows), From fa1dde749850060bc46f6e7cbc6898d30622cd6f Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 11:09:03 +0200 Subject: [PATCH 18/20] make parameters non-optional --- src/api/mod.rs | 19 +++++++++---------- src/commands/logs/list.rs | 8 ++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index c3dd77522f..c10d0c25c4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1446,13 +1446,13 @@ pub struct FetchEventsOptions<'a> { /// Cursor for pagination pub cursor: Option<&'a str>, /// Query string to filter events - pub query: Option<&'a str>, + pub query: &'a str, /// Number of events per page - pub per_page: Option, + pub per_page: usize, /// Time period for stats - pub stats_period: Option<&'a str>, + pub stats_period: &'a str, /// Sort order - pub sort: Option<&'a str>, + pub sort: &'a str, } impl<'a> FetchEventsOptions<'a> { @@ -1470,14 +1470,13 @@ impl<'a> FetchEventsOptions<'a> { params.push(format!("project={}", QueryArg(self.project_id))); - if let Some(query) = self.query { - params.push(format!("query={}", QueryArg(query))); + if !self.query.is_empty() { + params.push(format!("query={}", QueryArg(self.query))); } - params.push(format!("per_page={}", self.per_page.unwrap_or(100))); - params.push(format!("statsPeriod={}", self.stats_period.unwrap_or("1h"))); - - params.push(format!("sort={}", self.sort.unwrap_or("-timestamp"))); + params.push(format!("per_page={}", self.per_page)); + params.push(format!("statsPeriod={}", QueryArg(self.stats_period))); + params.push(format!("sort={}", QueryArg(self.sort))); params } diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 487647d1e4..052983d1aa 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -92,10 +92,10 @@ fn execute_single_fetch( fields, project_id: project, cursor: None, - query, - per_page: Some(args.max_rows), - stats_period: Some("90d"), - sort: Some("-timestamp"), + query: query.unwrap_or(""), + per_page: args.max_rows, + stats_period: "90d", + sort: "-timestamp", }; let logs = api From 6c6412023558fb04d8d6d0362521e81c168c1c4e Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 11:11:18 +0200 Subject: [PATCH 19/20] make parameters non-optional --- src/api/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index c10d0c25c4..2acf7f0516 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1469,11 +1469,7 @@ impl<'a> FetchEventsOptions<'a> { } params.push(format!("project={}", QueryArg(self.project_id))); - - if !self.query.is_empty() { - params.push(format!("query={}", QueryArg(self.query))); - } - + params.push(format!("query={}", QueryArg(self.query))); params.push(format!("per_page={}", self.per_page)); params.push(format!("statsPeriod={}", QueryArg(self.stats_period))); params.push(format!("sort={}", QueryArg(self.sort))); From 3291c36d4a80fbe267b9ba19acc5e381cc792850 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Wed, 6 Aug 2025 11:32:12 +0200 Subject: [PATCH 20/20] fix tests --- tests/integration/logs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 4cfa4eab0f..ac25ff6c15 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -6,7 +6,7 @@ fn command_logs_with_api_calls() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&per_page=100&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&query=&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_file("logs/get-logs.json"), ) @@ -20,7 +20,7 @@ fn command_logs_no_logs_found() { .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&query=&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_body(r#"{"data": []}"#), )