pgAudit通过挂载PostgreSQL的日志钩子,把审计事件写入服务器日志,本身并不提供独立的分析界面,也不改变日志的存储方式。这意味着想用好pgAudit,前提是能看懂它的输出格式,并能可靠地把审计行从混杂着其他日志的文件里挑出来。本文围绕日志格式与解析两个环节展开,先拆解字段结构,再给出可落地的解析代码。

pgAudit日志的完整结构拆解
开启pgAudit后,每条审计记录都带有AUDIT:前缀,后面依次跟着会话标识、序列号、记录类型、声明标识、命令标记、对象标识和具体语句。一条典型的日志长这样:
AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT * FROM orders WHERE id = 42",<not logged>
各字段以逗号分隔,含义分别是:审计模式(SESSION或OBJECT)、会话内审计序号、本条语句内的记录序号、语句类型(READ、WRITE、FUNCTION、ROLE、DDL、MISC、MISC_SET等)、命令标记、对象全名(可能是数据库、模式.表、模式.函数等)、SQL语句原文、以及语句参数。最后一项在开启log_parameter_max_length等相关参数时才会显示具体绑定值,默认显示<not logged>。
有一个非常关键的细节:AUDIT:前面的部分并不属于pgAudit,而是由log_line_prefix决定的标准日志前缀。例如配置了%m %u %d时,完整一行日志可能是这样的:
2024-06-01 10:23:45.123 CST app_user postgres db1 LOG: 00000: AUDIT: SESSION,1,1,READ,SELECT,TABLE orders,"SELECT count(*) FROM orders",<not logged>
解析时要先剥掉前缀和LOG:级别信息,再定位AUDIT:标记,剩下的部分才是pgAudit的结构化内容。很多解析失败的案例,根源都是把标准日志前缀当成了pgAudit的字段去匹配。
还需要注意对象标识字段的转义规则。当对象名或语句中含有逗号、双引号时,pgAudit会按照CSV规范将整个字段用双引号包裹,并把内部的双引号转义成两个连续双引号。解析器如果不处理这种转义,遇到含逗号的字符串常量就会把字段切错位置。
SESSION与OBJECT两种模式的日志差异
pgAudit有SESSION和OBJECT两种审计粒度。SESSION模式按语句类型批量审计,比如pgaudit.log='write,ddl'会把所有写操作和DDL都记下来,此时对象标识字段常常为空或只填部分信息。OBJECT模式则依托pgaudit.role参数指定的角色,审计发生在具体对象上的访问,日志首字段会是OBJECT,且对象标识一定会填写完整的模式.对象名。
两种模式的日志示例如下。第一条来自SESSION模式,第二条来自OBJECT模式:
AUDIT: SESSION,5,1,WRITE,UPDATE,,, "UPDATE accounts SET balance = balance - 100",<not logged> AUDIT: OBJECT,3,1,READ,SELECT,TABLE public.salary,"SELECT * FROM salary LIMIT 10",<not logged>
从分析角度看,OBJECT日志更适合做对象级访问统计,因为对象名天然结构化;SESSION日志则适合还原完整的操作行为序列,包括DDL和MISC类操作。生产环境常见的做法是同时开启两者:SESSION级别记录DDL和ROLE操作保证变更可追溯,OBJECT级别只对敏感表记录读写。两种记录混在同一日志文件时,解析器可以通过首字段值直接区分路由到不同的处理逻辑。
另外要理解语句序号的含义。第二列是会话级递增号,第三列是同一条语句内多条记录的编号。一条涉及多个对象的语句(比如关联了多张表的SELECT)在OBJECT模式下会生成多条日志,它们的会话号相同、语句内序号连续。解析入库时应该用会话号加序号组合作为去重或关联键,而不是简单取整行做主键。
用Python可靠解析pgAudit日志
自己写解析逻辑时,推荐先定位AUDIT:子串,再使用Python标准库的csv模块处理剩余部分,而不是手写正则按逗号切分。csv模块天然处理引号包裹和双引号转义,能避开绝大多数字段错位问题。
import csv
import io
import re
def parse_pgaudit_line(line):
# 先去掉标准日志前缀,定位 AUDIT: 标记
pos = line.find('AUDIT: ')
if pos == -1:
return None
payload = line[pos + len('AUDIT: '):].rstrip('\n')
# 使用 csv 模块解析,处理引号转义
reader = csv.reader(io.StringIO(payload))
fields = next(reader)
keys = ['scope', 'statement_id', 'substatement_id',
'class', 'cmd', 'object_type', 'statement', 'parameter']
record = dict(zip(keys, fields))
record['statement'] = re.sub(r'\s+', ' ', record.get('statement', '')).strip()
return record
# 测试一条含逗号和引号的日志
line = '2024-06-01 10:23:45 CST LOG: AUDIT: SESSION,1,1,READ,SELECT,,"SELECT name, age FROM users WHERE remark = ""vip""",<not logged>'
print(parse_pgaudit_line(line))上面代码里的re.sub只是简单压平换行,实际项目中更建议保留语句原文,因为多行SQL的缩进信息对后续人工审查有价值。如果日志量大,可以改成逐行流式处理,配合生成器把解析结果批量写入ClickHouse或Elasticsearch。
如果选择正则方案,匹配AUDIT标记后要给语句字段使用惰性匹配并锚定行尾,例如r'AUDIT: (\w+),(\d+),(\d+),(\w+),([^,]*),(.*?),"(.*)",(.*)$'这种模式。但要注意它无法覆盖语句中包含未转义引号的极端情况,长期维护成本比csv方案高,除非有性能上的特殊要求,否则不建议作为主方案。
接入日志采集管道的实践建议
生产环境通常不会手工解析文件,而是把pgAudit日志接入统一采集管道。若使用Filebeat加Logstash,Filebeat侧只需配置收集PostgreSQL日志目录,真正的解析放在Logstash的grok或dissect过滤器中。一个可用的dissect配置思路是:先用%{}匹配掉log_line_prefix生成的固定格式前缀,再以AUDIT:为分隔符取出审计载荷,最后用csv过滤器解析字段。
dissect {
mapping => {
"message" => "%{ts} %{tz} %{user} %{db} %{loglevel}: %{seq}: AUDIT: %{audit_payload}"
}
}
csv {
source => "audit_payload"
columns => ["scope","statement_id","subid","class","cmd","object","statement","parameter"]
}几个容易踩的坑值得提前规避。第一,log_line_prefix里不要把pgaudit.log_parameter打开后又关闭log_parameter相关设置,两者不一致会导致参数字段格式不稳定。第二,如果日志走的是JSON格式输出(log_destination='jsonlog'),审计内容位于message字段内,采集端应先做一层JSON解析再提取AUDIT载荷,路径完全不同。第三,OBJECT审计在语句失败时同样可能产生记录,分析告警时要结合ERROR行判断执行结果,不能默认有AUDIT记录就代表操作成功。
最后,解析入库后的典型用途包括按class维度统计DDL变更频率、按object维度监控敏感表访问、按user维度做行为画像。只要解析层把字段结构稳定下来,上层的分析和可视化都可以灵活扩展,这也是前期认真处理格式细节的最大回报。
pgAuditPostgreSQL审计日志解析修改时间:2026-09-12 06:36:36