Skip to content

Commit 34dc02b

Browse files
authored
fix(log): Capture service errors only once (#558)
For every service error, we received two issues in Sentry: 1. Captured within the service by the spawned task itself 2. Captured by the web middleware when unwinding the error Since tasks can outlive their requests, we need to keep capturing inside the service. Consequently, we now skip error tracking for service error kinds in the API layer. There are a few exceptions where a service error is constructed outside of the spawned task, which is now tracked explicitly.
1 parent 4e78a45 commit 34dc02b

3 files changed

Lines changed: 31 additions & 15 deletions

File tree

objectstore-server/src/endpoints/batch.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,10 @@ async fn got_to_part(
232232
.meter_stream(stream, context)
233233
.try_collect::<BytesMut>()
234234
.await
235-
.map_err(|e| ApiError::Service(e.into()))?
235+
.map_err(|e| {
236+
objectstore_log::error!(!!&e, "failed to collect payload stream");
237+
ApiError::Service(e.into())
238+
})?
236239
.freeze();
237240

238241
let mut metadata_headers = metadata.to_headers("").map_err(|err| {
@@ -317,6 +320,9 @@ fn create_success_part(
317320
}
318321

319322
fn create_error_part(idx: usize, error: &ApiError) -> Part {
323+
// Capture the error explicitly, as it is not converted via `IntoResponse` here.
324+
error.capture();
325+
320326
let mut headers = HeaderMap::new();
321327
insert_index_header(&mut headers, idx);
322328
insert_status_header(&mut headers, error.status());

objectstore-server/src/endpoints/common.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ impl ApiError {
7979
ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
8080
ApiError::Batch(BatchError::RateLimited) => StatusCode::TOO_MANY_REQUESTS,
8181
ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
82-
objectstore_log::error!(!!self, "error serializing batch response");
8382
StatusCode::INTERNAL_SERVER_ERROR
8483
}
8584

@@ -89,10 +88,7 @@ impl ApiError {
8988
ApiError::Auth(AuthError::UnknownKey) => StatusCode::UNAUTHORIZED,
9089
ApiError::Auth(AuthError::UnsupportedPresignedMethod) => StatusCode::FORBIDDEN,
9190
ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
92-
ApiError::Auth(AuthError::InternalError(_)) => {
93-
objectstore_log::error!(!!self, "auth system error");
94-
StatusCode::INTERNAL_SERVER_ERROR
95-
}
91+
ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,
9692

9793
ApiError::Service(ServiceError::Client(_)) => StatusCode::BAD_REQUEST,
9894
ApiError::Service(ServiceError::Metadata(_)) => StatusCode::BAD_REQUEST,
@@ -102,21 +98,31 @@ impl ApiError {
10298
ApiError::Service(ServiceError::InvalidUploadId(_)) => StatusCode::BAD_REQUEST,
10399
ApiError::Service(ServiceError::AtCapacity) => StatusCode::TOO_MANY_REQUESTS,
104100
ApiError::Service(ServiceError::NotImplemented) => StatusCode::NOT_IMPLEMENTED,
105-
ApiError::Service(_) => {
106-
objectstore_log::error!(!!self, "error handling request");
107-
StatusCode::INTERNAL_SERVER_ERROR
108-
}
101+
ApiError::Service(_) => StatusCode::INTERNAL_SERVER_ERROR,
109102

110-
ApiError::Internal(_) => {
111-
objectstore_log::error!(!!self, "internal error");
112-
StatusCode::INTERNAL_SERVER_ERROR
113-
}
103+
ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
104+
}
105+
}
106+
107+
/// Reports this error to error tracking if it indicates a server fault (5xx status).
108+
///
109+
/// Call this exactly once wherever an `ApiError` is serialized into a client-visible
110+
/// response: standalone responses ([`IntoResponse`]) and batch response parts.
111+
pub fn capture(&self) {
112+
// Captured at the source in the service layer to prevent double-logging.
113+
if matches!(self, ApiError::Service(_)) {
114+
return;
115+
}
116+
117+
if self.status().is_server_error() {
118+
objectstore_log::error!(!!self, "error handling request");
114119
}
115120
}
116121
}
117122

118123
impl IntoResponse for ApiError {
119124
fn into_response(self) -> Response {
125+
self.capture();
120126
let body = ApiErrorResponse::from_error(&self);
121127
(self.status(), Json(body)).into_response()
122128
}

objectstore-service/src/concurrency.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ where
193193
}
194194
.bind_hub(new_hub),
195195
);
196-
rx.await.map_err(|_| Error::Dropped)?
196+
197+
rx.await.map_err(|_| {
198+
objectstore_log::error!(!!&Error::Dropped, operation, "Task failed");
199+
Error::Dropped
200+
})?
197201
}
198202

199203
#[cfg(test)]

0 commit comments

Comments
 (0)