Skip to content

feat(logs): support log streaming #2666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
116 changes: 114 additions & 2 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,29 @@ impl<'a> AuthenticatedApi<'a> {
Ok(rv)
}

/// Fetch organization events from the specified dataset
pub fn fetch_organization_events(
&self,
org: &str,
options: &FetchEventsOptions,
) -> ApiResult<Vec<LogEntry>> {
let params = options.to_query_params();
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(
&self,
Expand Down Expand Up @@ -1390,6 +1413,78 @@ 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
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
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<usize>,
/// 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<String> {
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<ApiRequest> {
self.api
Expand Down Expand Up @@ -2343,7 +2438,7 @@ pub struct ProcessedEvent {
pub tags: Option<Vec<ProcessedEventTag>>,
}

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ProcessedEventUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
Expand Down Expand Up @@ -2377,7 +2472,7 @@ impl fmt::Display for ProcessedEventUser {
}
}

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ProcessedEventTag {
pub key: String,
pub value: String,
Expand All @@ -2401,3 +2496,20 @@ pub struct Region {
pub struct RegionResponse {
pub regions: Vec<Region>,
}

/// Response structure for logs API
#[derive(Debug, Deserialize)]
struct LogsResponse {
data: Vec<LogEntry>,
}

/// Log entry structure from the logs API
#[derive(Debug, Deserialize, Clone)]
pub struct LogEntry {
#[serde(rename = "sentry.item_id")]
pub item_id: String,
pub trace: Option<String>,
pub severity: Option<String>,
pub timestamp: String,
pub message: Option<String>,
}
2 changes: 2 additions & 0 deletions src/commands/derive_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -32,5 +33,6 @@ pub(super) struct SentryCLI {

#[derive(Subcommand)]
pub(super) enum SentryCLICommand {
Logs(LogsArgs),
SendMetric(SendMetricArgs),
}
Loading