日志与可观测性

专栏:new-api 源码拆解 · 第 9 / 12 篇
new-api日志可观测性

:::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
)

三个设计决策值得学习:

  1. 复合索引按查询模式建:按时间翻页(idx_created_at_id,时间+id 保证稳定排序)、按用户+模型筛选(idx_user_id_idindex_username_model_name)——每种管理台查询都有对应索引;
  2. request_id 与 upstream_request_id 双链路 ID:前者贯穿网关内部(第 2 篇的 RequestId 中间件生成),后者是上游返回的请求 ID——排障时分别定位网关侧与上游侧;
  3. 类型常量不用 iota,注释直说”avoid change log type value”——防重排导致历史数据语义漂移。

日志库可独立(LOG_SQL_DSN 指到单独库,支持 ClickHouse + TTL)——量上来之后日志不拖垮业务库。

一次排障的完整链路

图表(newapi-logging-observability.md)

错误日志单独一类(LogTypeError),由第 7 篇的 processChannelError 异步写入,记录渠道 id、状态码与 use_channel 重试轨迹——重试行为全部留痕。

与看板的联动:一份日志,两个消费方

RecordConsumeLog 写日志的同时把聚合数据投给 LogQuotaDatamodel/usedata.go)——内存缓存累加,UpdateQuotaData 协程定时落库,优雅关机时 SaveQuotaDataCache 兜底。看板(消费趋势、模型分布、渠道占比)由此而来,与明细日志同源,永不打架

性能观测三件套

工具开启方式用途
perf_metrics(pkg/perf_metrics)默认(可采样配置)请求采样 → 分桶计数 → Redis/DB 聚合,看板展示吞吐分布(RecordRelaySample 在结算后异步调用)
pprofENABLE_PPROF=true,端口 8005Go 标准剖析:CPU/内存/goroutine 现场
pyroscopePYROSCOPE_URL持续剖析,长期观测性能回归

配合 common/system_monitor.go 每 5 秒采样 CPU/内存/磁盘——观测数据供第 10 篇的 SystemPerformanceCheck 过载保护直接消费,数据不止给人看,还驱动保护逻辑

常见踩坑

  1. 日志表撑爆业务库——量上来之后第一件事:独立 LOG_SQL_DSN(支持 ClickHouse + TTL);
  2. IP 全量记录惹合规麻烦——RecordIpLog 按用户设置开关,默认克制;
  3. 只看消费日志排障——错误日志与 use_channel 重试轨迹才是失败问题的第一现场;
  4. 日志与看板数字对不上——先检查 DataExportEnabled 是否关闭(看板数据不落库)。

随堂练习(带验收标准)

  1. 发起 3 次请求(1 成功 2 失败),用 request_id 在日志里还原每次请求的渠道、倍率与结果。验收:三条日志全部找到且字段完整;
  2. 给日志表写一条按「用户 + 模型 + 时间段」聚合消费的 SQL。验收:EXPLAIN 确认走 idx_user_id_ididx_created_at_type 索引;
  3. 开启 pprof(ENABLE_PPROF=true),压测网关,抓一份 CPU profile 找热点函数;
  4. 进阶:对比 Other 字段里缓存命中数的记录方式,思考如何用这些数据验证第 6 篇(dsh 专栏)讲的缓存经济学。

← 返回文章列表