fix(error): perf error

This commit is contained in:
2026-01-29 15:49:46 +08:00
parent 88868050ba
commit c5b32f721c
3 changed files with 206 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "common-telemetry" name = "common-telemetry"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
description = "Microservice infrastructure library" description = "Microservice infrastructure library"
@@ -9,7 +9,14 @@ publish = ["kellnr"]
[features] [features]
# 默认开启所有功能 # 默认开启所有功能
default = ["full"] default = ["full"]
full = ["error", "telemetry"] full = [
"error",
"telemetry",
"with-sqlx",
"with-redis",
"with-anyhow",
"with-validator",
]
# --- Error 模块依赖 --- # --- Error 模块依赖 ---
# 开启 error feature 将引入 thiserror, axum, serde # 开启 error feature 将引入 thiserror, axum, serde
@@ -19,6 +26,13 @@ error = ["dep:thiserror", "dep:axum", "dep:serde", "dep:serde_json"]
# 开启 telemetry feature 将引入 tracing 全家桶 # 开启 telemetry feature 将引入 tracing 全家桶
telemetry = ["dep:tracing", "dep:tracing-subscriber", "dep:tracing-appender"] 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] [dependencies]
# Error 相关 # Error 相关
thiserror = { version = "2.0.18", optional = true } 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 = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", 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 相关 # Telemetry 相关
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3", features = [ tracing-subscriber = { version = "0.3", features = [

View File

