-
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.db" # 默认值,不带 {port};同目录起多实例时可自己
# 改成 "ipclick-trace.{port}.db" 按端口分开,见下
retention_days = 30 # 超期每小时清理一次;0 = 永久保留
only_errors = false # 只记失败的
queue_size = 5000 # 写队列容量。最小 100,写小了会告警并回落到 5000
record_url = true # false = 只记 host两层,各管一件事:
| 内存 | SQLite | |
|---|---|---|
| 默认 | 开(500 条) | 关 |
| 开销 | 零磁盘、上限固定 | 一个 db 文件 + -wal / -shm
|
| 重启 | 丢 | 留 |
| 能查 | 最近 N 条 | 历史、跨天统计、目标站点排行 |
SQLite 默认关,是因为多数人只需要"刚才发生了什么",不该默认在工作目录里造一个数据库 文件。要用 Web 端查历史就打开它。
{port}占位符。 同一目录用--port起多个实例时,这一项不岔开会让 多个进程写同一个库,链路记录会混在一起。{port}会被替换成运行时实际生效的端口 (--port覆盖之后的那个)。[LOG].output同样支持。详见 配置体系。真撞上了还会打一条明确告警,而不是静默合并。
记:时间、请求 uuid、执行节点、适配器、方法、URL、状态码、耗时、响应体大小、 重试次数、是否为转发请求、排队耗时、错误信息、是否流式。
刻意不记:请求头、cookie、请求体、代理串。
落盘那一份有截断:URL 只存前 512 字符,错误信息只存前 500 字符;内存缓冲里是完整的。 查带一长串 query 的接口时,这个差异容易被当成"记错了"。
那些含机密,而这些记录是要在 Web 端展示的。要看完整请求内容请把 [LOG].level
调成 debug——那是日志的事,日志有它自己的访问控制。
record_url = false 时只记 host,用于不想把抓取目标落盘的场景。它同时作用于 url 与
error 两个字段——错误信息里常嵌着完整 URL(重定向超上限、浏览器导航失败都会带上),
只脱敏 url 的话查询串里的密钥照样落库、照样上页面。
注意它管的是链路记录。日志文件里的完整 URL 不受这个开关约束(那是
[LOG]的事), 需要一并收敛请调整日志级别或日志的访问控制。
写盘走独立的写线程 + 有界队列。队列满了就丢弃并计数,业务请求一秒都不等。
可观测性数据让业务请求变慢,是把工具变成故障源。
丢弃会累加 dropped 计数并打日志:
链路记录队列已满,累计丢弃 1234 条(写盘跟不上请求速率;可关闭 [TRACE].sqlite_enabled)
静默丢弃比丢弃更糟——你会拿着一份不完整的数据下结论,还不知道它不完整。
Web 端的统计里也会显示 dropped 和 sqlite_failed。
SQLite 写失败(磁盘满、权限、文件损坏)会把 sink 标记为 failed,之后只用内存,
服务照常跑。Web 端会显示数据来自 memory 而不是 sqlite。
-
WAL 模式 +
synchronous = NORMAL—— 读写不互相阻塞 - 批量提交 —— 不是一条一次事务
-
auto_vacuum = INCREMENTAL—— 保留期清理后能真正把空间还给文件系统。 这个 PRAGMA 本身只在建库时生效,但每次启动都会自动检测并转换:已有库若还没 开启且体积 ≤256 MB,会自动跑一次VACUUM补上。只有超过 256 MB 的库才会跳过 自动转换(并打一条日志),需要手工执行一次VACUUM -
专门的失败索引 ——
WHERE status_code < 200 OR status_code >= 400的部分索引, 查失败请求不用全表扫 -
自动迁移 —— 每次启动都会检查表结构,已有的库缺列时自动补上(如目标站点排行用的
host列,补列时顺带从url回填),不用手动处理
Web 端的请求流页是主要入口。程序里也能直接用:
from ipclick.trace import get_recorder
r = get_recorder()
r.recent(limit=50) # 内存里最近的,返回 list[TraceRecord]
# query() 返回 (记录列表, 数据来源) 两个值
records, source = r.query(limit=100, status_class="failed")
records, source = r.query(keyword="example.com", adapter="browser")
r.stats(days=7) # 统计,结构见下query() 比 recent() 多返回一个 source:"sqlite" 或 "memory"。
落盘没开、或 sink 写坏了的时候它会自动退回内存,source 就是唯一能看出这件事的地方
——否则你会以为在查历史,其实只查到了最近 500 条。
status_class 可选 2xx / 3xx / 4xx / 5xx / failure / failed
(error 是 failed 的别名)。failed 的定义是 status_code < 200 或 >= 400,
failure 只要 < 200(也就是压根没拿到 HTTP 响应的那些)。
没有 ok 这个取值,填了等于不筛。
关键词是字面匹配——% 和 _ 会被转义,搜 api_v2 不会匹配到 apiXv2。
URL 里这两个字符太常见(/api_v2/、utm_source=),不转义的话筛出来的东西
跟你以为的不是一回事。
r.stats(days=30)
# {
# # 本进程的实时计数(重启归零)
# "process": {"total": 12045, "ok": 11890, "failed": 155,
# "success_rate": 98.7, "avg_ms": 412.3,
# "bytes": 1502398112, "by_adapter": {...}},
# # 记录后端自己的状态
# "recorder": {"source": "sqlite", "dropped": 0, "sqlite_failed": False},
#
# # 以下四项只在 sqlite_enabled 且 sink 健康时才出现
# "window_days": 30,
# "window": {...}, # 这个时间窗的聚合
# "daily": [...], # 按天趋势
# "top_hosts": [...], # 目标站点排行
# }process 里除了上面这些,还有两组容易被忽略的字段:
-
in_flight/peak_in_flight—— 当前正在执行、尚未返回的请求数,以及本次 启动以来的峰值。总览页和请求流页都会显示这两个数字。 -
retries/rejected—— 按"原因"字符串分类的累计次数,key 是原因本身: 适配器重试的browser:status_code、browser:exception(来自重试逻辑), 准入阶段拒绝的unauthenticated(鉴权失败)、以及按错误分类给出的其他标签 (如failed_precondition、invalid_argument)。这两组目前只能通过stats()取到,页面上没有单独的展示位。
两层的分工要分清:process 是本进程从启动至今的实时计数(重启就归零,也不跨节点),
window / daily / top_hosts 才是落盘数据的聚合。只看 process 会把"刚重启过"
误读成"没有流量"。
统计结果有 10 秒的窗口缓存。Web 端默认每 5 秒轮询一次(请求流页最快可切到 1 秒), 不缓存的话每次都会对整个保留期做一次聚合——记录一多就是每几秒扫一遍几十万行。 缓存 TTL 取 10 秒就是按这个轮询周期定的。
目标站点排行走 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 也不难。
也因此不需要 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——写成后者会被识别、告警并忽略,而不是照用(照用的话
每行日志会变成一串字面量)。