Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Add `otp` and `two[-_]factor` to default scrubbing rules. ([#5250](https://github.com/getsentry/relay/pull/5250))
- Add event merging logic for Playstation crashes. ([#5228](https://github.com/getsentry/relay/pull/5228))
- Implement PII scrubbing for V2 spans. ([#5168](https://github.com/getsentry/relay/pull/5168))
- Add vercel log drain endpoint. ([#5212](https://github.com/getsentry/relay/pull/5212))

**Bug Fixes**:

Expand Down
2 changes: 1 addition & 1 deletion relay-ourlogs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ mod vercel_to_sentry;

pub use self::otel_to_sentry::otel_to_sentry_log;
pub use self::size::calculate_size;
pub use self::vercel_to_sentry::vercel_log_to_sentry_log;
pub use self::vercel_to_sentry::{VercelLog, vercel_log_to_sentry_log};

pub use opentelemetry_proto::tonic::logs::v1 as otel_logs;
2 changes: 1 addition & 1 deletion relay-ourlogs/src/vercel_to_sentry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct VercelLog {
pub timestamp: i64,
/// Identifier for the Vercel project.
pub project_id: String,
// Log severity level.
/// Log severity level.
pub level: VercelLogLevel,
/// Log message content (may be truncated if over 256 KB).
pub message: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions relay-server/src/endpoints/integrations/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod otlp;
pub mod vercel;
51 changes: 51 additions & 0 deletions relay-server/src/endpoints/integrations/vercel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use axum::extract::DefaultBodyLimit;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{MethodRouter, post};
use relay_config::Config;
use relay_dynamic_config::Feature;

use crate::endpoints::common;
use crate::envelope::ContentType;
use crate::extractors::{IntegrationBuilder, RawContentType};
use crate::integrations::{LogsIntegration, VercelLogDrainFormat};
use crate::service::ServiceState;

/// All routes configured for the Vercel integration.
///
/// The integration currently supports the following endpoints:
/// - Vercel Log Drain
pub fn routes(config: &Config) -> axum::Router<ServiceState> {
axum::Router::new()
.route("/logs", logs::route(config))
.route("/logs/", logs::route(config))
}

mod logs {
use super::*;

async fn handle(
content_type: RawContentType,
state: ServiceState,
builder: IntegrationBuilder,
) -> axum::response::Result<impl IntoResponse> {
let format = match ContentType::from(content_type.as_ref()) {
ContentType::Json => VercelLogDrainFormat::Json,
ContentType::NdJson => VercelLogDrainFormat::NdJson,
_ => return Ok(StatusCode::UNSUPPORTED_MEDIA_TYPE),
};

let envelope = builder
.with_type(LogsIntegration::VercelDrainLog { format })
.with_required_feature(Feature::VercelLogDrainEndpoint)
.build();

common::handle_envelope(&state, envelope).await?;

Ok(StatusCode::ACCEPTED)
}

pub fn route(config: &Config) -> MethodRouter<ServiceState> {
post(handle).route_layer(DefaultBodyLimit::max(config.max_envelope_size()))
}
}
1 change: 1 addition & 0 deletions relay-server/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub fn routes(config: &Config) -> Router<ServiceState>{
// configured by users or protocols may force a specific variant.
let integration_routes = Router::new()
.nest("/api/{project_id}/integration/otlp", integrations::otlp::routes(config))
.nest("/api/{project_id}/integration/vercel", integrations::vercel::routes(config))
.route_layer(middlewares::cors());

// NOTE: If you add a new (non-experimental) route here, please also list it in
Expand Down
5 changes: 5 additions & 0 deletions relay-server/src/envelope/content_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum ContentType {
Text,
/// `application/json`
Json,
/// `application/x-ndjson`
NdJson,
/// `application/x-msgpack`
MsgPack,
/// `application/octet-stream`
Expand Down Expand Up @@ -45,6 +47,7 @@ impl ContentType {
match self {
Self::Text => "text/plain",
Self::Json => "application/json",
Self::NdJson => "application/x-ndjson",
Self::MsgPack => "application/x-msgpack",
Self::OctetStream => "application/octet-stream",
Self::Minidump => "application/x-dmp",
Expand Down Expand Up @@ -74,6 +77,8 @@ impl ContentType {
Some(Self::Text)
} else if ct.eq_ignore_ascii_case(Self::Json.as_str()) {
Some(Self::Json)
} else if ct.eq_ignore_ascii_case(Self::NdJson.as_str()) {
Some(Self::NdJson)
} else if ct.eq_ignore_ascii_case(Self::MsgPack.as_str()) {
Some(Self::MsgPack)
} else if ct.eq_ignore_ascii_case(Self::OctetStream.as_str()) {
Expand Down
4 changes: 4 additions & 0 deletions relay-server/src/envelope/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ impl Item {
(DataCategory::LogByte, self.len().max(1)),
(DataCategory::LogItem, item_count),
],
Some(Integration::Logs(LogsIntegration::VercelDrainLog { .. })) => smallvec![
(DataCategory::LogByte, self.len().max(1)),
(DataCategory::LogItem, item_count),
],
Some(Integration::Spans(SpansIntegration::OtelV1 { .. })) => {
smallvec![
(DataCategory::Span, item_count),
Expand Down
14 changes: 14 additions & 0 deletions relay-server/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ define_integrations!(
"application/vnd.sentry.integration.otel.logs+protobuf" => Integration::Logs(LogsIntegration::OtelV1 { format: OtelFormat::Protobuf }),
"application/vnd.sentry.integration.otel.spans+json" => Integration::Spans(SpansIntegration::OtelV1 { format: OtelFormat::Json }),
"application/vnd.sentry.integration.otel.spans+protobuf" => Integration::Spans(SpansIntegration::OtelV1 { format: OtelFormat::Protobuf }),
"application/vnd.sentry.integration.vercel.logs+json" => Integration::Logs(LogsIntegration::VercelDrainLog { format: VercelLogDrainFormat::Json }),
"application/vnd.sentry.integration.vercel.logs+ndjson" => Integration::Logs(LogsIntegration::VercelDrainLog { format: VercelLogDrainFormat::NdJson }),
);

/// An exhaustive list of all integrations supported by Relay.
Expand Down Expand Up @@ -74,6 +76,10 @@ pub enum LogsIntegration {
///
/// Supports OTeL's [`LogsData`](opentelemetry_proto::tonic::logs::v1::LogsData).
OtelV1 { format: OtelFormat },
/// The Vercel Log Drain integration.
///
/// Supports the [`relay_ourlogs::VercelLog`] format.
VercelDrainLog { format: VercelLogDrainFormat },
}

/// All span integrations supported by Relay.
Expand All @@ -93,3 +99,11 @@ pub enum OtelFormat {
/// OTeL data in a JSON container.
Json,
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum VercelLogDrainFormat {
// Vercel Log Drain data in a JSON array payload
Json,
// Vercel Log Drain data in a newline delimited JSON payload
NdJson,
}
2 changes: 2 additions & 0 deletions relay-server/src/processing/logs/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::integrations::{Integration, LogsIntegration};
use crate::managed::RecordKeeper;

mod otel;
mod vercel;

/// Expands a list of [`Integration`] items into `result`.
///
Expand Down Expand Up @@ -43,6 +44,7 @@ pub fn expand_into(

let result = match integration {
LogsIntegration::OtelV1 { format } => otel::expand(format, &payload, produce),
LogsIntegration::VercelDrainLog { format } => vercel::expand(format, &payload, produce),
};

match result {
Expand Down
52 changes: 52 additions & 0 deletions relay-server/src/processing/logs/integrations/vercel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use relay_event_schema::protocol::OurLog;
use relay_ourlogs::VercelLog;

use crate::integrations::VercelLogDrainFormat;
use crate::processing::logs::{Error, Result};
use crate::services::outcome::DiscardReason;

/// Expands Vercel logs into the [`OurLog`] format.
pub fn expand<F>(format: VercelLogDrainFormat, payload: &[u8], mut produce: F) -> Result<()>
where
F: FnMut(OurLog),
{
let mut count: i32 = 0;

match format {
VercelLogDrainFormat::Json => {
let logs = serde_json::from_slice::<Vec<VercelLog>>(payload).map_err(|e| {
relay_log::debug!(
error = &e as &dyn std::error::Error,
"Failed to parse logs data as JSON"
);
Error::Invalid(DiscardReason::InvalidJson)
})?;

for log in logs {
count += 1;
let ourlog = relay_ourlogs::vercel_log_to_sentry_log(log);
produce(ourlog);
}
}
VercelLogDrainFormat::NdJson => {
for line in payload.split(|&b| b == b'\n') {
if line.is_empty() {
continue;
}

if let Ok(log) = serde_json::from_slice::<VercelLog>(line) {
count += 1;
let ourlog = relay_ourlogs::vercel_log_to_sentry_log(log);
produce(ourlog);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inconsistent Error Handling in NDJSON Parsing

The NDJSON parser silently drops individual lines that fail to parse, unlike the JSON array parser which fails the entire payload on any error. This inconsistent error handling can lead to silent data loss and make debugging challenging.

Fix in Cursor Fix in Web

}
}
}

if count == 0 {
relay_log::debug!("Failed to parse any logs from vercel log drain payload");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does output when you get empty arrays, but this is unexpected from the log drain endpoint.

After implementing this I also realized that we could return the count from expand, and use that accordingly, we can make a similar refactor to the otlp integration.

return Err(Error::Invalid(DiscardReason::InvalidJson));
}

Ok(())
}
19 changes: 19 additions & 0 deletions tests/integration/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@ def send_otel_logs(

response.raise_for_status()

def send_vercel_logs(
self,
project_id,
data=None,
headers=None,
dsn_key_idx=0,
dsn_key=None,
):

if dsn_key is None:
dsn_key = self.get_dsn_public_key(project_id, dsn_key_idx)

url = f"/api/{project_id}/integration/vercel/logs?sentry_key={dsn_key}"

response = self.post(url, headers=headers, data=data)

response.raise_for_status()
return response

def send_options(self, project_id, headers=None, dsn_key_idx=0):
headers = {
"X-Sentry-Auth": self.get_auth_header(project_id, dsn_key_idx),
Expand Down
Loading