feat(project): init
This commit is contained in:
126
src/error.rs
Normal file
126
src/error.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
/// 业务状态码 (Business Code)
|
||||
/// 前端根据此 Code 进行逻辑跳转
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub enum BizCode {
|
||||
Success = 0,
|
||||
|
||||
// 10xxx: 服务端通用错误
|
||||
ServerError = 10000,
|
||||
BadRequest = 10001,
|
||||
|
||||
// 20xxx: 认证授权相关 (双 Token 核心逻辑)
|
||||
Unauthorized = 20000, // 通用未授权/签名错误
|
||||
AccessTokenExpired = 20001, // 前端捕获 -> 用 RefreshToken 换新 AccessToken (静默)
|
||||
RefreshTokenExpired = 20002, // 前端捕获 -> 强制退出到登录页
|
||||
PermissionDenied = 20003,
|
||||
}
|
||||
|
||||
/// 全局应用错误枚举
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
// --- 基础设施错误 ---
|
||||
#[error("Database error: {0}")]
|
||||
DbError(String), // 实际建议: DbError(#[from] sqlx::Error)
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
// --- 业务逻辑错误 ---
|
||||
#[error("Resource not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Invalid parameters: {0}")]
|
||||
InvalidParam(String),
|
||||
|
||||
// --- 认证相关 ---
|
||||
#[error("Authentication failed")]
|
||||
AuthError,
|
||||
|
||||
#[error("Access token expired")]
|
||||
AccessTokenExpired,
|
||||
|
||||
#[error("Refresh token expired")]
|
||||
RefreshTokenExpired,
|
||||
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
}
|
||||
|
||||
/// 响应给前端的 JSON 结构
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub code: u32,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub trace_id: Option<String>, // 可选:返回 trace_id 方便排查
|
||||
}
|
||||
|
||||
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::PermissionDenied(_) => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
|
||||
// 映射业务状态码 (给前端代码看)
|
||||
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, // 关键
|
||||
AppError::PermissionDenied(_) => BizCode::PermissionDenied,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 核心:实现 Axum 的 IntoResponse
|
||||
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();
|
||||
|
||||
// 尝试获取当前的 Trace ID (如果有 telemetry 功能)
|
||||
// 这里只是简单的占位,实际需要配合 opentelemetry 获取 span id
|
||||
let trace_id = None;
|
||||
|
||||
// 1. 自动记录错误日志 (利用 tracing)
|
||||
// 这样业务代码里只需要 return Err(...),不需要手动 error!(...)
|
||||
#[cfg(feature = "telemetry")]
|
||||
{
|
||||
tracing::error!(
|
||||
%status,
|
||||
code = ?biz_code,
|
||||
error = %message,
|
||||
"Request failed"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 构建 JSON
|
||||
let body = Json(ErrorResponse {
|
||||
code: biz_code as u32,
|
||||
message,
|
||||
trace_id,
|
||||
});
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// 只有开启了 'error' feature 才会编译 error 模块
|
||||
#[cfg(feature = "error")]
|
||||
pub mod error;
|
||||
|
||||
// 只有开启了 'telemetry' feature 才会编译 telemetry 模块
|
||||
#[cfg(feature = "telemetry")]
|
||||
pub mod telemetry;
|
||||
|
||||
// 方便外部直接 use common_lib::AppError;
|
||||
#[cfg(feature = "error")]
|
||||
pub use error::{AppError, BizCode};
|
||||
81
src/telemetry.rs
Normal file
81
src/telemetry.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::{EnvFilter, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
/// 遥测配置
|
||||
pub struct TelemetryConfig {
|
||||
pub service_name: String, // 服务名 (如 "user-service")
|
||||
pub log_level: String, // 日志级别 (如 "info" 或 "debug,sqlx=error")
|
||||
pub log_to_file: bool, // 是否输出到文件
|
||||
pub log_dir: Option<String>, // 日志目录
|
||||
pub log_file: Option<String>, // 日志文件名
|
||||
}
|
||||
|
||||
impl Default for TelemetryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
service_name: "unknown-service".into(),
|
||||
log_level: "info".into(),
|
||||
log_to_file: false,
|
||||
log_dir: None,
|
||||
log_file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化 Tracing 系统
|
||||
///
|
||||
/// # 返回值
|
||||
/// 返回 `Option<WorkerGuard>`。
|
||||
/// **重要**:这个 Guard 必须一直存活到 main 函数结束。如果被丢弃,文件日志将无法写入。
|
||||
pub fn init(config: TelemetryConfig) -> Option<WorkerGuard> {
|
||||
// 1. 设置过滤器 (RUST_LOG 环境变量优先,否则用 config)
|
||||
let env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
|
||||
|
||||
// 2. 基础的 Console 输出层
|
||||
let stdout_layer = fmt::layer()
|
||||
.json() // 生产环境通常用 json
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(std::io::stdout);
|
||||
|
||||
// 3. 准备注册表
|
||||
let subscriber = Registry::default().with(env_filter);
|
||||
|
||||
// 4. 判断是否需要文件输出
|
||||
if config.log_to_file {
|
||||
let dir = config.log_dir.unwrap_or_else(|| "logs".into());
|
||||
let file = config.log_file.unwrap_or_else(|| "app.log".into());
|
||||
|
||||
// 设置滚动日志 (每天轮转)
|
||||
let file_appender = tracing_appender::rolling::daily(&dir, &file);
|
||||
|
||||
// 设置非阻塞写入 (关键性能点)
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
|
||||
let file_layer = fmt::layer()
|
||||
.json()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false); // 文件不需要颜色
|
||||
|
||||
// 组合:Console + File
|
||||
subscriber.with(stdout_layer).with(file_layer).init();
|
||||
|
||||
tracing::info!(
|
||||
"Telemetry initialized (Console + File) for {}",
|
||||
config.service_name
|
||||
);
|
||||
Some(guard)
|
||||
} else {
|
||||
// 组合:Console Only
|
||||
subscriber.with(stdout_layer).init();
|
||||
|
||||
tracing::info!(
|
||||
"Telemetry initialized (Console Only) for {}",
|
||||
config.service_name
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user