采集请求要不要记日志?结构化日志体系的工程实践

采集代码写完了,很多人的日志是「print 几行 + 一个 log 文件」。直到某天要对账、要排查、要回溯,才发现日志啥都不够用——没有 request_id、没有关键指标、格式对不上。这篇记录我给采集系统搭结构化日志的完整过程:记什么、怎么记、怎么查、怎么和排查/对账配合。

为什么采集日志要「结构化」

搜索数据采集的日志,和普通应用日志不一样,它有三重用途:

  1. 排查:某天任务失败,日志要能定位是哪个关键词、哪次请求
  2. 对账:月底核算 credits,日志就是账本
  3. 质量:成功率、延迟、字段完整度,都从日志里统计

普通 print 日志满足不了这三个用途。结构化日志就是每个日志行都是「字段 = 值」,机器可解析、可按字段查询。

记什么:每个请求一个日志行

以 SerpBase(serpbase.dev)的接口为例,每个响应自带 statusrequest_idelapsed_mscredits_charged 四个字段——它们直接就是日志的核心字段,不用自己造:

import json
import logging

logger = logging.getLogger("serp.crawl")

def log_request(endpoint, params, data, error=None):
    entry = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "endpoint": endpoint,
        "keyword": params.get("q"),
        "market": params.get("gl"),
        "lang": params.get("hl"),
        "status": data.get("status") if data else None,
        "request_id": data.get("request_id") if data else None,
        "elapsed_ms": data.get("elapsed_ms") if data else None,
        "credits_charged": data.get("credits_charged") if data else None,
        "organic_count": len(data.get("organic", [])) if data else 0,
        "error": error,
    }
    logger.info(json.dumps(entry, ensure_ascii=False))

一行 JSON,机器可解析。关键设计:

  • request_id 必记:这是回溯的锚点,没有它排查无从谈起
  • 计费字段必记credits_charged 是对账的原料
  • 业务字段带全:keyword、market、lang,让日志能按业务维度筛选

怎么记:JSON Lines + 每日轮转

日志存成 JSON Lines(每行一个 JSON),配合按天轮转:

import logging.handlers

handler = logging.handlers.TimedRotatingFileHandler(
    "serp_crawl.log",
    when="midnight",        # 每天轮转
    backupCount=90,         # 保留 90 天
    encoding="utf-8",
)
handler.setFormatter(logging.Formatter("%(message)s"))  # 只留 JSON
logger.addHandler(handler)

按天轮转 + 保留周期,日志不会无限膨胀,也方便「按天查」。

怎么查:一行命令按字段筛选

结构化日志的最大好处是查询快。用 rg 或 jq 就能按字段筛:

# 某关键词的所有请求
rg '"keyword": "搜索 API"' serp_crawl.log

# 今天所有失败的请求
rg '"status": 5' serp_crawl.log

# 某 request_id 的完整记录(排查神器)
rg 'req_01Hxxxx' serp_crawl.log

不想用命令行,日志也可以直接导入 SQLite 查询:

import sqlite3, json

con = sqlite3.connect("logs.db")
con.execute("""CREATE TABLE IF NOT EXISTS crawl_logs (
    ts TEXT, endpoint TEXT, keyword TEXT, market TEXT, lang TEXT,
    status INTEGER, request_id TEXT, elapsed_ms INTEGER,
    credits REAL, organic_count INTEGER, error TEXT
)""")

# 把当天的日志行批量导入
for line in open("serp_crawl.log", encoding="utf-8"):
    try:
        e = json.loads(line)
        con.execute("INSERT INTO crawl_logs VALUES (?,?,?,?,?,?,?,?,?,?,?)",
                    (e["ts"], e["endpoint"], e.get("keyword"), e.get("market"),
                     e.get("lang"), e.get("status"), e.get("request_id"),
                     e.get("elapsed_ms"), e.get("credits_charged"),
                     e.get("organic_count"), e.get("error")))
    except (json.JSONDecodeError, KeyError):
        continue
con.commit()

导入后就是一张可 SQL 查询的表,对账、统计都是现成的。

和排查/对账怎么配合

排查场景: 用户报「数据缺了」,第一步查日志:

SELECT * FROM crawl_logs
WHERE keyword = '搜索 API' AND ts >= '2026-08-20 00:00'
ORDER BY ts DESC
LIMIT 20;

看到 status 非 0 和 request_id,立刻知道问题出在哪。

对账场景: 月底核算:

SELECT endpoint, COUNT(*), SUM(credits_charged)
FROM crawl_logs
GROUP BY endpoint;

按端点聚合,和账单对比——这就是「接口计费对账」那篇里的原料来源。

质量场景: 成功率、延迟分布:

SELECT date(ts) AS day,
       AVG(status = 0) AS success_rate,
       AVG(elapsed_ms) AS avg_latency,
       COUNT(*) AS req_count
FROM crawl_logs
GROUP BY day;

踩坑记录

坑 1:日志没有 request_id。 第一版只记了 keyword 和 status,排查时定位不到具体请求。补上 request_id 后,排查从「猜」变成「查」。

坑 2:JSON 里带中文没 ensure_ascii=False。 日志里 keyword 变成 \u641c\u7d22,可读性差。加 ensure_ascii=False 解决。

坑 3:忘记轮转。 日志无限增长,磁盘被占满,采集任务直接挂。加 TimedRotatingFileHandler 按天轮转。

坑 4:多余字段噪声。 一开始把所有响应字段都塞进日志,日志又大又难查。只记核心字段 + 业务定位字段,够用就好。

工程清单沉淀

  1. 每请求一行 JSON 日志,核心字段:request_id/status/elapsed_ms/credits_charged + 业务字段
  2. JSON Lines 存储 + 按天轮转 + 保留周期
  3. 日志可导入 SQLite,查询、对账、统计都走 SQL
  4. 排查先查日志,对账先查日志,质量先查日志

采集日志不是「写完就没用的东西」,它是排查、对账、质量三件事的共同原料。结构化 + request_id + 计费字段,让日志从「记录」升级成「基础设施」。

接口响应自带的 status/request_id/elapsed_ms/credits_charged 字段,在 SerpBase 官方文档 里都有语义说明,做日志设计时直接对照。你的采集日志是 print 还是结构化?建议从「一行 JSON」开始升级。

posted @ 2026-08-31 12:35  蜘蛛人  阅读(4)  评论(0)    收藏  举报