Skip to content

Commit 7f3cb0f

Browse files
committed
feat(logs): Introduce logs command
1 parent ea97170 commit 7f3cb0f

File tree

16 files changed

+428
-8
lines changed

16 files changed

+428
-8
lines changed

src/api/mod.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,75 @@ impl<'a> AuthenticatedApi<'a> {
12251225

12261226
Ok(rv)
12271227
}
1228+
}
1229+
1230+
/// Options for fetching organization events
1231+
#[derive(Debug, Default)]
1232+
pub struct FetchEventsOptions<'a> {
1233+
/// Project ID to filter events by
1234+
pub project_id: Option<&'a str>,
1235+
/// Cursor for pagination
1236+
pub cursor: Option<&'a str>,
1237+
/// Query string to filter events
1238+
pub query: Option<&'a str>,
1239+
/// Number of events per page (default: 100)
1240+
pub per_page: Option<usize>,
1241+
/// Time period for stats (default: "1h")
1242+
pub stats_period: Option<&'a str>,
1243+
/// Sort order (default: "-timestamp")
1244+
pub sort: Option<&'a str>,
1245+
}
1246+
1247+
impl<'a> AuthenticatedApi<'a> {
1248+
/// Fetch organization events from the specified dataset
1249+
pub fn fetch_organization_events(
1250+
&self,
1251+
org: &str,
1252+
dataset: &str,
1253+
fields: &[&str],
1254+
options: FetchEventsOptions,
1255+
) -> ApiResult<Vec<LogEntry>> {
1256+
let mut params = vec![format!("dataset={}", QueryArg(dataset))];
1257+
1258+
for field in fields {
1259+
params.push(format!("field={}", QueryArg(field)));
1260+
}
1261+
1262+
if let Some(cursor) = options.cursor {
1263+
params.push(format!("cursor={}", QueryArg(cursor)));
1264+
}
1265+
1266+
if let Some(project_id) = options.project_id {
1267+
params.push(format!("project={}", QueryArg(project_id)));
1268+
}
1269+
1270+
if let Some(query) = options.query {
1271+
params.push(format!("query={}", QueryArg(query)));
1272+
}
1273+
1274+
params.push(format!("per_page={}", options.per_page.unwrap_or(100)));
1275+
params.push(format!(
1276+
"statsPeriod={}",
1277+
options.stats_period.unwrap_or("1h")
1278+
));
1279+
params.push("referrer=sentry-cli-tail".to_owned());
1280+
params.push(format!("sort={}", options.sort.unwrap_or("-timestamp")));
1281+
1282+
let url = format!(
1283+
"/organizations/{}/events/?{}",
1284+
PathArg(org),
1285+
params.join("&")
1286+
);
1287+
1288+
let resp = self.get(&url)?;
1289+
1290+
if resp.status() == 404 {
1291+
return Err(ApiErrorKind::OrganizationNotFound.into());
1292+
}
1293+
1294+
let logs_response: LogsResponse = resp.convert()?;
1295+
Ok(logs_response.data)
1296+
}
12281297

12291298
/// List all issues associated with an organization and a project
12301299
pub fn list_organization_project_issues(
@@ -2343,7 +2412,7 @@ pub struct ProcessedEvent {
23432412
pub tags: Option<Vec<ProcessedEventTag>>,
23442413
}
23452414

2346-
#[derive(Clone, Debug, Deserialize)]
2415+
#[derive(Clone, Debug, Deserialize, Serialize)]
23472416
pub struct ProcessedEventUser {
23482417
#[serde(skip_serializing_if = "Option::is_none")]
23492418
pub id: Option<String>,
@@ -2377,7 +2446,7 @@ impl fmt::Display for ProcessedEventUser {
23772446
}
23782447
}
23792448

2380-
#[derive(Clone, Debug, Deserialize)]
2449+
#[derive(Clone, Debug, Deserialize, Serialize)]
23812450
pub struct ProcessedEventTag {
23822451
pub key: String,
23832452
pub value: String,
@@ -2401,3 +2470,20 @@ pub struct Region {
24012470
pub struct RegionResponse {
24022471
pub regions: Vec<Region>,
24032472
}
2473+
2474+
/// Response structure for logs API
2475+
#[derive(Debug, Deserialize)]
2476+
struct LogsResponse {
2477+
data: Vec<LogEntry>,
2478+
}
2479+
2480+
/// Log entry structure from the logs API
2481+
#[derive(Debug, Deserialize)]
2482+
pub struct LogEntry {
2483+
#[serde(rename = "sentry.item_id")]
2484+
pub item_id: String,
2485+
pub trace: Option<String>,
2486+
pub severity: Option<String>,
2487+
pub timestamp: String,
2488+
pub message: Option<String>,
2489+
}

src/commands/derive_parser.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::utils::auth_token::AuthToken;
22
use crate::utils::value_parsers::{auth_token_parser, kv_parser};
33
use clap::{command, ArgAction::SetTrue, Parser, Subcommand};
44

5+
use super::logs::LogsArgs;
56
use super::send_metric::SendMetricArgs;
67

78
#[derive(Parser)]
@@ -32,5 +33,6 @@ pub(super) struct SentryCLI {
3233

3334
#[derive(Subcommand)]
3435
pub(super) enum SentryCLICommand {
36+
Logs(LogsArgs),
3537
SendMetric(SendMetricArgs),
3638
}

src/commands/logs/common_args.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use clap::Args;
2+
3+
/// Common arguments for all logs subcommands.
4+
#[derive(Args)]
5+
pub(super) struct CommonLogsArgs {
6+
#[arg(short = 'o', long = "org")]
7+
#[arg(help = "The organization ID or slug.")]
8+
pub(super) org: Option<String>,
9+
10+
#[arg(short = 'p', long = "project")]
11+
#[arg(help = "The project ID or slug.")]
12+
pub(super) project: Option<String>,
13+
}

src/commands/logs/list.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use anyhow::Result;
2+
use clap::Args;
3+
4+
use crate::api::{Api, FetchEventsOptions};
5+
use crate::config::Config;
6+
use crate::utils::formatting::Table;
7+
8+
use super::common_args::CommonLogsArgs;
9+
10+
/// Arguments for listing logs
11+
#[derive(Args)]
12+
pub(super) struct ListLogsArgs {
13+
#[command(flatten)]
14+
pub(super) common: CommonLogsArgs,
15+
16+
#[arg(long = "max-rows")]
17+
#[arg(help = "Maximum number of rows to print.")]
18+
pub(super) max_rows: Option<usize>,
19+
20+
#[arg(long = "per-page", default_value = "100")]
21+
#[arg(help = "Number of log entries per request (max 1000).")]
22+
pub(super) per_page: usize,
23+
24+
#[arg(long = "query", default_value = "")]
25+
#[arg(help = "Query to filter logs. Example: \"level:error\"")]
26+
pub(super) query: String,
27+
28+
#[arg(long = "live")]
29+
#[arg(help = "Live-tail logs (not implemented yet).")]
30+
pub(super) live: bool,
31+
}
32+
33+
pub(super) fn execute(args: ListLogsArgs) -> Result<()> {
34+
let config = Config::current();
35+
let (default_org, default_project) = config.get_org_and_project_defaults();
36+
37+
let org = args.common.org.or(default_org).ok_or_else(|| {
38+
anyhow::anyhow!("No organization specified. Use --org or set a default in config.")
39+
})?;
40+
let project = args.common.project.or(default_project).ok_or_else(|| {
41+
anyhow::anyhow!("No project specified. Use --project or set a default in config.")
42+
})?;
43+
44+
let api = Api::current();
45+
46+
let query = if args.query.is_empty() {
47+
None
48+
} else {
49+
Some(args.query.as_str())
50+
};
51+
let fields = [
52+
"sentry.item_id",
53+
"trace",
54+
"severity",
55+
"timestamp",
56+
"message",
57+
];
58+
59+
let options = FetchEventsOptions {
60+
project_id: Some(&project),
61+
query,
62+
per_page: Some(args.per_page),
63+
stats_period: Some("1h"),
64+
..Default::default()
65+
};
66+
67+
let logs = api
68+
.authenticated()?
69+
.fetch_organization_events(&org, "ourlogs", &fields, options)?;
70+
71+
let mut table = Table::new();
72+
table
73+
.title_row()
74+
.add("Item ID")
75+
.add("Timestamp")
76+
.add("Severity")
77+
.add("Message")
78+
.add("Trace");
79+
80+
let max_rows = std::cmp::min(logs.len(), args.max_rows.unwrap_or(usize::MAX));
81+
82+
if let Some(logs) = logs.get(..max_rows) {
83+
for log in logs {
84+
let row = table.add_row();
85+
row.add(&log.item_id)
86+
.add(&log.timestamp)
87+
.add(log.severity.as_deref().unwrap_or(""))
88+
.add(log.message.as_deref().unwrap_or(""))
89+
.add(log.trace.as_deref().unwrap_or(""));
90+
}
91+
}
92+
93+
if table.is_empty() {
94+
println!("No logs found");
95+
} else {
96+
table.print();
97+
}
98+
99+
Ok(())
100+
}

src/commands/logs/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
pub mod common_args;
2+
3+
mod list;
4+
5+
use self::list::ListLogsArgs;
6+
use super::derive_parser::{SentryCLI, SentryCLICommand};
7+
use anyhow::Result;
8+
use clap::ArgMatches;
9+
use clap::{Args, Command, Parser as _, Subcommand};
10+
11+
const LIST_ABOUT: &str = "List logs from your organization";
12+
13+
#[derive(Args)]
14+
pub(super) struct LogsArgs {
15+
#[command(subcommand)]
16+
subcommand: LogsSubcommand,
17+
}
18+
19+
#[derive(Subcommand)]
20+
#[command(about = "Manage logs in Sentry")]
21+
#[command(long_about = "Manage and query logs in Sentry. \
22+
This command provides access to log entries and supports live-tailing functionality.")]
23+
enum LogsSubcommand {
24+
#[command(about = LIST_ABOUT)]
25+
#[command(long_about = format!("{LIST_ABOUT}. \
26+
Query and filter log entries from your Sentry projects. \
27+
Supports filtering by time period, log level, and custom queries."))]
28+
List(ListLogsArgs),
29+
}
30+
31+
pub(super) fn make_command(command: Command) -> Command {
32+
LogsSubcommand::augment_subcommands(command)
33+
}
34+
35+
pub(super) fn execute(_: &ArgMatches) -> Result<()> {
36+
let SentryCLICommand::Logs(LogsArgs { subcommand }) = SentryCLI::parse().command else {
37+
unreachable!("expected logs subcommand");
38+
};
39+
40+
match subcommand {
41+
LogsSubcommand::List(args) => list::execute(args),
42+
}
43+
}

src/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod files;
2828
mod info;
2929
mod issues;
3030
mod login;
31+
mod logs;
3132
mod mobile_app;
3233
mod monitors;
3334
mod organizations;
@@ -57,6 +58,7 @@ macro_rules! each_subcommand {
5758
$mac!(info);
5859
$mac!(issues);
5960
$mac!(login);
61+
$mac!(logs);
6062
#[cfg(feature = "unstable-mobile-app")]
6163
$mac!(mobile_app);
6264
$mac!(monitors);
@@ -95,6 +97,7 @@ const UPDATE_NAGGER_CMDS: &[&str] = &[
9597
"info",
9698
"issues",
9799
"login",
100+
"logs",
98101
"organizations",
99102
"projects",
100103
"releases",

src/commands/send_metric/mod.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,10 @@ pub(super) fn make_command(command: Command) -> Command {
5959
}
6060

6161
pub(super) fn execute(_: &ArgMatches) -> Result<()> {
62-
// When adding a new subcommand to the derive_parser SentryCLI, replace the line below with the following:
63-
// let subcommand = match SentryCLI::parse().command {
64-
// SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) => subcommand,
65-
// _ => panic!("expected send-metric subcommand"),
66-
// };
67-
let SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) = SentryCLI::parse().command;
62+
let subcommand = match SentryCLI::parse().command {
63+
SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) => subcommand,
64+
_ => unreachable!("expected send-metric subcommand"),
65+
};
6866

6967
log::warn!("{DEPRECATION_MESSAGE}");
7068

tests/integration/_cases/help/help.trycmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Commands:
1717
info Print information about the configuration and verify authentication.
1818
issues Manage issues in Sentry.
1919
login Authenticate with the Sentry server.
20+
logs Manage logs in Sentry
2021
monitors Manage cron monitors on Sentry.
2122
organizations Manage organizations on Sentry.
2223
projects Manage projects on Sentry.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
$ sentry-cli logs --help
2+
? success
3+
Manage logs in Sentry.
4+
5+
Usage: sentry-cli[EXE] logs [OPTIONS] <COMMAND>
6+
7+
Commands:
8+
list List logs from your organization.
9+
help Print this message or the help of the given subcommand(s)
10+
11+
Options:
12+
-o, --org <ORG>
13+
The organization ID or slug.
14+
15+
-p, --project <PROJECT>
16+
The project ID or slug.
17+
18+
--live
19+
Live-tail logs (not implemented yet).
20+
21+
--header <KEY:VALUE>
22+
Custom headers that should be attached to all requests
23+
in key:value format.
24+
25+
--auth-token <AUTH_TOKEN>
26+
Use the given Sentry auth token.
27+
28+
--log-level <LOG_LEVEL>
29+
Set the log output verbosity. [possible values: trace, debug, info, warn, error]
30+
31+
--quiet
32+
Do not print any output while preserving correct exit code. This flag is currently
33+
implemented only for selected subcommands.
34+
35+
[aliases: silent]
36+
37+
-h, --help
38+
Print help
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$ sentry-cli logs list --org wat-org --project wat-project --max-rows 0
2+
? success
3+
No logs found

0 commit comments

Comments
 (0)