@@ -113,7 +113,7 @@ common-telemetry = { git = "ssh://git@gitea.shay7sev.site:2222/admin/common-tele
#### A. 初始化日志 (main.rs) #### A. 初始化日志 (main.rs)
```rust ```rust
use common_lib::telemetry::{self, TelemetryConfig}; use common_telemetry::telemetry::{self, TelemetryConfig};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -141,7 +141,7 @@ async fn main() {
```rust ```rust
use axum::{Json, response::IntoResponse}; use axum::{Json, response::IntoResponse};
use common_lib::AppError; // 引入统一错误 use common_telemetry::AppError; // 引入统一错误
async fn get_profile(token: String) -> Result<Json<UserProfile>, AppError> { async fn get_profile(token: String) -> Result<Json<UserProfile>, AppError> {
// 模拟Token 过期 // 模拟Token 过期

View File

@@ -14,45 +14,111 @@ pub enum BizCode {
Success = 0, Success = 0,
// 10xxx: 服务端通用错误 // 10xxx: 服务端通用错误
ServerError = 10000, ServerError = 10000, // 未知/兜底错误
BadRequest = 10001, DbError = 10001, // 数据库操作失败
CacheError = 10002, // Redis/缓存失败
SerializationError = 10003, // JSON 解析/序列化失败
ExternalServiceError = 10004, // 调用第三方/下游服务失败
ConfigError = 10005, // 配置加载失败
// 20xxx: 认证授权相关 (双 Token 核心逻辑) // 20xxx: 认证授权相关 (双 Token 核心逻辑)
Unauthorized = 20000, // 通用未授权/签名错误 Unauthorized = 20000, // 未认证 (通用)
AccessTokenExpired = 20001, // 前端捕获 -> 用 RefreshToken 换新 AccessToken (静默) AccessTokenExpired = 20001, // [前端触发] 静默刷新
RefreshTokenExpired = 20002, // 前端捕获 -> 强制退出到登录页 RefreshTokenExpired = 20002, // [前端触发] 强制登出
PermissionDenied = 20003, 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)] #[derive(Error, Debug)]
pub enum AppError { pub enum AppError {
// --- 基础设施错误 --- // --- 基础设施错误 ---
#[cfg(feature = "with-sqlx")]
#[error("Database error: {0}")] #[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}")] #[error("IO error: {0}")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
// --- 业务逻辑错误 --- // 序列化/反序列化错误
#[error("Resource not found: {0}")] #[error("Serialization error: {0}")]
NotFound(String), 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")] // 2. 认证与授权层 (Auth) - 双 Token 核心
AuthError, // ======================================================
#[error("Authentication failed: {0}")]
AuthError(String), // 通用认证失败 (签名错误、格式错误)
#[error("Access token expired")] #[error("Missing authorization header")]
AccessTokenExpired, MissingAuthHeader,
#[error("Refresh token expired")] #[error("Access token has expired")]
RefreshTokenExpired, AccessTokenExpired, // 触发 refresh
#[error("Refresh token has expired")]
RefreshTokenExpired, // 触发 logout
#[error("Permission denied: {0}")] #[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 结构 /// 响应给前端的 JSON 结构
@@ -64,29 +130,93 @@ pub struct ErrorResponse {
pub trace_id: Option<String>, // 可选:返回 trace_id 方便排查 pub trace_id: Option<String>, // 可选:返回 trace_id 方便排查
} }
// 1. 实现 sqlx::Error -> AppError
#[cfg(feature = "with-sqlx")]
impl From<sqlx::Error> 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<validator::ValidationErrors> for AppError {
fn from(e: validator::ValidationErrors) -> Self {
// 将复杂的校验错误结构体转为字符串供前端显示
// 你也可以选择在这里只取第一个错误,或者转为 JSON 字符串
AppError::ValidationError(e.to_string())
}
}
impl AppError { impl AppError {
// 映射 HTTP 状态码 (给网关/浏览器看) // 映射 HTTP 状态码 (给网关/浏览器看)
fn http_status(&self) -> StatusCode { fn http_status(&self) -> StatusCode {
match self { match self {
AppError::DbError(_) | AppError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR, AppError::DbError(_)
AppError::NotFound(_) => StatusCode::NOT_FOUND, | AppError::CacheError(_)
AppError::InvalidParam(_) => StatusCode::BAD_REQUEST, | AppError::ExternalReqError(_)
AppError::AuthError | AppError::AccessTokenExpired | AppError::RefreshTokenExpired => { | AppError::MqError(_)
StatusCode::UNAUTHORIZED | 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, 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 { fn biz_code(&self) -> BizCode {
match self { match self {
AppError::DbError(_) | AppError::IoError(_) => BizCode::ServerError, // Infra
AppError::NotFound(_) | AppError::InvalidParam(_) => BizCode::BadRequest, AppError::DbError(_) => BizCode::DbError,
AppError::AuthError => BizCode::Unauthorized, AppError::CacheError(_) => BizCode::CacheError,
AppError::AccessTokenExpired => BizCode::AccessTokenExpired, // 关键 AppError::ExternalReqError(_) => BizCode::ExternalServiceError,
AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired, // 关键 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::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 { fn into_response(self) -> Response {
let status = self.http_status(); let status = self.http_status();
let biz_code = self.biz_code(); 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 功能) // 1. 自动打印 Error 日志 (带上 Trace)
// 这里只是简单的占位,实际需要配合 opentelemetry 获取 span id // 只有开启 telemetry feature 时才打印
let trace_id = None;
// 1. 自动记录错误日志 (利用 tracing)
// 这样业务代码里只需要 return Err(...),不需要手动 error!(...)
#[cfg(feature = "telemetry")] #[cfg(feature = "telemetry")]
{ {
tracing::error!( tracing::error!(
%status, %status,
code = ?biz_code, code = ?biz_code, // 打印枚举名
error = %message, error_msg = %self, // 打印详细错误信息
"Request failed" "Request failed"
); );
} }
@@ -118,7 +260,7 @@ impl IntoResponse for AppError {
let body = Json(ErrorResponse { let body = Json(ErrorResponse {
code: biz_code as u32, code: biz_code as u32,
message, message,
trace_id, trace_id: None, // 实际项目中从 tracing context 获取
}); });
(status, body).into_response() (status, body).into_response()