JSON 日志(jsonlog)
PG15 给 log_destination 增加了第四种取值 jsonlog:每条日志消息输出为一个 JSON 对象,一行一个对象。它承载的信息与 csvlog 完全相同——同样的 26 类字段、同样的转义规则——但载体从「按位置取列的 CSV 行」换成了「按名字取值的 JSON 键」。对现代日志生态(Loki、ELK、Vector、Fluent Bit 等)来说,这是原生友好的输入格式:拿到就能解析,不需要为「第 12 列是 error_severity 还是 message」维护列定义。
本页讲清三件事:jsonlog 的键名体系(它不是简单复用 csvlog 的列名,而是一套官方独立定义的键)、它与日志采集工具的对接方式、以及与 csvlog / stderr 相比应该怎么选。键名清单是本页最核心的资产——对接脚本、解析器字段映射、下游告警规则,全部以这张表为准。
前置知识
基础概念
jsonlog 的三个设计事实决定了它的用法:
- 信息与 csvlog 完全一致,但键名自成体系。 官方定义了独立的 JSON 键名(如
timestamp、user、dbname、state_code、statement),与 csvlog 的列名(log_time、user_name、database_name、sql_state_code、query)同名不同形。用 csvlog 的列名去访问 jsonlog 的键,会全部得到 undefined——这是从 csvlog 迁移过来时最高频的错误。 - 一行一个 JSON 对象(JSON Lines)。 每条消息独立成行,
tail -f、jq -c、逐行流式读取都直接可用;不存在跨行拼接问题。 - 空值键被整体省略。 没有信息量的键(如非查询消息里的
statement、无 hint 时的hint)不出现在输出里,而不是输出null或空字符串。
一条真实形态的 jsonlog 行(含完整键集):
{"timestamp":"2026-09-30 09:41:22.101 +08","user":"postgres","dbname":"postgres","pid":18241,"remote_host":"[local]","session_id":"68e79c22.4741","line_num":7,"ps":"SELECT","session_start":"2026-09-30 09:40:10 +08","vxid":"4/1922","error_severity":"ERROR","state_code":"22012","message":"division by zero","statement":"SELECT 1/0;","application_name":"psql","backend_type":"client backend"}一条 LOG 级消息(无 SQL 上下文,statement / state_code 等键整体缺席):
{"timestamp":"2026-09-30 02:14:07.882 +08","pid":1001,"session_id":"68e77141.3e9","line_num":1,"session_start":"2026-09-30 02:14:01 +08","error_severity":"LOG","message":"checkpoint starting: time","backend_type":"checkpointer"}jsonlog 与 csvlog 的关系
两者是「同源不同形」:同一个 log_destination 机制下并列的两种结构化输出,字段信息一一对应,实现层面共用同一条日志装配逻辑。因此:
- 两者可以同时启用(
log_destination = 'csvlog,jsonlog'),每条消息写出两份——灰度迁移期就这么做,日志体量按双份预估。 - 键名与列名的映射关系是固定的(见详细说明的全览表),一次性建好映射表,两格式可以互换。
- 版本演进同步:csvlog 新增的列(PG13 加
leader_pid、PG14 加query_id),jsonlog 同步出现对应键(leader_pid、query_id),PG15 发布 jsonlog 时这套键已齐全。
一行一个 JSON(JSON Lines)
JSON Lines 的工程优势在《CSV 日志分析》里对比过 csvlog 的处理成本,jsonlog 把这些成本全部消掉:
- 自描述:键名即含义,解析器零配置;新增键(版本升级)不破坏旧解析逻辑。
- 流式友好:逐行
readline即可增量处理,jq一行命令完成过滤与提取。 - 生态原生:Loki 的
json解析阶段、Filebeat 的 JSON 解码器、Vector 的parse_json变换、Fluent Bit 的 JSON parser,全部开箱即用。 - 嵌套友好:未来若出现结构化复合字段,JSON 能自然表达,CSV 只能转义拼接。
代价是体积:JSON 每条消息要比 csvlog 多写一遍键名,同等日志量下文件大 30%~80%。存储敏感的场景要权衡(详见选型一节)。
空值键的省略规则
jsonlog 省略的规则很一致:该字段对这条消息没有值时,键整体不出现。典型场景:
| 场景 | 被省略的键 |
|---|---|
| 非查询消息(检查点、连接日志等) | ps、vxid、txid、statement、cursor_position |
| 无 SQLSTATE 的 LOG 消息 | state_code |
| 普通消息(无内部查询/位置信息) | internal_query、internal_position、context、func_name、file_name、file_line_num |
| 非 leader 的并行执行 | leader_pid |
| 未启用 query id 或 PG15 之前语义 | query_id |
| 远程连接(Unix socket) | remote_port(remote_host 为 [local]) |
这决定了下游的消费写法:用 // 兜底而不是假设键存在。jq 里 (.state_code // "-"),Python 里 row.get("state_code", "-"),Logstash 里 if [state_code] { ... }。
语法与用法
启用 jsonlog 三步(与启用 csvlog 的流程一致):
# 1. 结构化输出 + 文件化收集(jsonlog 必须配 logging_collector)
log_destination = 'jsonlog'
logging_collector = on
# 2. 文件管理(沿用本站《日志轮转与清理》的方案)
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d.json'
log_rotation_age = 1d
log_rotation_size = 0
# 3. 可选:错误消息的详细程度(影响 detail/hint/context 键的内容量)
log_error_verbosity = verbose# 重启使 logging_collector 生效(首次启用必须重启;已启用时改 log_destination 可 reload)
sudo systemctl restart postgresql
# 确认输出
tail -1 /var/lib/postgresql/18/main/log/postgresql-2026-09-30.json | jq .在线核对与格式自检
改配置后,用三个手段确认 jsonlog 正确产出:
# 自检 1:每行都是合法 JSON(jq 全量校验,报错行号定位)
jq -c . /var/lib/postgresql/18/main/log/postgresql-2026-09-30.json > /dev/null && echo "all lines valid"
# 自检 2:键集抽查——看看真实输出里有哪些键
tail -100 /var/lib/postgresql/18/main/log/*.json | jq -r 'keys[]' | sort | uniq -c | sort -rn
# 自检 3:触发一条已知错误,核对键值
psql -c "SELECT 1/0;" > /dev/null
tail -1 /var/lib/postgresql/18/main/log/*.json | jq 'select(.state_code == "22012") | {timestamp, state_code, message, statement}'log_line_prefix 对 jsonlog 无效——所有前缀信息本来就是结构化键。这也是 jsonlog 相比 stderr 的一个简化:不需要再维护 %t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h 这样的前缀模板。
详细说明
jsonlog 键全览(官方键名清单)
这是本页的核心资产:官方定义的全部键、类型与 csvlog 对应列。对接脚本以此表为唯一依据:
| jsonlog 键 | 类型 | 含义 | csvlog 对应列 |
|---|---|---|---|
timestamp | string | 消息时间(带毫秒与时区) | log_time |
user | string | 执行用户 | user_name |
dbname | string | 数据库名 | database_name |
pid | number | 后端进程号 | process_id |
remote_host | string | 客户端主机 / IP(本地连接为 [local]) | connection_from(拆分) |
remote_port | number | 客户端端口 | connection_from(拆分) |
session_id | string | 会话 ID(%c 同源) | session_id |
line_num | number | 会话内的消息行号 | session_line_num |
ps | string | 当前命令标签(如 SELECT) | command_tag |
session_start | string | 会话开始时间 | session_start_time |
vxid | string | 虚拟事务 ID(如 4/1921) | virtual_transaction_id |
txid | string | 顶层事务 ID | transaction_id |
error_severity | string | 消息级别(ERROR/LOG/...) | error_severity |
state_code | string | SQLSTATE(如 22012) | sql_state_code |
message | string | 主消息文本 | message |
detail | string | 详细信息(如死锁等待环) | detail |
hint | string | 提示信息 | hint |
internal_query | string | 内部查询文本 | internal_query |
internal_position | number | 内部查询出错位置 | internal_query_pos |
context | string | 调用上下文(栈) | context |
statement | string | 出错 / 被记录的语句文本 | query |
cursor_position | number | 语句中的出错位置 | query_pos |
func_name | string | 出错的源码函数 | location(拆分) |
file_name | string | 出错的源码文件 | location(拆分) |
file_line_num | number | 源码文件行号 | location(拆分) |
application_name | string | 应用名 | application_name |
backend_type | string | 后端类型(client backend / checkpointer / ...) | backend_type |
leader_pid | string | 并行 leader 进程号 | leader_pid(PG13+) |
query_id | string | 查询 ID | query_id(PG14+) |
几个值得注意的映射细节:
- 两处「一列拆两键」:csvlog 的
connection_from(形如10.0.0.11(51022))在 jsonlog 里拆成remote_host与remote_port;location(形如exec_simple_query.c:1234)拆成func_name/file_name/file_line_num三键。聚合分析时反而更方便。 txid与leader_pid用字符串表达,数字语义的键(pid、line_num、cursor_position等)是 JSON number——类型敏感的下游 schema 按此对齐。- 键集从 PG15 引入 jsonlog 起保持稳定,随内核新增列同步追加;升级版本后用「自检 2」的键集统计核对一次即可。
键名迁移:从 csvlog 到 jsonlog
把既有 csvlog 解析资产迁移到 jsonlog 时,按这张速查表替换字段名(高频键):
| csvlog(旧) | jsonlog(新) |
|---|---|
log_time | timestamp |
user_name | user |
database_name | dbname |
process_id | pid |
connection_from | remote_host + remote_port |
session_line_num | line_num |
command_tag | ps |
session_start_time | session_start |
virtual_transaction_id | vxid |
transaction_id | txid |
sql_state_code | state_code |
query | statement |
query_pos | cursor_position |
internal_query_pos | internal_position |
迁移要点:SQL 侧把 sql_state_code = '40P01' 换成 state_code = '40P01' 这类谓词;jq/Python 侧按上表改名;再按「空值键省略」一节把列默认值改成键存在性判断。
jq 命令行实战
jq 是 jsonlog 的「命令行瑞士军刀」,日常排障四板斧:
# 1) 错误流:按级别过滤,输出时间 / 级别 / 消息
jq -c 'select(.error_severity == "ERROR") | {timestamp, state_code, message}' postgresql-2026-09-30.json
# 2) 错误分诊:按 SQLSTATE 计数(死锁 40P01、唯一冲突 23505 一眼可见)
jq -r 'select(.error_severity == "ERROR" or .error_severity == "FATAL")
| (.state_code // "none")' postgresql-2026-09-30.json | sort | uniq -c | sort -rn
# 3) 慢查询小时分布:慢语句日志的 message 里提取毫秒数
jq -r 'select(.message | startswith("duration: "))
| .message | capture("duration: (?<ms>[0-9.]+) ms").ms' postgresql-2026-09-30.json \
| awk '{s+=$1; c++} END {printf "count=%d avg=%.1fms\n", c, s/c}'
# 4) 追踪单个会话:session_id 是跨消息串联的天然主键
jq -c 'select(.session_id == "68e79c22.4741") | {timestamp, ps, message}' postgresql-2026-09-30.json{"timestamp":"2026-09-30 09:41:22.101 +08","state_code":"22012","message":"division by zero"}
412 23505
18 28P01
3 40P01
count=1024 avg=142.7ms
{"timestamp":"2026-09-30 09:41:20.001 +08","ps":"SELECT","message":"statement: SELECT 1/0;"}
{"timestamp":"2026-09-30 09:41:22.101 +08","ps":"SELECT","message":"division by zero"}两个易错点:select(.state_code == "40P01") 会把没有 state_code 键的行直接过滤掉(对 LOG 消息来说正确,但要知道原因);startswith("duration: ") 比正则快得多,先用它缩小流再提取数字。
对接 Grafana Loki
Promtail 的 json 解析阶段直接消费 jsonlog,无需正则:
# promtail 配置片段
scrape_configs:
- job_name: postgresql_jsonlog
static_configs:
- targets: [localhost]
labels:
job: postgresql
host: db-main-01
__path__: /var/lib/postgresql/18/main/log/*.json
pipeline_stages:
- json:
expressions:
severity: error_severity
sqlstate: state_code
user: user
db: dbname
app: application_name
session: session_id
- labels:
severity:
- timestamp:
source: timestamp
format: "2006-01-02 15:04:05.000 MST"
location: Asia/Shanghai要点:expressions 里的右侧是 jsonlog 的键名,左侧是 promtail 内部变量名——「jsonlog 键 ≠ csvlog 列名」在这个配置里最容易踩;labels 只放低基数标签(severity、db),session_id、user 这类高基数字段只提字段不打标签。时间戳阶段注意 jsonlog 的时间不是 ISO 8601,格式串要写对。
对接 ELK(Filebeat)
Filebeat 的 JSON 解码同样零正则:
# filebeat 配置片段
filebeat.inputs:
- type: filestream
id: pg-jsonlog
paths:
- /var/lib/postgresql/18/main/log/*.json
parsers:
- ndjson:
overwrite_keys: true
add_error_key: true
processors:
- add_fields:
target: pg
fields:
host: db-main-01overwrite_keys: true 让 jsonlog 的键(message、timestamp 等)直接覆盖 Filebeat 的默认字段;add_error_key: true 把解析失败的行原样放进 error 字段,方便发现「意外混入的非 JSON 行」。Logstash/Ingest pipeline 里基于 error_severity、state_code 建告警规则即可。
对接 Vector 与 Fluent Bit
两个新一代采集器的写法:
# Vector:sources.file + transforms.parse_json
[sources.pg_jsonlog]
type = "file"
include = ["/var/lib/postgresql/18/main/log/*.json"]
read_from = "begin"
[transforms.parse]
type = "remap"
inputs = ["pg_jsonlog"]
source = '''
. = parse_json!(.message)
'''
[sinks.loki]
type = "loki"
inputs = ["parse"]
out_of_order_action = "accept"
labels.error_severity = "{{ error_severity }}"
labels.dbname = "{{ dbname }}"# Fluent Bit:tail 输入 + JSON 解析(需先注册一个 JSON parser)
[INPUT]
Name tail
Path /var/lib/postgresql/18/main/log/*.json
Tag pg.json
Parser json
DB /var/lib/fluentbit/pg-json.db
[FILTER]
Name modify
Match pg.json
Copy error_severity severity
[OUTPUT]
Name stdout # 排查期先落地验证,再切 es / loki
Match pg.jsonVector 的 parse_json! 失败会丢弃事件,灰度期可先用 parse_json(软失败)+ 错误计数观察;Fluent Bit 的 tail DB 文件保证重启后不重读,Parser json 让消息直接展开为键值对。
Python 最小消费者
没有采集器时的轻量方案——流式读取、按规则过滤、落地告警:
#!/usr/bin/env python3
"""jsonlog_tailer.py —— 增量消费 jsonlog,把 ERROR/FATAL 推到告警管道"""
import json
import sys
from_pos = int(sys.argv[2]) if len(sys.argv) > 2 else 0
with open(sys.argv[1], "r") as f:
f.seek(from_pos)
for line in f:
line = line.strip()
if not line:
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue # 半行(轮转截断)直接跳过
if row.get("error_severity") in ("ERROR", "FATAL", "PANIC"):
alert = {
"ts": row.get("timestamp"),
"sev": row["error_severity"],
"code": row.get("state_code", "-"),
"db": row.get("dbname", "-"),
"msg": row.get("message", "")[:200],
}
print(json.dumps(alert, ensure_ascii=False))
# 生产中把 f.tell() 持久化,实现断点续读{"ts":"2026-09-30 09:41:22.101 +08","sev":"ERROR","code":"22012","db":"postgres","msg":"division by zero"}模式要点:json.loads 异常兜底处理轮转造成的半行;.get() 全程防御「空值键省略」;游标位置持久化后即可安全重启。这个骨架加上 Pydantic 模型与告警通道,就是一个可用的最小监控面。
与监控视图、日志前缀的字段对齐
jsonlog 的键与监控视图字段有稳定的对应关系,交叉取证全靠它:
| jsonlog 键 | 对应视图字段 | 用途 |
|---|---|---|
pid | pg_stat_activity.pid | 日志 ↔ 实时会话互查 |
vxid | pg_locks.virtualtransaction | 锁排查的会话锚点 |
txid | pg_stat_activity.backend_xid | 事务级追踪 |
query_id | pg_stat_activity.query_id ↔ pg_stat_statements.queryid(PG14+) | 三方联动的关键键 |
session_id | (仅日志侧,log_line_prefix 的 %c) | 实时视图不暴露会话 ID,日志里串轨迹的唯一主键 |
user / dbname / application_name | 同名字段 | 画像维度 |
拿到一条 jsonlog 错误后,若 query_id 非空,直接去 pg_stat_statements 查该模板的历史画像;若只有 session_id,则在 jsonlog 内部按它串联该会话的完整轨迹——这是「ERROR 现场还原」的标准路径。
stderr、csvlog、jsonlog 三格式选型
| 维度 | stderr | csvlog | jsonlog |
|---|---|---|---|
| 引入版本 | 远古 | PG8.3 起 | PG15+ |
| 结构化 | 无(靠前缀约定) | 26 列定长结构 | 键值对,自描述 |
| 体积 | 最小 | 中 | 中偏大(键名重复) |
| 解析成本 | 高(正则/前缀约定) | 中(列映射) | 低(原生 JSON) |
| pgBadger 支持 | 支持 | 支持最好 | 需较新版本 |
| 采集生态 | 文本管道 | 文本管道 + 列定义 | 原生 JSON 管道 |
| 典型用途 | 极简部署 / journald 消费 | SQL 分析、pgBadger | 现代采集平台、快速检索 |
选型经验:已有 csvlog 体系且 pgBadger 是主要消费者,不必强迁;新建监控体系或以 Loki/ELK/Vector 为主的团队,jsonlog 是起步成本最低的选择;两者并行灰度一个周期(观察体积与查询收益)再定,log_destination 支持 'csvlog,jsonlog' 双写。
敏感信息与合规
jsonlog 与 csvlog 一样,statement 键会把语句原文写进日志——包括字面量里的数据。合规要点:
- 语句原文是敏感数据通道:
INSERT的字面量值、查询条件里的身份证号都会进日志。评估保留策略与访问控制时按「与业务数据同级」对待。 - 绑定参数可控:扩展协议语句的参数值受
log_parameter_max_length(非错误语句)与log_parameter_max_length_on_error(错误消息)控制,后者默认不记录参数。 - 列级脱敏先行:数据侧的脱敏方案(见《数据脱敏方案》)让日志里的敏感值在源头就变形,日志侧只做兜底。
- 采集管道的下游同样敏感:日志进了 Loki/ES 之后,这些平台的权限模型、保留期、加密都要纳入同一合规框架——日志外泄是常见的安全事件路径。
采集到归档的完整管道
一个生产级的端到端形态:
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
jsonlog 文件 ──────►│ 采集器 │─────►│ 检索平台 │─────►│ 告警/看板 │
postgresql-*.json │ Vector / FB │ │ Loki / ES │ │ Grafana │
└──────┬───────┘ └─────────────────┘ └──────────────┘
│ 原始文件按天压缩
▼
┌──────────────┐
│ 对象存储归档 │ 90 天~按合规要求
└──────────────┘各环节的职责边界:文件保留期 ≥ 采集器的读取延迟窗口(防删丢);检索平台只保留热数据(7~30 天);归档层满足审计但不必可即时查询。通道容量按「日志峰值 × 双写系数」预留,具体见《日志轮转与清理》。
版本与兼容性
- PG15+ 才有 jsonlog;更早版本的
log_destination取值只有stderr、csvlog、syslog、eventlog,配置里写了jsonlog会在启动时报错。 - 键集自 PG15 起稳定,后续随内核列扩展追加(与 csvlog 同步);做跨版本日志合并时,低版本文件里缺的键靠「键存在性」逻辑兼容。
- PG13 的
leader_pid、PG14 的query_id在 PG15 的 jsonlog 里已就位;对接 schema 时把它们定义为「可选键」即可覆盖老文件。 - pgBadger 对 jsonlog 的支持随版本更新(可解析 jsonlog 生成报告),用旧版 pgBadger 消费 jsonlog 前先确认其版本支持。
实战示例
示例一:错误分诊日报(jq + shell)
#!/usr/bin/env bash
# jsonlog_daily.sh —— 汇总当日 ERROR/FATAL 的 SQLSTATE 分布与样本
LOG=/var/lib/postgresql/18/main/log/postgresql-$(date +%F).json
[ -f "$LOG" ] || { echo "no log: $LOG"; exit 0; }
jq -c 'select(.error_severity == "ERROR" or .error_severity == "FATAL")
| {code: (.state_code // "-"), msg: .message}' "$LOG" \
| sort | uniq -c | sort -rn | head -10 412 {"code":"23505","msg":"duplicate key value violates unique constraint \"orders_pkey\""}
18 {"code":"28P01","msg":"password authentication failed for user \"app\""}
3 {"code":"40P01","msg":"deadlock detected"}读到 23505 集中增长先查业务写入路径,28P01 增长警惕撞库(配合 connection authorized 失败的来源 IP),40P01 走死锁专项流程。
示例二:慢查询现场还原(jq 串联)
# 1) 找到当日最慢语句所在会话
jq -c 'select(.message | startswith("duration: "))
| {ts: .timestamp, sid: .session_id,
ms: (.message | capture("duration: (?<ms>[0-9.]+) ms").ms | tonumber)}' \
postgresql-2026-09-30.json | sort_by(-.ms) | head -3
# 2) 还原该会话的完整轨迹(user/db/语句/错误全部串起来)
jq -c 'select(.session_id == "68e79c22.4741")
| {ts: .timestamp, ps: .ps, sev: .error_severity,
msg: (.message | .[0:120])}' postgresql-2026-09-30.json{"ts":"2026-09-30 02:14:07.882 +08","sid":"68e79a1d.12f","ms":18422.334}
{"ts":"2026-09-30 02:14:02.001 +08","ps":"SELECT","sev":"LOG","msg":"statement: SELECT ... FROM events JOIN ..."}
{"ts":"2026-09-30 02:14:07.882 +08","ps":"SELECT","sev":"LOG","msg":"duration: 18422.334 ms statement: SELECT ..."}session_id 让「这条慢 SQL 前后还发生了什么」有完整时间线——用户会话、语句序列、报错链条一屏看完。
示例三:Loki 灰度接入(csvlog 与 jsonlog 并行)
# promtail:双文件监听,jsonlog 打上 format=json 标签便于按格式过滤
scrape_configs:
- job_name: pg_csvlog
static_configs:
- targets: [localhost]
labels: { job: postgresql, format: csv, __path__: /var/lib/postgresql/18/main/log/*.csv }
- job_name: pg_jsonlog
static_configs:
- targets: [localhost]
labels: { job: postgresql, format: json, __path__: /var/lib/postgresql/18/main/log/*.json }
pipeline_stages:
- json:
expressions:
severity: error_severity
sqlstate: state_code
db: dbname
- labels:
severity:# 灰度验证:同一时段两条日志流的错误计数应一致
sum by (format) (count_over_time({job="postgresql"} |~ "ERROR" [1h]))灰度判定:双写 3~7 天,对比错误计数、体积增量与查询延迟;指标一致、收益明确后,把 csvlog 从 log_destination 摘除并同步关闭对应采集器。
示例四:Python 消费者接入告警管道
用「Python 最小消费者」一节的脚本对接一个 webhook:
# 运行:从上次位置继续消费,输出的告警行喂给告警网关
./jsonlog_tailer.py /var/lib/postgresql/18/main/log/postgresql-2026-09-30.json 0 \
| while read -r line; do
curl -s -X POST -H 'Content-Type: application/json' \
-d "$line" http://alert-gateway.internal/hooks/pg >/dev/null
done{"ts":"2026-09-30 09:41:22.101 +08","sev":"ERROR","code":"22012","db":"postgres","msg":"division by zero"}生产化的增强顺序:游标持久化(防重启重发)→ FATAL/PANIC 与 ERROR 分级(不同通道)→ state_code 白名单降噪(应用可预期的业务错误不打扰值班)→ 按 dbname/user 维度做聚合告警。骨架脚本 + 这些增量,成本远低于引入完整 APM。
示例五:Fluent Bit 自检(配置上线前)
# 用输出到 stdout 的方式验证解析结果(示例四的配置片段)
fluent-bit -c /etc/fluent-bit/pg-json.conf[0] pg.json: [[1727659282.101, {}], {"timestamp"=>"2026-09-30 09:41:22.101 +08",
"user"=>"postgres", "dbname"=>"postgres", "pid"=>18241,
"error_severity"=>"ERROR", "state_code"=>"22012",
"message"=>"division by zero", "statement"=>"SELECT 1/0;"}]自检清单:① 消息被展开为键值对(Parser json 生效)而非整行文本;② 空值键确实缺席(确认消费端兜底逻辑);③ 重启 fluent-bit 后无重读(DB 文件生效);④ 轮转切换文件时无告警抖动。四项全过再把 OUTPUT 从 stdout 切到正式 sink。
示例六:日志体积与键集趋势(容量治理)
采集平台接入后,用 jq 做体积画像,指导轮转与保留参数:
# 1) 按小时统计条数与字节量(对照日志量峰值时段)
jq -r '.timestamp[0:13]' postgresql-2026-09-30.json \
| sort | uniq -c | awk '{print $2, $1}' | sort
# 2) 键集分布:哪些键占据行宽大头(statement 最常见)
tail -20000 postgresql-2026-09-30.json | jq -r 'keys[]' \
| sort | uniq -c | sort -rn | head -10
# 3) 单行体积 Top 5(定位超长 statement/context)
jq -c 'select(length > 4000) | {ts: .timestamp, len: length, key: .error_severity}' \
postgresql-2026-09-30.json | head -5 412 2026-09-30 01
8210 2026-09-30 02
3114 2026-09-30 09
18204 statement
8402 error_severity
{"ts":"2026-09-30 02:14:07.882 +08","len":38211,"key":"LOG"}读法:凌晨 2 点是日志峰值(ETL 窗口);statement 键出现在 1.8 万行里,是行宽主因——如果单行长尾突出,回到 log_parameter_max_length 与 log_error_verbosity 找压缩空间。这类画像每季度跑一次,与《日志轮转与清理》的容量预算闭环。
注意事项
键名不是 csvlog 列名。 本页最值得重复的一句话:jsonlog 用 timestamp/user/dbname/state_code/statement 这套独立键名,从 csvlog 迁移过来的脚本必须改名(见迁移速查表),否则全部字段解析为空。
timestamp 的值不是 ISO 8601。 它与 csvlog 的 log_time 一样采用 log_timezone 的本地格式(形如 2026-09-30 09:41:22.101 +08,无 T 分隔符)。交给严格 ISO 解析器会失败;Loki/Filebeat/Vector 的消费配置里要指定正确的格式或先转换。
PG15+ 才可用。 更早版本的配置写了 jsonlog 会启动失败;混合版本集群(主库 15、备库 14)的备库侧配置要区分对待。
空值键省略,消费端必须兜底。 所有 .get()、//、if [key] 的防御写法不是风格问题,而是 jsonlog 输出模型的一部分。
体积比 csvlog 大。 键名每条重复一次,同等日志量文件大 30%~80%;轮转、压缩、归档的容量估算按 jsonlog 的实际体量做(见《日志轮转与清理》)。
statement 键含语句原文,按敏感数据管理。 与 csvlog 的合规要求一致(见敏感信息一节),且采集管道的下游平台要纳入同一权限与保留框架。
最佳实践
- 新建监控体系优先 jsonlog:解析零配置、生态原生友好;存量 csvlog 体系不必强行迁移,双写灰度后再定。
- 把键名全览表固化为团队资产:解析 schema、字段映射、告警规则全部引用它,避免「想当然用列名」的返工。
- 用
session_id做会话轨迹主键:它只在日志侧存在,正好承担「跨消息串联」的职责;配合query_id连通历史统计。 - 消费端统一走「键存在性」防御:
//、.get()、if [key]成为团队惯例,空值键省略永远不会变成线上故障。 - 时间戳处理写进采集配置:jsonlog 的时间非 ISO 8601,Loki/Filebeat/Vector 的格式串一次配对、持续复用。
- 采集器先落地 stdout/本地验证:Vector/Fluent Bit 上线先跑自检四项,再切正式 sink,避免「采集器故障被当成日志丢失」的误判。
- 高基数字段不打标签:
session_id、user、statement只做字段不做 label,防止基数爆炸拖垮检索平台。 - 游标与断点续读是采集器标配:文件轮转、进程重启后不重读不漏读,
DB文件或自建位移表二选一。 - 轮转参数与文件后缀联动:
log_filename用*.json后缀,清理与采集脚本的通配符同步对齐(轮转事故三的教训)。 - 合规双控制:源头用
log_parameter_max_length_on_error与数据脱敏压敏感面,管道用权限与保留期兜底,两层都要有。 - 灰度用
log_destination双写:'csvlog,jsonlog'并行 3~7 天,用同一组 LogQL/KQL 对比计数后再切换,切换窗口选低峰。 - 升级大版本后核对键集:用
jq -r 'keys[]'的键集统计对比一次,新增键(如果有)同步进解析 schema。
下一节
本章「系统视图与统计」与「日志系统」两部分到此完整收官。接下来进入慢查询专题:log_min_duration_statement 慢查询定位