Skip to content

Observability

元气码农少女酱 edited this page Aug 20, 2026 · 7 revisions

链路记录与统计

每个请求处理完记一条:谁处理的、用了哪个适配器、状态码、耗时、重试几次。 Web 管理端的「请求流」和统计就是看这个。

这套内置记录是唯一的可观测通路,没有 Prometheus 导出——理由见为什么不是 Prometheus

配置

[TRACE]
memory_size = 500              # 内存里保留最近多少条;0 = 关掉内存缓冲
sqlite_enabled = false         # 落盘。开了才能查历史与跨天统计
sqlite_path = "ipclick-trace.{port}.db"   # {port} = 运行时实际监听端口,见下
retention_days = 30            # 超期每小时清理一次;0 = 永久保留
only_errors = false            # 只记失败的
queue_size = 5000              # 写队列容量
record_url = true              # false = 只记 host

两层,各管一件事:

内存 SQLite
默认 (500 条)
开销 零磁盘、上限固定 一个 db 文件 + -wal / -shm
重启
能查 最近 N 条 历史、跨天统计、目标站点排行

SQLite 默认关,是因为多数人只需要"刚才发生了什么",不该默认在工作目录里造一个数据库 文件。要用 Web 端查历史就打开它。

{port} 占位符(0.4)。 同一目录用 --port 起多个实例时,这一项不岔开会让 多个进程静默写同一个库——不报错、不提示,链路记录混在一起,界面上完全看不出来, 用户以为在看单个实例的数据。{port} 会被替换成运行时实际生效的端口 (--port 覆盖之后的那个)。[LOG].output 同样支持。详见 配置体系

真撞上了还会打一条明确告警,而不是静默合并。

记什么,不记什么

:时间、请求 uuid、执行节点、适配器、方法、URL、状态码、耗时、响应体大小、 重试次数、是否为转发请求、排队耗时、错误信息、是否流式。

刻意不记:请求头、cookie、请求体、代理串。

那些含机密,而这些记录是要在 Web 端展示的。要看完整请求内容请把 [LOG].level 调成 debug——那是日志的事,日志有它自己的访问控制。

record_url = false 时只记 host,用于不想把抓取目标落盘的场景。

三条设计约束

1. 绝不反压业务

写盘走独立的写线程 + 有界队列。队列满了就丢弃并计数,业务请求一秒都不等。

可观测性数据让业务请求变慢,是把工具变成故障源。

2. 丢了必须能看见

丢弃会累加 dropped 计数并打日志:

链路记录队列已满,累计丢弃 1234 条(写盘跟不上请求速率;可关闭 [TRACE].sqlite_enabled)

静默丢弃比丢弃更糟——你会拿着一份不完整的数据下结论,还不知道它不完整。 Web 端的统计里也会显示 droppedsqlite_failed

3. 写盘挂了不影响服务

SQLite 写失败(磁盘满、权限、文件损坏)会把 sink 标记为 failed,之后只用内存, 服务照常跑。Web 端会显示数据来自 memory 而不是 sqlite

SQLite 细节

  • WAL 模式 + synchronous = NORMAL —— 读写不互相阻塞
  • 批量提交 —— 不是一条一次事务
  • auto_vacuum = INCREMENTAL —— 保留期清理后能真正把空间还给文件系统。 注意这个 PRAGMA 只在建库时生效,已有的库改不了(要 VACUUM 重建)
  • 专门的失败索引 —— WHERE status_code < 0 OR status_code >= 400 的部分索引, 查失败请求不用全表扫
  • 自动迁移 —— 旧版本建的库会补上新增列,不用手动处理

查询

Web 端的请求流页是主要入口。程序里也能直接用:

from ipclick.trace import get_recorder

r = get_recorder()
r.recent(limit=50)                              # 内存里最近的
r.query(limit=100, status_class="failed")       # 落盘的,可筛选
r.query(keyword="example.com", adapter="browser")
r.stats(days=7)                                 # 统计

status_class 可选 ok / failedfailed 的定义是 status_code < 0 或 >= 400

关键词是字面匹配——%_ 会被转义,搜 api_v2 不会匹配到 apiXv2。 URL 里这两个字符太常见(/api_v2/utm_source=),不转义的话筛出来的东西 跟你以为的不是一回事。

统计

r.stats(days=30)
# {
#   "total": 12045, "ok": 11890, "failed": 155,
#   "success_rate": 98.7, "avg_ms": 412.3, "bytes": 1502398112,
#   "by_adapter": {...},
#   "source": "sqlite", "dropped": 0, "sqlite_failed": False,
# }

统计结果有 10 秒的窗口缓存。Web 端每 2 秒轮询一次,不缓存的话每次都会对整个保留期 做一次聚合——记录一多就是每 2 秒扫一遍几十万行。

目标站点排行走 SQL 的 GROUP BYhost 是独立的列,建库时就算好),不是把记录全拉到 Python 里再统计。

host 用的是和按 host 限流同一套定义limiter.host_of)。两处对 "host" 的理解 必须一致,否则排行榜和限流说的不是一回事。record_url = falseurl 存的已经是 host 了,解析不出来就原样用——否则整个排行会全是 -

磁盘占用

一条记录大约 200~400 字节(取决于 URL 长度)。

日请求量 30 天大约
1 万 100 MB
10 万 1 GB
100 万 10 GB

量大又只关心异常的话,only_errors = true 能把库缩小一两个数量级。 或者把 retention_days 调小。

为什么不是 Prometheus

没有 [metrics] extra,也没有 Prometheus 导出端点。

Prometheus 给的是聚合指标:"过去 5 分钟错误率 3%"。而实际排查时要问的是 "刚才那个请求为什么 403"——具体哪个 URL、哪个节点、哪个适配器、重试了几次。 聚合指标答不了这个,你还得再去翻日志。

换成内置记录之后:单请求可查、Web 端直接看、零外部依赖、零额外端口。代价是没有 长期时序曲线——真需要的话,请求流数据自己导出去接 Grafana 也不难。

移除 [metrics] 同时也去掉了 prometheus-client 依赖和一个必须防护的额外监听端口。

健康检查

[MONITOR]
health_check = true

注册标准的 grpc.health.v1免鉴权,供 K8s 探针和服务网格用。集群探活也走它。

grpc_health_probe -addr=127.0.0.1:9528

关掉后探活会收到 UNIMPLEMENTED

日志

[LOG]
level = "info"      # debug 会打完整请求内容
output = "stdout"   # stdout / 文件路径 / 目录(以 / 结尾),见下

[LOG.rotation]
max_size = 100
max_backups = 5

output 有三种写法:

output = "stdout"           # 只打控制台
output = "logs/app.log"     # 写这个文件(没有扩展名时自动补 .log)
output = "logs/"            # 以 / 结尾 = 写进这个**目录**,文件名用 ipclick.log

第三种是专门处理过的:不这么做的话 logs/ 会被当成"缺扩展名的文件名", 改写成同级的 logs.log,而 logs/ 里空空如也,且不报任何错。 日志是排障的基础设施,在这里静默配错的代价远大于别处。

output 也支持 {port} 占位符,同目录多实例时不岔开会静默抢写同一个文件。

底层是 loguru。占位符是 {time} / {level} / {message} 这种花括号写法,不是 标准库 logging 的 %(asctime)s——写成后者会被识别、告警并忽略,而不是照用(照用的话 每行日志会变成一串字面量)。

下一步

Clone this wiki locally