按明确的字段与优先级规则查询 journal
有效的 journal 查询应该选出能够回答具体事故问题的记录。单元身份、运行实例、时间、优先级和应用字段提供了不同维度;正确组合它们,比在整台机器的历史里找到一个熟悉词语更重要。journald 可以附加进程元数据,但不会自动收集应用写出的每个文件,也不会自动理解每条消息的业务含义。查询条件和日志实际采集路径必须相互对应。 坏掉的样子 字段匹配有明确的布尔组合规则:不同字段之间是 AND,同一字段的多个取值之间是 OR,单独一个 + 则把两组条件按 OR 连接。因此,再添加一个字段值,有时会扩大结果集,而不是进一步缩小。应把查询看成一个明确的条件表达式,先用已知记录验证它,再放进事故脚本,避免因为“参数更多了”就误认为结果必然更精确。 -p err 选择错误及更严重的级别,并不是只选择数字三。如果真正需要精确的三级记录,应使用 PRIORITY=3 字段匹配。应用在文本中写了“error”,本身也不等于设置了错误优先级;标准输出的采集使用配置好的优先级规则,除非应用通过受支持的表示方式提供级别。消息文字、日志字段和筛选阈值不能互相混同。 命令成功但没有记录,不证明服务从未运行。所选启动周期、时间窗口、命名空间、读取权限、保留策略或输出路由,都可能解释空结果。反过来,查询一个单元的全部历史,又可能找到以前的成功启动,然后错误归因到当前进程。应区分服务长期身份与某一次具体运行,不能只因为单元名称相同就把证据拼接成一次执行。 快速验证 在 PID 1 为 systemd 的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。实测环境是 Debian 12、systemd 252.39。脚本安装一个唯一命名的临时服务,并激活两次。每个工作进程向原生日志套接字发送四条较短的结构化消息,分别包含两个应用组件和几个明确的优先级: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid name='xbcatm-query-'+uuid.uuid4().hex[:8];unit=name+'.service' path=Path('/run/systemd/system')/unit assert not path.exists() def run(*args): result=subprocess.run(args,text=True,capture_output=True,timeout=10) assert result.returncode==0,(args,result.stderr) return result.stdout.strip() def query(*matches): output=run('journalctl','--no-pager','-o','json','-u',unit,*matches) return [json.loads(line) for line in output.splitlines() if line] def labels(rows):return sorted(row['MESSAGE'] for row in rows) with TemporaryDirectory(prefix=name+'-',dir='/run') as directory: root=Path(directory);worker=root/'worker.py' worker.write_text(r'''from pathlib import Path import socket phase=Path(__file__).with_name('phase').read_text() with socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) as log: log.connect('/run/systemd/journal/socket') for component,priority in [('api',2),('api',3),('api',6),('db',3)]: fields=[f'MESSAGE={phase}-{component}-{priority}',f'PRIORITY={priority}', f'LAB_RUN={phase}',f'LAB_COMPONENT={component}', '_SYSTEMD_UNIT=forged.service'] log.sendall(('\n'.join(fields)+'\n').encode()) ''') path.write_text(f'[Service]\nType=oneshot\nExecStart=/usr/bin/python3 {worker}\n') try: run('systemd-analyze','verify',str(path));run('systemctl','daemon-reload') for phase in ('first','second'): (root/'phase').write_text(phase);run('systemctl','start',unit) run('journalctl','--sync') first=query('LAB_RUN=first') assert len(first)==4 and all(row['_SYSTEMD_UNIT']==unit for row in first) second=query('LAB_RUN=second');assert len(second)==4 invocation=first[0]['_SYSTEMD_INVOCATION_ID'] assert {row['_SYSTEMD_INVOCATION_ID'] for row in first}=={invocation} assert {row['_SYSTEMD_INVOCATION_ID'] for row in second}!={invocation} assert labels(query('LAB_RUN=first','LAB_COMPONENT=api','-p','err'))==['first-api-2','first-api-3'] print('different fields AND; -p err includes critical priority 2 and error priority 3') assert labels(query('LAB_RUN=first','LAB_COMPONENT=api','PRIORITY=3'))==['first-api-3'] assert labels(query('LAB_RUN=first','LAB_COMPONENT=api','LAB_COMPONENT=db','PRIORITY=3'))==['first-api-3','first-db-3'] print('exact PRIORITY=3 differs from threshold; repeated component fields OR') union=query('LAB_RUN=first','LAB_COMPONENT=db','+','LAB_RUN=second','LAB_COMPONENT=db') assert labels(union)==['first-db-3','second-db-3'] assert labels(query('_SYSTEMD_INVOCATION_ID='+invocation,'LAB_COMPONENT=db'))==['first-db-3'] print('explicit + joins match groups; invocation filter separates repeated starts') assert query('LAB_RUN=absent')==[] print('empty query still succeeds; underscore unit field comes from journal metadata') finally: run('systemctl','stop',unit) if run('systemctl','show',unit,'-p','ActiveState','--value')=='failed':run('systemctl','reset-failed',unit) path.unlink();run('systemctl','daemon-reload') 第一次激活生成 API 严重消息、API 错误消息、API 普通信息和数据库错误消息;第二次生成相同类别,但带另一个应用运行标签。按第一次运行、API 组件和 -p err 过滤,会返回两条记录,优先级分别为二和三。改成精确匹配三级,只剩 API 错误。这组对照直接展示阈值与精确值的区别,而不是仅解释命令名称。 ...