diff --git a/Cargo.toml b/Cargo.toml index 7efcae6..e7b2240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common-telemetry" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Microservice infrastructure library" @@ -9,7 +9,14 @@ publish = ["kellnr"] [features] # 默认开启所有功能 default = ["full"] -full = ["error", "telemetry"] +full = [ + "error", + "telemetry", + "with-sqlx", + "with-redis", + "with-anyhow", + "with-validator", +] # --- Error 模块依赖 --- # 开启 error feature 将引入 thiserror, axum, serde @@ -19,6 +26,13 @@ error = ["dep:thiserror", "dep:axum", "dep:serde", "dep:serde_json"] # 开启 telemetry feature 将引入 tracing 全家桶 telemetry = ["dep:tracing", "dep:tracing-subscriber", "dep:tracing-appender"] +# === 第三方库集成特性 (Feature Flags) === +# 这里的 dep:xxx 对应下方 dependencies 里的 optional = true +with-sqlx = ["dep:sqlx"] +with-redis = ["dep:redis"] +with-anyhow = ["dep:anyhow"] +with-validator = ["dep:validator"] + [dependencies] # Error 相关 thiserror = { version = "2.0.18", optional = true } @@ -26,6 +40,11 @@ axum = { version = "0.8.8", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } +sqlx = { version = "0.8.6", optional = true } +redis = { version = "1.0.2", optional = true } +anyhow = { version = "1.0", optional = true } +validator = { version = "0.20.0", optional = true } + # Telemetry 相关 tracing = { version = "0.1", optional = true } tracing-subscriber = { version = "0.3", features = [ diff --git a/README.md b/README.md index e62fc7a..901bb1f 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ common-telemetry = { git = "ssh://git@gitea.shay7sev.site:2222/admin/common-tele #### A. 初始化日志 (main.rs) ```rust -use common_lib::telemetry::{self, TelemetryConfig}; +use common_telemetry::telemetry::{self, TelemetryConfig}; #[tokio::main] async fn main() { @@ -141,7 +141,7 @@ async fn main() { ```rust use axum::{Json, response::IntoResponse}; -use common_lib::AppError; // 引入统一错误 +use common_telemetry::AppError; // 引入统一错误 async fn get_profile(token: String) -> Result, AppError> { // 模拟:Token 过期 diff --git a/src/error.rs b/src/error.rs index 0fec408..e89bf4f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,45 +14,111 @@ pub enum BizCode { Success = 0, // 10xxx: 服务端通用错误 - ServerError = 10000, - BadRequest = 10001, + ServerError = 10000, // 未知/兜底错误 + DbError = 10001, // 数据库操作失败 + CacheError = 10002, // Redis/缓存失败 + SerializationError = 10003, // JSON 解析/序列化失败 + ExternalServiceError = 10004, // 调用第三方/下游服务失败 + ConfigError = 10005, // 配置加载失败 // 20xxx: 认证授权相关 (双 Token 核心逻辑) - Unauthorized = 20000, // 通用未授权/签名错误 - AccessTokenExpired = 20001, // 前端捕获 -> 用 RefreshToken 换新 AccessToken (静默) - RefreshTokenExpired = 20002, // 前端捕获 -> 强制退出到登录页 - PermissionDenied = 20003, + Unauthorized = 20000, // 未认证 (通用) + AccessTokenExpired = 20001, // [前端触发] 静默刷新 + RefreshTokenExpired = 20002, // [前端触发] 强制登出 + PermissionDenied = 20003, // 无权限 (403) + AccountDisabled = 20004, // 账号被禁用/锁定 + InvalidCredentials = 20005, // 账号或密码错误 + MissingHeader = 20006, // 缺少必要的 Header (如 x-trace-id, Authorization) + + // === 30xxx: 客户端输入错误 === + BadRequest = 30000, // 通用请求参数错误 + ValidationError = 30001, // 字段校验不通过 (Validation) + ResourceNotFound = 30002, // 资源不存在 (404) + ResourceAlreadyExists = 30003, // 资源冲突/重复 (409) + MethodNotAllowed = 30004, // HTTP 方法不支持 + + // === 40xxx: 业务流控/状态 === + RateLimitExceeded = 40000, // 请求过于频繁 (429) + PreconditionFailed = 40001, // 业务前置条件不满足 (如:余额不足无法支付) } /// 全局应用错误枚举 #[derive(Error, Debug)] pub enum AppError { // --- 基础设施错误 --- + #[cfg(feature = "with-sqlx")] #[error("Database error: {0}")] - DbError(String), // 实际建议: DbError(#[from] sqlx::Error) + DbError(sqlx::Error), + + #[cfg(feature = "with-redis")] + #[error("Cache error: {0}")] + CacheError(#[from] redis::RedisError), + + // 外部服务调用错误 (如调用其他微服务失败) + #[error("External service error: {0}")] + ExternalReqError(String), // 对应 reqwest::Error + + // 消息队列错误 + #[error("MQ error: {0}")] + MqError(String), #[error("IO error: {0}")] IoError(#[from] std::io::Error), - // --- 业务逻辑错误 --- - #[error("Resource not found: {0}")] - NotFound(String), + // 序列化/反序列化错误 + #[error("Serialization error: {0}")] + SerdeError(#[from] serde_json::Error), - #[error("Invalid parameters: {0}")] - InvalidParam(String), + // 任意未预期错误 (兜底) + #[cfg(feature = "with-anyhow")] + #[error("Unexpected error: {0}")] + AnyhowError(#[from] anyhow::Error), - // --- 认证相关 --- - #[error("Authentication failed")] - AuthError, + // ====================================================== + // 2. 认证与授权层 (Auth) - 双 Token 核心 + // ====================================================== + #[error("Authentication failed: {0}")] + AuthError(String), // 通用认证失败 (签名错误、格式错误) - #[error("Access token expired")] - AccessTokenExpired, + #[error("Missing authorization header")] + MissingAuthHeader, - #[error("Refresh token expired")] - RefreshTokenExpired, + #[error("Access token has expired")] + AccessTokenExpired, // 触发 refresh + + #[error("Refresh token has expired")] + RefreshTokenExpired, // 触发 logout #[error("Permission denied: {0}")] - PermissionDenied(String), + PermissionDenied(String), // 403 + + #[error("Account is disabled or locked")] + AccountLocked, + + // ====================================================== + // 3. 客户端输入层 (Client Side) + // ====================================================== + #[error("Resource not found: {0}")] + NotFound(String), // 404 + + #[error("Resource already exists: {0}")] + AlreadyExists(String), // 409 Conflict + + #[error("Invalid request parameters: {0}")] + BadRequest(String), // 400 + + #[cfg(feature = "with-validator")] + #[error("Validation error: {0}")] + ValidationError(String), + + // ====================================================== + // 4. 业务逻辑层 (Business Logic) + // ====================================================== + #[error("Rate limit exceeded, please try again later")] + RateLimitExceeded, // 429 + + #[error("Business precondition failed: {0}")] + BusinessLogicError(String), // 业务状态冲突 (例如:订单已支付不能取消) } /// 响应给前端的 JSON 结构 @@ -64,29 +130,93 @@ pub struct ErrorResponse { pub trace_id: Option, // 可选:返回 trace_id 方便排查 } +// 1. 实现 sqlx::Error -> AppError +#[cfg(feature = "with-sqlx")] +impl From for AppError { + fn from(e: sqlx::Error) -> Self { + match e { + // 将 "查不到数据" 转换为 "NotFound" 业务错误 + sqlx::Error::RowNotFound => AppError::NotFound("Database row not found".into()), + // 其他错误保持为 DbError + other => AppError::DbError(other), + } + } +} + +// 2. 实现 validator::ValidationErrors -> AppError +#[cfg(feature = "with-validator")] +impl From for AppError { + fn from(e: validator::ValidationErrors) -> Self { + // 将复杂的校验错误结构体转为字符串供前端显示 + // 你也可以选择在这里只取第一个错误,或者转为 JSON 字符串 + AppError::ValidationError(e.to_string()) + } +} + impl AppError { // 映射 HTTP 状态码 (给网关/浏览器看) fn http_status(&self) -> StatusCode { match self { - AppError::DbError(_) | AppError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR, - AppError::NotFound(_) => StatusCode::NOT_FOUND, - AppError::InvalidParam(_) => StatusCode::BAD_REQUEST, - AppError::AuthError | AppError::AccessTokenExpired | AppError::RefreshTokenExpired => { - StatusCode::UNAUTHORIZED - } + AppError::DbError(_) + | AppError::CacheError(_) + | AppError::ExternalReqError(_) + | AppError::MqError(_) + | AppError::IoError(_) + | AppError::SerdeError(_) + | AppError::AnyhowError(_) => StatusCode::INTERNAL_SERVER_ERROR, + // 401 Unauthorized + AppError::AuthError(_) + | AppError::MissingAuthHeader + | AppError::AccessTokenExpired + | AppError::RefreshTokenExpired + | AppError::AccountLocked => StatusCode::UNAUTHORIZED, + + // 403 Forbidden AppError::PermissionDenied(_) => StatusCode::FORBIDDEN, + + // 404 Not Found + AppError::NotFound(_) => StatusCode::NOT_FOUND, + + // 409 Conflict + AppError::AlreadyExists(_) => StatusCode::CONFLICT, + + // 429 Too Many Requests + AppError::RateLimitExceeded => StatusCode::TOO_MANY_REQUESTS, + + // 400 Bad Request (默认) + _ => StatusCode::BAD_REQUEST, } } // 映射业务状态码 (给前端代码看) fn biz_code(&self) -> BizCode { match self { - AppError::DbError(_) | AppError::IoError(_) => BizCode::ServerError, - AppError::NotFound(_) | AppError::InvalidParam(_) => BizCode::BadRequest, - AppError::AuthError => BizCode::Unauthorized, - AppError::AccessTokenExpired => BizCode::AccessTokenExpired, // 关键 - AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired, // 关键 + // Infra + AppError::DbError(_) => BizCode::DbError, + AppError::CacheError(_) => BizCode::CacheError, + AppError::ExternalReqError(_) => BizCode::ExternalServiceError, + AppError::MqError(_) | AppError::IoError(_) | AppError::AnyhowError(_) => { + BizCode::ServerError + } + AppError::SerdeError(_) => BizCode::SerializationError, + + // Auth + AppError::AuthError(_) => BizCode::Unauthorized, + AppError::MissingAuthHeader => BizCode::MissingHeader, + AppError::AccessTokenExpired => BizCode::AccessTokenExpired, + AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired, AppError::PermissionDenied(_) => BizCode::PermissionDenied, + AppError::AccountLocked => BizCode::AccountDisabled, + + // Client + AppError::NotFound(_) => BizCode::ResourceNotFound, + AppError::AlreadyExists(_) => BizCode::ResourceAlreadyExists, + AppError::BadRequest(_) => BizCode::BadRequest, + AppError::ValidationError(_) => BizCode::ValidationError, + + // Biz + AppError::RateLimitExceeded => BizCode::RateLimitExceeded, + AppError::BusinessLogicError(_) => BizCode::PreconditionFailed, } } } @@ -96,20 +226,32 @@ impl IntoResponse for AppError { fn into_response(self) -> Response { let status = self.http_status(); let biz_code = self.biz_code(); - let message = self.to_string(); + // 生产环境通常不把详细的 DB 报错返回给前端,防止泄露表结构 + // 但这里为了演示,我们先直接使用 self.to_string() + // 建议:在生产环境针对 DbError/AnyhowError 返回统一的 "Internal Server Error" + let message = match self { + AppError::DbError(_) | AppError::AnyhowError(_) => { + // 如果是生产环境(release模式),隐藏敏感信息 + #[cfg(not(debug_assertions))] + { + "Internal server error".to_string() + } + #[cfg(debug_assertions)] + { + self.to_string() + } + } + _ => self.to_string(), + }; - // 尝试获取当前的 Trace ID (如果有 telemetry 功能) - // 这里只是简单的占位,实际需要配合 opentelemetry 获取 span id - let trace_id = None; - - // 1. 自动记录错误日志 (利用 tracing) - // 这样业务代码里只需要 return Err(...),不需要手动 error!(...) + // 1. 自动打印 Error 日志 (带上 Trace) + // 只有开启 telemetry feature 时才打印 #[cfg(feature = "telemetry")] { tracing::error!( %status, - code = ?biz_code, - error = %message, + code = ?biz_code, // 打印枚举名 + error_msg = %self, // 打印详细错误信息 "Request failed" ); } @@ -118,7 +260,7 @@ impl IntoResponse for AppError { let body = Json(ErrorResponse { code: biz_code as u32, message, - trace_id, + trace_id: None, // 实际项目中从 tracing context 获取 }); (status, body).into_response()