Improve error logging and observability (#4443)

* Replace actix tracing with custom error tracing

* Fix logging

* wip: JSON logging

* Use LABRINTH_FORMAT to change to JSON output

* sqlx fix?

* CI fix

* Add tracing span info to HTTP requests

* Merge Result and Option error wrapping

* Add http.authorized to tracing
This commit is contained in:
aecsocket
2025-10-03 14:02:20 +01:00
committed by GitHub
parent 7e84659249
commit b96c5cd5ab
9 changed files with 326 additions and 31 deletions

View File

@@ -87,53 +87,57 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("Environment Error")]
#[error(transparent)]
Internal(eyre::Report),
#[error(transparent)]
Request(eyre::Report),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Environment error")]
Env(#[from] dotenvy::Error),
#[error("Error while uploading file: {0}")]
FileHosting(#[from] FileHostingError),
#[error("Database Error: {0}")]
#[error("Database error: {0}")]
Database(#[from] crate::database::models::DatabaseError),
#[error("Database Error: {0}")]
#[error("SQLx database error: {0}")]
SqlxDatabase(#[from] sqlx::Error),
#[error("Database Error: {0}")]
#[error("Redis database error: {0}")]
RedisDatabase(#[from] redis::RedisError),
#[error("Clickhouse Error: {0}")]
#[error("Clickhouse error: {0}")]
Clickhouse(#[from] clickhouse::error::Error),
#[error("Internal server error: {0}")]
#[error("XML error: {0}")]
Xml(String),
#[error("Deserialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("Authentication Error: {0}")]
#[error("Authentication error: {0}")]
Authentication(#[from] crate::auth::AuthenticationError),
#[error("Authentication Error: {0}")]
#[error("Authentication error: {0}")]
CustomAuthentication(String),
#[error("Invalid Input: {0}")]
InvalidInput(String),
#[error("Error while validating input: {0}")]
Validation(String),
#[error("Search Error: {0}")]
#[error("Search error: {0}")]
Search(#[from] meilisearch_sdk::errors::Error),
#[error("Indexing Error: {0}")]
#[error("Indexing error: {0}")]
Indexing(#[from] crate::search::indexing::IndexingError),
#[error("Payments Error: {0}")]
#[error("Payments error: {0}")]
Payments(String),
#[error("Discord Error: {0}")]
#[error("Discord error: {0}")]
Discord(String),
#[error("Slack Webhook Error: {0}")]
#[error("Slack webhook error: {0}")]
Slack(String),
#[error("Captcha Error. Try resubmitting the form.")]
#[error("Captcha error. Try resubmitting the form.")]
Turnstile,
#[error("Error while decoding Base62: {0}")]
Decoding(#[from] ariadne::ids::DecodingError),
#[error("Image Parsing Error: {0}")]
#[error("Image parsing error: {0}")]
ImageParse(#[from] image::ImageError),
#[error("Password Hashing Error: {0}")]
#[error("Password hashing error: {0}")]
PasswordHashing(#[from] argon2::password_hash::Error),
#[error("{0}")]
Mail(#[from] crate::queue::email::MailError),
#[error("Error while rerouting request: {0:?}")]
Reroute(#[from] reqwest::Error),
#[error("Unable to read Zip Archive: {0}")]
#[error("Unable to read zip archive: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
@@ -141,7 +145,7 @@ pub enum ApiError {
NotFound,
#[error("Conflict: {0}")]
Conflict(String),
#[error("External tax compliance API Error")]
#[error("External tax compliance API error")]
TaxComplianceApi,
#[error(transparent)]
TaxProcessor(#[from] crate::util::anrok::AnrokError),
@@ -157,6 +161,8 @@ impl ApiError {
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
crate::models::error::ApiError {
error: match self {
ApiError::Internal(..) => "internal_error",
Self::Request(..) => "request_error",
ApiError::Env(..) => "environment_error",
ApiError::Database(..) => "database_error",
ApiError::SqlxDatabase(..) => "database_error",
@@ -197,6 +203,9 @@ impl ApiError {
impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> StatusCode {
match self {
ApiError::Internal(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Request(..) => StatusCode::BAD_REQUEST,
ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST,
ApiError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
@@ -209,7 +218,6 @@ impl actix_web::ResponseError for ApiError {
ApiError::Search(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Indexing(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST,
ApiError::Validation(..) => StatusCode::BAD_REQUEST,
ApiError::Payments(..) => StatusCode::FAILED_DEPENDENCY,
ApiError::Discord(..) => StatusCode::FAILED_DEPENDENCY,

View File

@@ -14,6 +14,7 @@ use crate::{
use actix_web::{HttpRequest, HttpResponse, web};
use ariadne::ids::base62_impl::to_base62;
use chrono::{DateTime, Duration, Utc};
use eyre::eyre;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use sqlx::postgres::types::PgInterval;
@@ -331,7 +332,7 @@ pub async fn revenue_get(
let duration: PgInterval = Duration::minutes(resolution_minutes as i64)
.try_into()
.map_err(|_| {
ApiError::InvalidInput("Invalid resolution_minutes".to_string())
ApiError::Request(eyre!("Invalid `resolution_minutes`"))
})?;
// Get the revenue data
let project_ids = project_ids.unwrap_or_default();

View File

@@ -12,6 +12,7 @@ use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::v3::project_creation::CreateError;
use crate::util::error::Context;
use crate::util::img::delete_old_images;
use crate::util::routes::read_limited_from_payload;
use crate::util::validate::validation_errors_to_string;
@@ -20,6 +21,7 @@ use actix_web::web::Data;
use actix_web::{HttpRequest, HttpResponse, web};
use ariadne::ids::base62_impl::parse_base62;
use chrono::Utc;
use eyre::eyre;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -335,10 +337,8 @@ pub async fn collection_edit(
project_id, &**pool, &redis,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"The specified project {project_id} does not exist!"
))
.wrap_request_err_with(|| {
eyre!("The specified project {project_id} does not exist!")
})?;
validated_project_ids.push(project.inner.id.0);
}