跳转至

日志与窗口

两种读取,两个作用域

get_job_log(job_id)                 失败 step 的文本(或合并文本)
get_job_log(job_id, whole_job=true) 明确要整个 job 的合并文本(ghat_job_logs)
get_job_log(job_id, step=N)         第 N 个 step 自己的文件(ghat_step_logs)
get_job_log(job_id, failed_steps_only=true)   全部失败 step 的文本
search_job_log(job_id, query)       在合并文本里搜,返回行号与上下文
search_job_log(job_id, query, step=N)   在某个 step 的文本里搜
get_run_failed_logs(run_id)         整个 run 里失败 job 的失败 step,按文本去重后各给一份

search_job_log 与 get_job_log 用同一个 step 参数:返回里的 source (job / step)说明行号属于哪个作用域,照它把 from_line 传回去即可。 step 没采集到时返回 reason(capture_closed / not_a_step_archive …),不把 "没文档"说成"没匹配"。

失败的 job 从失败的 step 读起

不指定作用域时,由 job 自己决定:

  • 失败的 job 返回没有通过的 step 的文本,回答里带 scoped_to_failed: true—— 自行缩小了范围的回答不能读起来像一份完整文档;
  • whole_job=true 照样要合并文本:默认只是便利,不是限制;
  • 失败 step 的日志当初没采集到(GitHub 的窗口已过)时,返回合并文本并附 scope_note 说明原因。这正是"失败文本没取回来"与"根本没有日志"的区别;
  • 通过的 job、仍在运行的 job 返回合并文本。conclusion = null 不算失败:只有 failure / cancelled / timed_out / startup_failure / action_required / stale 算。

这个选择读的是 step 的结论,从不读日志文本:没有错误标记,也不猜是哪个项目。

N 个 job 打印同一条失败,就是一组

get_run_failed_logs(run_id) 读取 run 里每个失败 job 的失败 step,按逐字节相同的文本 分组:文本只给一份,外加打印它的那些 steps(job、step 号、名称、结论)。矩阵里十二个 job 因同一个原因失败是常态,这个工具一次调用回答"一个原因还是多个",而不是十二次。

分组故意是精确的:文本只要不同就是不同的组——判定"两段不同文本意思相同"属于解释, 在项目改了输出的那天就会判错。没采集到日志的失败 step 列在 steps_without_logs 里,绝不顶替; 一个失败 job 若根本没有失败 step(startup_failure),它列在 jobs_without_failed_steps 里—— 它算在 jobs_failed 之内,这个数必须能对上账。每组带文本尾部(tail=,默认 60 行,受长度 上限约束);要读完整 step 用 get_job_log(job_id=..., step=N)。

窗口只寻址,不解释

参数 语义
不传 文档最后 200 行
tail=N 最后 N 行
from_line / to_line 显式区间(1-based,闭区间)
grep 调用方给的正则(非法时退化为字面量)+ context 行上下文
prefer="tail" / "head" search_job_log 里给哪一端的匹配配上下文

日志是边跑边追加的:结论在末尾,初始化噪音在开头。所以 search_job_log 默认给最后 那些匹配配上下文;两端都会报告全部匹配位置,prefer="head" 只把文本挪到另一端——那才是 "这东西第一次出现在什么时候"要问的。

没有测试摘要锚定、没有错误标记、没有"跳过 runner 收尾日志"——这些是某个项目的输出特征, 通用跟踪器猜了就是错的。

读 step 不会触发采集

get_job_log(step=N) 只回答已存的行。要再抓一次,用 get_job(job_id, refresh_logs=true);它只在 GitHub 仍提供 step 分片时有意义 (capture.window_closes_at 之前)。

容量与清理

  • 部署 里 db usage 看当前占用;
  • 日志表按月分区,db partitions --drop-before <N>m 丢整月数据(不锁表、不需要 VACUUM)。

一条日志属于哪个月

每条日志行带着它所属的 bucket:job 完成时间所在 UTC 月的第一天。 ghat_jobs.log_bucket 存同一个值,ghat_steps.log_bucket 从 job 复制——读路径手里有 job (或 step)就知道该查哪个月,不需要再查一次。

bucket 是算出来的,不是单独存的:写入方用同一个函数(bucket_of)从 job 自己的 completed_at 算出,读取方按它寻址。这就是每个月成为可寻址单位——以及将来可整体丢弃的 单位——的原因。

它是主键的一半。 分区表要求每个主键都含分区键,所以日志表的键是 (id, bucket):id 由数据库生成(GENERATED BY DEFAULT AS IDENTITY,模型上声明为 identity 键,ORM 因而把它排除在 INSERT 之外、用 RETURNING 取回),bucket 由写入方给。忘了给,模型在构造时就报 Composite primary key field 'bucket' must be provided —— 比让 PostgreSQL 报一个指名约束而不是指名缺失字段的 CheckViolation 有用。推论是:bucket 为 NULL 的行不可能存在,"无 bucket 也能读"的读法从此等于 "读全部 bucket",字面与语义一致。

持有父行的调用方用 JobLog.store_for_job(job, text) / StepLog.store_for_step(step, text),month 由模型推导;自己构造时就得显式给 bucket。

既有库要重建:id 与 bucket 组成主键是 schema 形状的改变,而本项目没有迁移—— db reset 会重建(分区表与 pg_trgm 一并就位)。迁移期的库读仍正常,写会因子键不在而失败, 所以重建是必需步骤而非可选项。