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]
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 = [

View File

@@ -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<Json<UserProfile>, AppError> {
// 模拟Token 过期

View File

@@ -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<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 {
// 映射 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()