Files
common-telemetry/README.md
2026-01-29 18:09:58 +08:00

301 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Microservice Common Lib (Rust)
这是微服务架构的通用基础库 (`common-telemetry`),用于统一服务的响应结构(成功/错误)、错误处理标准、日志格式以及分布式链路追踪。
## ✨ 核心特性
* **统一 API 响应 (Response)**:
* **成功响应**`AppResponse<T>` 自动映射 HTTP 状态码200/201/202并返回统一 JSON 结构 `{ code, message, data?, trace_id? }`
* **错误响应**`AppError` 实现 `IntoResponse`,自动映射 HTTP 状态码并返回统一 JSON 结构 `{ code, message, details?, trace_id? }`
* **业务码 (BizCode)**:前端可根据 `code` 做稳定分支;成功恒为 `0`
* **双 Token 场景**:明确区分 `AccessTokenExpired` (20001) 与 `RefreshTokenExpired` (20002)。
* **第三方库适配**:可选集成 `sqlx` / `redis` / `validator` 的错误转换。
* 例如:`sqlx::Error::RowNotFound` 自动转换为 **404 Not Found**
* **可观测性 (Telemetry)**:
* 基于 `tracing` 生态。
* 支持 **JSON 结构化日志** (适配 ELK/Loki)。
* 支持 **控制台 + 文件双写**
* 支持 **非阻塞 (Non-blocking)** 异步日志写入与按天滚动。
* **模块化设计**: 通过 Feature Flags 按需引入,保持依赖轻量。
---
## 🚀 发布指南
你可以选择将此库发布到私有 Cargo 仓库 (Kellnr),或者直接作为 Git 依赖发布到 Gitea。
### 方法一:发布到 Kellnr (推荐)
Kellnr 是一个私有的 Crates.io 镜像与仓库。
#### 1. 配置本地 Cargo
在本项目根目录(或全局 `~/.cargo/config.toml`)创建/修改 `.cargo/config.toml`,注册你的私有仓库:
```toml
# .cargo/config.toml
[registries.kellnr]
# 注册表名称自定义,这里叫 "kellnr"
index = "sparse+https://kellnr.shay7sev.site/api/v1/crates/"
[net]
git-fetch-with-cli = true
```
#### 2. 登录认证
使用你的 Kellnr 账户 Token 进行登录(只需执行一次):
```bash
cargo login --registry kellnr <YOUR_AUTH_TOKEN>
```
#### 3. 修改 Cargo.toml
确保 `Cargo.toml` 中配置了禁止发布到公网,并指定了私有仓库:
```toml
[package]
name = "common-telemetry"
version = "0.1.0"
# ...
publish = ["kellnr"] # 关键:防止误发到 crates.io
```
#### 4. 执行发布
```bash
cargo publish --registry kellnr
```
---
### 方法二:推送到 Gitea (Git 依赖)
如果你不想走 Cargo Registry 流程,可以直接作为 Git 仓库使用。
```bash
# 初始化 git (如果尚未初始化)
git init
git branch -M main
# 添加远程仓库 (替换为你的实际仓库地址)
git remote add origin ssh://git@gitea.shay7sev.site:2222/admin/common-telemetry.git
# 推送代码
git add .
git commit -m "Initial commit"
git push -u origin main
```
---
## 📦 如何在其他服务中使用
假设你要在 `user-service` 中使用此库。
### 1. 引入依赖
#### 方式 A: 通过 Kellnr 引入 (如果已发布)
`user-service``Cargo.toml` 中,你可以按需选择“全功能”或“最小依赖”。
```toml
[dependencies]
# 方案 1默认全功能等价于 features = ["full"]
common-telemetry = { version = "0.1", registry = "kellnr" }
# 方案 2只启用响应模型 + SQLX/Validator更轻量
# common-telemetry = { version = "0.1", registry = "kellnr", default-features = false, features = ["response", "with-sqlx", "with-validator"] }
# 注意:你需要确保服务本身引用的 sqlx 版本与 common-telemetry 兼容
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio"] }
```
*注意:使用此方式,`user-service` 项目也需要在 `.cargo/config.toml` 中配置 registry 地址。*
#### 方式 B: 通过 Gitea (Git) 引入
```toml
[dependencies]
common-telemetry = { git = "ssh://git@gitea.shay7sev.site:2222/admin/common-telemetry.git", branch = "main", features = ["full"] }
```
### 2. 代码集成示例
#### A. 初始化日志 (main.rs)
```rust
use common_telemetry::telemetry::{self, TelemetryConfig};
#[tokio::main]
async fn main() {
// 1. 配置
let config = TelemetryConfig {
service_name: "user-service".into(),
log_level: "info".into(), // 或 "debug,sqlx=error"
log_to_file: true, // 生产环境建议开启
log_dir: Some("./logs".into()),
log_file: Some("user.log".into()),
};
// 2. 初始化 Tracing
// !!! 警告:必须将 guard 赋值给一个变量 (_guard),并保持它在 main 函数整个生命周期内存活
// 否则日志文件写入线程会被立即销毁!
let _guard = telemetry::init(config);
tracing::info!("User Service started success!");
// ... 启动 Axum ...
}
```
**重要:环境变量优先级**
`telemetry::init` 会优先读取 `RUST_LOG` 环境变量;若未设置则使用 `TelemetryConfig.log_level`
#### B. 错误处理与第三方库集成 (Handler)
由于实现了 `From<T>`,你可以直接使用 `?` 操作符,库会自动处理类型转换和 HTTP 映射。
```rust
use axum::{Json, response::IntoResponse};
use common_telemetry::AppError; // 引入统一错误
use validator::Validate;
#[derive(serde::Deserialize, Validate)]
struct CreateUserReq {
#[validate(email)]
email: String,
}
async fn create_user(
Json(payload): Json<CreateUserReq>,
) -> Result<Json<String>, AppError> {
// 1. 校验错误 (自动转 AppError::ValidationError -> HTTP 400)
payload.validate()?;
// 2. 数据库查询 (自动转 AppError::DbError -> HTTP 500)
// 特殊情况:如果是 RowNotFound会自动转为 AppError::NotFound -> HTTP 404
let _user = sqlx::query!("SELECT * FROM users WHERE email = $1", payload.email)
.fetch_optional(&pool)
.await?;
// 3. 业务逻辑错误 (手动返回)
if _user.is_some() {
return Err(AppError::AlreadyExists(format!("User {} already exists", payload.email)));
}
Ok(Json("Created".into()))
}
```
#### C. 统一成功响应 (可选)
如果你希望“成功”和“错误”都返回统一的 JSON 结构(成功包含 `code/message/data/trace_id`;错误包含 `code/message/details/trace_id`),建议让 handler 返回 `AppResponse<T>`
```rust
use common_telemetry::{AppError, AppResponse};
async fn get_profile() -> Result<AppResponse<String>, AppError> {
Ok(AppResponse::ok("profile data".to_string()))
}
```
#### D. 响应体格式(前后端对齐)
成功响应(`AppResponse<T>`
```json
{ "code": 0, "message": "Success", "data": { "any": "payload" }, "trace_id": null }
```
错误响应(`AppError`
```json
{ "code": 30001, "message": "Validation error: ...", "details": "field required", "trace_id": null }
```
---
## ⚙️ 功能模块说明 (Feature Flags)
本库采用高度模块化设计,建议按需开启 Feature 以减少编译体积:
| Feature | 说明 | 包含依赖 |
| :--- | :--- | :--- |
| **`default`** | 默认开启全功能 | `full` |
| **`full`** | 包含基础功能及所有第三方集成 | `response`, `telemetry`, `with-sqlx`, `with-redis`, `with-anyhow`, `with-validator` |
| **`response`** | 统一成功/错误响应 + 错误处理 | `thiserror`, `axum`, `serde` |
| **`telemetry`** | 仅使用日志与链路追踪 | `tracing` 全家桶 |
| **`with-sqlx`** | 集成 `sqlx` 错误转换 | `sqlx` (自动处理 RowNotFound) |
| **`with-redis`** | 集成 `redis` 错误转换 | `redis` |
| **`with-validator`** | 集成 `validator` 错误转换 | `validator` |
| **`with-anyhow`** | 集成 `anyhow` 兜底错误 | `anyhow` |
兼容性说明:历史上的 `error` feature 已被重命名为 `response`,但仍保留 `error` 作为别名(`error = ["response"]`),以降低已有服务的迁移成本。
**示例 (只用响应 + SQLX支持):**
```toml
common-telemetry = { version = "0.1", default-features = false, features = ["response", "with-sqlx"] }
```
---
## 📝 错误码对照表
前端开发请参考以下业务状态码 (Code)
| Code | 枚举 | HTTP | 含义 | 前端建议动作 |
| :--- | :--- | :--- | :--- | :--- |
| `0` | `Success` | 200 | 成功 | - |
| **10xxx: 基础设施** | | | | |
| `10000` | `ServerError` | 500 | 服务器内部错误 | 提示“系统繁忙” |
| `10001` | `DbError` | 500 | 数据库错误 | 提示“系统繁忙” |
| `10002` | `CacheError` | 500 | 缓存服务错误 | 提示“系统繁忙” |
| `10003` | `SerializationError` | 500 | 序列化/反序列化失败 | 提示“系统繁忙” |
| `10004` | `ExternalServiceError` | 500 | 下游/第三方调用失败 | 提示“系统繁忙” |
| `10005` | `ConfigError` | 500 | 配置加载失败 | 提示“系统繁忙” |
| **20xxx: 认证授权** | | | | |
| `20000` | `Unauthorized` | 401 | 未授权/签名无效 | 跳转登录 |
| **`20001`** | **`AccessTokenExpired`** | **401** | **Access Token 过期** | **使用 Refresh Token 静默刷新** |
| **`20002`** | **`RefreshTokenExpired`** | **401** | **Refresh Token 过期** | **强制登出,跳转登录页** |
| `20003` | `PermissionDenied` | 403 | 权限不足 | 提示无权访问 |
| `20004` | `AccountDisabled` | 401 | 账号禁用/锁定 | 跳转登录或提示 |
| `20005` | `InvalidCredentials` | 401 | 账号或密码错误 | 提示重试 |
| `20006` | `MissingHeader` | 401 | 缺少必要 Header | 提示重试 |
| **30xxx: 客户端错误** | | | | |
| `30000` | `BadRequest` | 400 | 请求参数通用错误 | 提示错误信息 |
| `30001` | `ValidationError` | 400 | 表单校验失败 | 提示具体字段错误 |
| `30002` | `ResourceNotFound` | 404 | 资源不存在 | 提示“未找到数据” |
| `30003` | `ResourceAlreadyExists`| 409 | 资源已存在 | 提示“重复创建” |
| `30004` | `MethodNotAllowed` | 405 | HTTP 方法不支持 | 提示“请求方式错误” |
| **40xxx: 业务流控/状态** | | | | |
| `40000` | `RateLimitExceeded` | 429 | 请求过于频繁 | 退避重试 |
| `40001` | `PreconditionFailed` | 400 | 业务前置条件不满足 | 提示原因 |
---
## 🛠 开发注意事项
1. **Cargo.lock**: 本项目已将 `Cargo.lock` 加入 `.gitignore`,这是作为 Library 的最佳实践。
2. **测试**: 运行集成测试以验证全链路功能。
```bash
# 使用 --nocapture 查看详细步骤打印
cargo test -- --nocapture
```
---
## 🧩 前端 TypeScript 类型 (ApiResponse)
本仓库提供前后端对齐的 TypeScript 类型定义文件(非 NPM 包形式发布):
- 入口: [types/index.ts](file:///home/shay/project/backend/common-telemetry/types/index.ts)
- 定义: [types/api-response.ts](file:///home/shay/project/backend/common-telemetry/types/api-response.ts)
- 便于引用的单文件 re-export [types.ts](file:///home/shay/project/backend/common-telemetry/types.ts)
前端可直接在代码库中引用(例如作为 git 子模块/直接拷贝),并使用:
```ts
import { ApiResponse, isSuccessResponse } from "./path/to/common-telemetry/types";
function handle<T>(res: ApiResponse<T>) {
if (isSuccessResponse(res)) return res.data;
throw new Error(res.message);
}
```