Skip to content

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 的三个设计事实决定了它的用法:

  1. 信息与 csvlog 完全一致,但键名自成体系。 官方定义了独立的 JSON 键名(如 timestamp、user、dbname、state_code、statement),与 csvlog 的列名(log_time、user_name、database_name、sql_state_code、query)同名不同形。用 csvlog 的列名去访问 jsonlog 的键,会全部得到 undefined——这是从 csvlog 迁移过来时最高频的错误。
  2. 一行一个 JSON 对象(JSON Lines)。 每条消息独立成行,tail -f、jq -c、逐行流式读取都直接可用;不存在跨行拼接问题。
  3. 空值键被整体省略。 没有信息量的键(如非查询消息里的 statement、无 hint 时的 hint)不出现在输出里,而不是输出 null 或空字符串。

一条真实形态的 jsonlog 行(含完整键集):

json
{"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 等键整体缺席):

json
{"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 的流程一致):

conf
# 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
bash
# 重启使 logging_collector 生效(首次启用必须重启;已启用时改 log_destination 可 reload)
sudo systemctl restart postgresql

# 确认输出
tail -1 /var/lib/postgresql/18/main/log/postgresql-2026-09-30.json | jq .

在线核对与格式自检 ​

改配置后,用三个手段确认 jsonlog 正确产出:

bash
# 自检 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 对应列
timestampstring消息时间(带毫秒与时区)log_time
userstring执行用户user_name
dbnamestring数据库名database_name
pidnumber后端进程号process_id
remote_hoststring客户端主机 / IP(本地连接为 [local])connection_from(拆分)
remote_portnumber客户端端口connection_from(拆分)
session_idstring会话 ID(%c 同源)session_id
line_numnumber会话内的消息行号session_line_num
psstring当前命令标签(如 SELECT)command_tag
session_startstring会话开始时间session_start_time
vxidstring虚拟事务 ID(如 4/1921)virtual_transaction_id
txidstring顶层事务 IDtransaction_id
error_severitystring消息级别(ERROR/LOG/...)error_severity
state_codestringSQLSTATE(如 22012)sql_state_code
messagestring主消息文本message
detailstring详细信息(如死锁等待环)detail
hintstring提示信息hint
internal_querystring内部查询文本internal_query
internal_positionnumber内部查询出错位置internal_query_pos
contextstring调用上下文(栈)context
statementstring出错 / 被记录的语句文本query
cursor_positionnumber语句中的出错位置query_pos
func_namestring出错的源码函数location(拆分)
file_namestring出错的源码文件location(拆分)
file_line_numnumber源码文件行号location(拆分)
application_namestring应用名application_name
backend_typestring后端类型(client backend / checkpointer / ...)backend_type
leader_pidstring并行 leader 进程号leader_pid(PG13+)
query_idstring查询 IDquery_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_timetimestamp
user_nameuser
database_namedbname
process_idpid
connection_fromremote_host + remote_port
session_line_numline_num
command_tagps
session_start_timesession_start
virtual_transaction_idvxid
transaction_idtxid
sql_state_codestate_code
querystatement
query_poscursor_position
internal_query_posinternal_position

迁移要点:SQL 侧把 sql_state_code = '40P01' 换成 state_code = '40P01' 这类谓词;jq/Python 侧按上表改名;再按「空值键省略」一节把列默认值改成键存在性判断。

jq 命令行实战 ​

jq 是 jsonlog 的「命令行瑞士军刀」,日常排障四板斧:

bash
# 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
text
{"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,无需正则:

yaml
# 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 解码同样零正则:

yaml
# 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-01

overwrite_keys: true 让 jsonlog 的键(message、timestamp 等)直接覆盖 Filebeat 的默认字段;add_error_key: true 把解析失败的行原样放进 error 字段,方便发现「意外混入的非 JSON 行」。Logstash/Ingest pipeline 里基于 error_severity、state_code 建告警规则即可。

对接 Vector 与 Fluent Bit ​

两个新一代采集器的写法:

toml
# 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 }}"
conf
# 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.json

Vector 的 parse_json! 失败会丢弃事件,灰度期可先用 parse_json(软失败)+ 错误计数观察;Fluent Bit 的 tail DB 文件保证重启后不重读,Parser json 让消息直接展开为键值对。

Python 最小消费者 ​

没有采集器时的轻量方案——流式读取、按规则过滤、落地告警:

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() 持久化,实现断点续读
text
{"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 键对应视图字段用途
pidpg_stat_activity.pid日志 ↔ 实时会话互查
vxidpg_locks.virtualtransaction锁排查的会话锚点
txidpg_stat_activity.backend_xid事务级追踪
query_idpg_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 三格式选型 ​

维度stderrcsvlogjsonlog
引入版本远古PG8.3 起PG15+
结构化无(靠前缀约定)26 列定长结构键值对,自描述
体积最小中中偏大(键名重复)
解析成本高(正则/前缀约定)中(列映射)低(原生 JSON)
pgBadger 支持支持支持最好需较新版本
采集生态文本管道文本管道 + 列定义原生 JSON 管道
典型用途极简部署 / journald 消费SQL 分析、pgBadger现代采集平台、快速检索

选型经验:已有 csvlog 体系且 pgBadger 是主要消费者,不必强迁;新建监控体系或以 Loki/ELK/Vector 为主的团队,jsonlog 是起步成本最低的选择;两者并行灰度一个周期(观察体积与查询收益)再定,log_destination 支持 'csvlog,jsonlog' 双写。

敏感信息与合规 ​

jsonlog 与 csvlog 一样,statement 键会把语句原文写进日志——包括字面量里的数据。合规要点:

  1. 语句原文是敏感数据通道:INSERT 的字面量值、查询条件里的身份证号都会进日志。评估保留策略与访问控制时按「与业务数据同级」对待。
  2. 绑定参数可控:扩展协议语句的参数值受 log_parameter_max_length(非错误语句)与 log_parameter_max_length_on_error(错误消息)控制,后者默认不记录参数。
  3. 列级脱敏先行:数据侧的脱敏方案(见《数据脱敏方案》)让日志里的敏感值在源头就变形,日志侧只做兜底。
  4. 采集管道的下游同样敏感:日志进了 Loki/ES 之后,这些平台的权限模型、保留期、加密都要纳入同一合规框架——日志外泄是常见的安全事件路径。

采集到归档的完整管道 ​

一个生产级的端到端形态:

text
                     ┌──────────────┐      ┌─────────────────┐      ┌──────────────┐
 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) ​

bash
#!/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
text
 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 串联) ​

bash
# 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
text
{"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 并行) ​

yaml
# 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:
logql
# 灰度验证:同一时段两条日志流的错误计数应一致
sum by (format) (count_over_time({job="postgresql"} |~ "ERROR" [1h]))

灰度判定:双写 3~7 天,对比错误计数、体积增量与查询延迟;指标一致、收益明确后,把 csvlog 从 log_destination 摘除并同步关闭对应采集器。

示例四:Python 消费者接入告警管道 ​

用「Python 最小消费者」一节的脚本对接一个 webhook:

bash
# 运行:从上次位置继续消费,输出的告警行喂给告警网关
./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
text
{"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 自检(配置上线前) ​

bash
# 用输出到 stdout 的方式验证解析结果(示例四的配置片段)
fluent-bit -c /etc/fluent-bit/pg-json.conf
text
[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 做体积画像,指导轮转与保留参数:

bash
# 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
text
    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 的合规要求一致(见敏感信息一节),且采集管道的下游平台要纳入同一权限与保留框架。

最佳实践 ​

  1. 新建监控体系优先 jsonlog:解析零配置、生态原生友好;存量 csvlog 体系不必强行迁移,双写灰度后再定。
  2. 把键名全览表固化为团队资产:解析 schema、字段映射、告警规则全部引用它,避免「想当然用列名」的返工。
  3. 用 session_id 做会话轨迹主键:它只在日志侧存在,正好承担「跨消息串联」的职责;配合 query_id 连通历史统计。
  4. 消费端统一走「键存在性」防御://、.get()、if [key] 成为团队惯例,空值键省略永远不会变成线上故障。
  5. 时间戳处理写进采集配置:jsonlog 的时间非 ISO 8601,Loki/Filebeat/Vector 的格式串一次配对、持续复用。
  6. 采集器先落地 stdout/本地验证:Vector/Fluent Bit 上线先跑自检四项,再切正式 sink,避免「采集器故障被当成日志丢失」的误判。
  7. 高基数字段不打标签:session_id、user、statement 只做字段不做 label,防止基数爆炸拖垮检索平台。
  8. 游标与断点续读是采集器标配:文件轮转、进程重启后不重读不漏读,DB 文件或自建位移表二选一。
  9. 轮转参数与文件后缀联动:log_filename 用 *.json 后缀,清理与采集脚本的通配符同步对齐(轮转事故三的教训)。
  10. 合规双控制:源头用 log_parameter_max_length_on_error 与数据脱敏压敏感面,管道用权限与保留期兜底,两层都要有。
  11. 灰度用 log_destination 双写:'csvlog,jsonlog' 并行 3~7 天,用同一组 LogQL/KQL 对比计数后再切换,切换窗口选低峰。
  12. 升级大版本后核对键集:用 jq -r 'keys[]' 的键集统计对比一次,新增键(如果有)同步进解析 schema。

下一节

本章「系统视图与统计」与「日志系统」两部分到此完整收官。接下来进入慢查询专题:log_min_duration_statement 慢查询定位

参考链接 ​