-
Notifications
You must be signed in to change notification settings - Fork 0
Observability
每个请求处理完记一条:谁处理的、用了哪个适配器、状态码、耗时、重试几次。 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,用于不想把抓取目标落盘的场景。
写盘走独立的写线程 + 有界队列。队列满了就丢弃并计数,业务请求一秒都不等。
可观测性数据让业务请求变慢,是把工具变成故障源。
丢弃会累加 dropped 计数并打日志:
链路记录队列已满,累计丢弃 1234 条(写盘跟不上请求速率;可关闭 [TRACE].sqlite_enabled)
静默丢弃比丢弃更糟——你会拿着一份不完整的数据下结论,还不知道它不完整。
Web 端的统计里也会显示 dropped 和 sqlite_failed。
SQLite 写失败(磁盘满、权限、文件损坏)会把 sink 标记为 failed,之后只用内存,
服务照常跑。Web 端会显示数据来自 memory 而不是 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 / failed。failed 的定义是 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 BY(host 是独立的列,建库时就算好),不是把记录全拉到
Python 里再统计。
host用的是和按 host 限流同一套定义(limiter.host_of)。两处对 "host" 的理解 必须一致,否则排行榜和限流说的不是一回事。record_url = false时url存的已经是 host 了,解析不出来就原样用——否则整个排行会全是-。
一条记录大约 200~400 字节(取决于 URL 长度)。
| 日请求量 | 30 天大约 |
|---|---|
| 1 万 | 100 MB |
| 10 万 | 1 GB |
| 100 万 | 10 GB |
量大又只关心异常的话,only_errors = true 能把库缩小一两个数量级。
或者把 retention_days 调小。
没有 [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 = 5output 有三种写法:
output = "stdout" # 只打控制台
output = "logs/app.log" # 写这个文件(没有扩展名时自动补 .log)
output = "logs/" # 以 / 结尾 = 写进这个**目录**,文件名用 ipclick.log第三种是专门处理过的:不这么做的话
logs/会被当成"缺扩展名的文件名", 改写成同级的logs.log,而logs/里空空如也,且不报任何错。 日志是排障的基础设施,在这里静默配错的代价远大于别处。
output也支持{port}占位符,同目录多实例时不岔开会静默抢写同一个文件。
底层是 loguru。占位符是 {time} / {level} / {message} 这种花括号写法,不是
标准库 logging 的 %(asctime)s——写成后者会被识别、告警并忽略,而不是照用(照用的话
每行日志会变成一串字面量)。