日志与可观测性
专栏:new-api 源码拆解 · 第 9 / 12 篇:::info 学习目标 完成本篇后你能够:说出 logs 表的关键字段、索引设计与八类日志;用一个 request_id 串起一次请求的完整链路;列举 new-api 的性能观测三件套及其用途。 前置:第 8 篇完成。预计时长:45 分钟。 :::
:::note 本章术语速查(新手建议先读)
- 可观测性(Observability):出问题时能完整还原”发生了什么”的能力。
- 复合索引:数据库索引的一种,多个列联合排序,让常用查询走索引提速。
- request_id:每个请求的唯一编号,贯穿网关全部日志——排障用它一查到底。
- pprof:Go 自带的性能分析工具,能看到程序时间花在哪个函数上。
- TTL(Time To Live):数据自动过期的存活时间,日志表常用它自动清理。 :::
网关的日志承担三种角色:对账凭证(每笔消费的计费依据)、排障现场(失败请求的第一现场)、数据资产(看板与评测集的来源)。new-api 的 logs 表设计同时服务这三者。
logs 表:为查询而设计的日志
// model/log.go:57-91(节选)
type Log struct {
Id int `gorm:"index:idx_created_at_id,priority:2;..."`
UserId int `gorm:"index;index:idx_user_id_id,priority:1"`
CreatedAt int64 `gorm:"bigint;index:idx_created_at_id,priority:1;..."`
Type int // 日志类型,见下表
Content string
Username string `gorm:"index;index:index_username_model_name,priority:2"`
TokenName string `gorm:"index"`
ModelName string `gorm:"index;index:index_username_model_name,priority:1"`
Quota int // 本条消费的 quota
PromptTokens int
CompletionTokens int
UseTime int // 耗时(秒)
IsStream bool
ChannelId int `gorm:"index"`
Group string `gorm:"index"`
Ip string `gorm:"index"`
RequestId string `gorm:"type:varchar(64);index:idx_logs_request_id"`
UpstreamRequestId string `gorm:"type:varchar(128);index"`
Other string // 扩展字段(缓存命中数、倍率明细等)
}
const (
LogTypeUnknown = 0; LogTypeTopup = 1; LogTypeConsume = 2; LogTypeManage = 3
LogTypeSystem = 4; LogTypeError = 5; LogTypeRefund = 6; LogTypeLogin = 7
)
三个设计决策值得学习:
- 复合索引按查询模式建:按时间翻页(
idx_created_at_id,时间+id 保证稳定排序)、按用户+模型筛选(idx_user_id_id、index_username_model_name)——每种管理台查询都有对应索引; - request_id 与 upstream_request_id 双链路 ID:前者贯穿网关内部(第 2 篇的
RequestId中间件生成),后者是上游返回的请求 ID——排障时分别定位网关侧与上游侧; - 类型常量不用 iota,注释直说”avoid change log type value”——防重排导致历史数据语义漂移。
日志库可独立(LOG_SQL_DSN 指到单独库,支持 ClickHouse + TTL)——量上来之后日志不拖垮业务库。
一次排障的完整链路
错误日志单独一类(LogTypeError),由第 7 篇的 processChannelError 异步写入,记录渠道 id、状态码与 use_channel 重试轨迹——重试行为全部留痕。
与看板的联动:一份日志,两个消费方
RecordConsumeLog 写日志的同时把聚合数据投给 LogQuotaData(model/usedata.go)——内存缓存累加,UpdateQuotaData 协程定时落库,优雅关机时 SaveQuotaDataCache 兜底。看板(消费趋势、模型分布、渠道占比)由此而来,与明细日志同源,永不打架。
性能观测三件套
| 工具 | 开启方式 | 用途 |
|---|---|---|
| perf_metrics(pkg/perf_metrics) | 默认(可采样配置) | 请求采样 → 分桶计数 → Redis/DB 聚合,看板展示吞吐分布(RecordRelaySample 在结算后异步调用) |
| pprof | ENABLE_PPROF=true,端口 8005 | Go 标准剖析:CPU/内存/goroutine 现场 |
| pyroscope | PYROSCOPE_URL | 持续剖析,长期观测性能回归 |
配合 common/system_monitor.go 每 5 秒采样 CPU/内存/磁盘——观测数据供第 10 篇的 SystemPerformanceCheck 过载保护直接消费,数据不止给人看,还驱动保护逻辑。
常见踩坑
- 日志表撑爆业务库——量上来之后第一件事:独立
LOG_SQL_DSN(支持 ClickHouse + TTL); - IP 全量记录惹合规麻烦——
RecordIpLog按用户设置开关,默认克制; - 只看消费日志排障——错误日志与
use_channel重试轨迹才是失败问题的第一现场; - 日志与看板数字对不上——先检查
DataExportEnabled是否关闭(看板数据不落库)。
随堂练习(带验收标准)
- 发起 3 次请求(1 成功 2 失败),用 request_id 在日志里还原每次请求的渠道、倍率与结果。验收:三条日志全部找到且字段完整;
- 给日志表写一条按「用户 + 模型 + 时间段」聚合消费的 SQL。验收:EXPLAIN 确认走
idx_user_id_id与idx_created_at_type索引; - 开启 pprof(
ENABLE_PPROF=true),压测网关,抓一份 CPU profile 找热点函数; - 进阶:对比
Other字段里缓存命中数的记录方式,思考如何用这些数据验证第 6 篇(dsh 专栏)讲的缓存经济学。