diff --git a/.gitignore b/.gitignore index d3906e740..c6510702a 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ node_modules/ /prds .DS_Store +.aider* diff --git a/api/Cargo.toml b/api/Cargo.toml index 679a14493..199fc0aff 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -74,8 +74,9 @@ redis = { version = "0.27.5", features = [ "tls-rustls-webpki-roots", ] } resend-rs = "0.10.0" -sentry = { version = "0.37.0", features = ["tokio", "sentry-tracing"] } +sentry = { version = "0.37.0", features = ["tokio"] } sentry-tower = { version = "0.37.0", features = ["axum", "http"] } +sentry-tracing = { version = "0.37.0"} serde_urlencoded = "0.7.1" snowflake-api = "0.11.0" tempfile = "3.10.1" diff --git a/api/libs/middleware/Cargo.toml b/api/libs/middleware/Cargo.toml index 1076f98b9..45553f35d 100644 --- a/api/libs/middleware/Cargo.toml +++ b/api/libs/middleware/Cargo.toml @@ -22,9 +22,16 @@ serde_urlencoded = { workspace = true } # Web framework dependencies axum = { workspace = true } +tower = { workspace = true } tower-http = { workspace = true } futures = { workspace = true } +# Sentry dependencies +sentry = { workspace = true } +sentry-tower = { workspace = true } +sentry-tracing = { workspace = true } +tracing-subscriber = { workspace = true } + # Internal workspace dependencies database = { path = "../database" } diff --git a/api/libs/middleware/src/error.rs b/api/libs/middleware/src/error.rs new file mode 100644 index 000000000..0967d81e9 --- /dev/null +++ b/api/libs/middleware/src/error.rs @@ -0,0 +1,192 @@ +//! Error handling middleware with Sentry integration +//! +//! This module provides middleware for error tracking and logging with Sentry + +use std::env; +use std::fmt::Display; +use anyhow::Error; +use axum::extract::Request; +use sentry::protocol::{Event, Level}; +use tower::ServiceBuilder; +use tracing::{error, warn}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +/// Creates a Sentry layer for the Axum application +/// +/// This function configures Sentry error tracking for an Axum application. +/// It adds two layers: +/// 1. NewSentryLayer - Creates a new Sentry hub for each request +/// 2. SentryHttpLayer - Automatically creates transactions for HTTP requests +/// +/// # Returns +/// A ServiceBuilder with the Sentry layers configured +pub fn sentry_layer() -> ServiceBuilder, axum::extract::Request>, + tower::layer::util::Identity + > +>> { + ServiceBuilder::new() + .layer(sentry_tower::NewSentryLayer::::new_from_top()) + .layer(sentry_tower::SentryHttpLayer::with_transaction()) +} + +/// Initializes the Sentry client with proper configuration +/// +/// # Arguments +/// * `dsn` - The Sentry DSN (Data Source Name) +/// +/// # Returns +/// A Sentry client guard that keeps the client alive +pub fn init_sentry(dsn: &str) -> Option { + let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()); + let is_development = environment == "development"; + + if is_development { + return None; + } + + let options = sentry::ClientOptions { + release: sentry::release_name!(), + environment: Some(environment.into()), + traces_sample_rate: 1.0, + attach_stacktrace: true, + default_integrations: true, + ..Default::default() + }; + + Some(sentry::init((dsn, options))) +} + +/// Determines if Sentry should be enabled based on the environment +/// +/// # Returns +/// true if Sentry should be enabled, false otherwise +pub fn is_sentry_enabled() -> bool { + let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()); + environment != "development" +} + +/// Initializes the tracing subscriber with optional Sentry integration +/// +/// This function creates a tracing subscriber and conditionally adds Sentry integration +/// based on the environment. +/// +/// # Arguments +/// * `env_filter` - The environment filter to use +/// +/// # Returns +/// Unit, after initializing the global subscriber +pub fn init_tracing_subscriber(env_filter: EnvFilter) { + // Create a base registry + let registry = tracing_subscriber::registry() + .with(env_filter) + .with(tracing_subscriber::fmt::layer()); + + if is_sentry_enabled() { + // Add Sentry layer if Sentry is enabled + registry.with(sentry_tracing::layer()).init(); + } else { + // Otherwise just initialize the registry as is + registry.init(); + } +} + +/// Report an error to Sentry and also log it with tracing +/// +/// This function should be used when you want to both log an error and report it to Sentry. +/// It accepts any error type that implements Display + Send + Sync + 'static. +/// +/// # Arguments +/// * `err` - The error to report +/// * `msg` - Optional additional context message +/// +/// # Examples +/// ``` +/// use middleware::error::report_error; +/// +/// if let Err(e) = some_operation() { +/// report_error(e, Some("Failed during important operation")); +/// return some_fallback(); +/// } +/// ``` +pub fn report_error(err: E, msg: Option<&str>) +where + E: Display + Send + Sync + 'static +{ + let error_msg = if let Some(context) = msg { + format!("{}: {}", context, err) + } else { + format!("{}", err) + }; + + // Log the error with tracing + error!("{}", error_msg); + + // Report to Sentry + sentry::capture_event(Event { + message: Some(error_msg), + level: Level::Error, + ..Default::default() + }); +} + +/// Report a warning to Sentry and also log it with tracing +/// +/// Similar to report_error but for warning level events +/// +/// # Arguments +/// * `warning` - The warning message +/// * `context` - Optional additional context +pub fn report_warning(warning: &str, context: Option<&str>) { + let warning_msg = if let Some(ctx) = context { + format!("{}: {}", ctx, warning) + } else { + warning.to_string() + }; + + // Log the warning with tracing + warn!("{}", warning_msg); + + // Report to Sentry + sentry::capture_event(Event { + message: Some(warning_msg), + level: Level::Warning, + ..Default::default() + }); +} + +/// Capture an anyhow::Error and report it to Sentry +/// +/// This function is particularly useful for handling anyhow errors, +/// which are commonly used in the codebase. +/// +/// # Arguments +/// * `err` - The anyhow error to report +/// * `msg` - Optional additional context message +pub fn capture_anyhow(err: &Error, msg: Option<&str>) { + // Extract the error chain for better context + let mut err_msg = if let Some(context) = msg { + format!("{}: {}", context, err) + } else { + format!("{}", err) + }; + + // Add the error chain for better context + let mut source = err.source(); + while let Some(err) = source { + err_msg.push_str(&format!("\nCaused by: {}", err)); + source = err.source(); + } + + // Log the error with tracing + error!("{}", err_msg); + + // Capture the full error chain in Sentry + sentry::capture_event(Event { + message: Some(err_msg), + level: Level::Error, + ..Default::default() + }); +} \ No newline at end of file diff --git a/api/libs/middleware/src/lib.rs b/api/libs/middleware/src/lib.rs index eeef20075..e0638eabd 100644 --- a/api/libs/middleware/src/lib.rs +++ b/api/libs/middleware/src/lib.rs @@ -6,8 +6,18 @@ pub mod auth; pub mod cors; pub mod types; +pub mod error; // Re-export commonly used types pub use auth::auth; pub use cors::cors; +pub use error::{ + sentry_layer, + init_sentry, + is_sentry_enabled, + init_tracing_subscriber, + report_error, + report_warning, + capture_anyhow +}; pub use types::{AuthenticatedUser, OrganizationMembership, TeamMembership}; diff --git a/api/server/src/main.rs b/api/server/src/main.rs index 10aa697e3..2c5371de0 100644 --- a/api/server/src/main.rs +++ b/api/server/src/main.rs @@ -5,7 +5,7 @@ use std::env; use std::sync::Arc; use axum::{Extension, Router, extract::Request}; -use middleware::cors::cors; +use middleware::{cors::cors, error::{init_sentry, sentry_layer, init_tracing_subscriber}}; use database::{self, pool::init_pools}; use diesel::{Connection, PgConnection}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; @@ -30,33 +30,21 @@ async fn main() { .install_default() .expect("Failed to install default crypto provider"); - // Only initialize Sentry if not in development environment - let _guard = if !is_development { - Some(sentry::init(( - "https://a417fbed1de30d2714a8afbe38d5bc1b@o4505360096428032.ingest.us.sentry.io/4507360721043456", - sentry::ClientOptions { - release: sentry::release_name!(), - environment: Some(environment.clone().into()), - traces_sample_rate: 1.0, - ..Default::default() - } - ))) - } else { - None - }; + // Initialize Sentry using our middleware helper + let _guard = init_sentry( + "https://a417fbed1de30d2714a8afbe38d5bc1b@o4505360096428032.ingest.us.sentry.io/4507360721043456" + ); - tracing_subscriber::registry() - .with( - EnvFilter::try_from_default_env() - .unwrap_or_else(|_| { - let log_level = env::var("LOG_LEVEL") - .unwrap_or_else(|_| "warn".to_string()) - .to_uppercase(); - EnvFilter::new(log_level) - }), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); + // Set up the tracing subscriber with conditional Sentry integration + let log_level = env::var("LOG_LEVEL") + .unwrap_or_else(|_| "warn".to_string()) + .to_uppercase(); + + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(log_level)); + + // Initialize the tracing subscriber with Sentry integration using our middleware helper + init_tracing_subscriber(env_filter); if let Err(e) = init_pools().await { tracing::error!("Failed to initialize database pools: {}", e); @@ -78,7 +66,7 @@ async fn main() { let (shutdown_tx, _) = broadcast::channel::<()>(1); let shutdown_tx = Arc::new(shutdown_tx); - // Build the router with or without Sentry layers based on environment + // Base router configuration let app = Router::new() .merge(protected_router) .merge(public_router) @@ -87,13 +75,9 @@ async fn main() { .layer(CompressionLayer::new()) .layer(Extension(shutdown_tx.clone())); - // Add Sentry layers if not in development + // Add Sentry layers if not in development using our middleware helper let app = if !is_development { - app.layer( - ServiceBuilder::new() - .layer(sentry_tower::NewSentryLayer::::new_from_top()) - .layer(sentry_tower::SentryHttpLayer::with_transaction()) - ) + app.layer(sentry_layer()) } else { app };