一个原生 C 模块,让 Nginx 拥有完整 HTTP 审计能力------无需 Lua、无需 Sidecar、零运行时依赖。
网关层的 HTTP 审计,核心诉求很简单:记录完整的请求和响应对,包括 headers 和 body。
但原生 Nginx 的 access_log 天然做不到这一点------它只能记录 URI、状态码、响应时间等元数据,请求体和响应体不在其视野范围内。
OpenResty/Lua 方案可以补齐这个能力,但在金融、运营商、政企等对合规和稳定性要求极高的环境中,引入 LuaJIT 运行时本身就是一道门槛。
原生 Nginx 的审计盲区
先说说原生 Nginx 的 access_log 到底能记什么:
nginx
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
能看到:谁、什么时候、访问了什么 URL、返回了什么状态码、花了多少字节。
看不到的:请求体里提交了哪些参数、后端返回了什么业务数据、内容在压缩前长什么样。
对于等保审计来说,这相当于只记了"某人进了门",但没记"他带走了什么文件"。
更麻烦的是,即使你自己写 C 模块,Nginx 的 body_filter 链是在 gzip 压缩之后执行的。这意味着你拿到的是压缩后的乱码,不是原始的业务数据。
OpenResty 不是银弹
有人可能会说:OpenResty + Lua 就能读 body 啊。
技术上确实可以:
lua
-- access_by_lua 读请求体
ngx.req.read_body()
local req_body = ngx.req.get_body_data()
-- body_filter_by_lua 拼响应体(每个 chunk 都是新 Lua string)
resp_body = resp_body .. ngx.arg[1]
-- log_by_lua 输出 JSON 审计日志
但这个方案有几道隐形门槛:
1. 运行时依赖 = 合规风险
金融、运营商、政企的外网网关,安全基线通常写得明明白白:禁止在网关上引入动态语言运行时。
LuaJIT 是动态执行的,代码可以热更新,这意味着审计逻辑本身可能被篡改。等保测评要的是"审计系统自身可被审计",Lua 层过不了这一关。
2. 拿不到原始 body
body_filter_by_lua 运行在 gzip 压缩之后 。想要原始 body,必须开 gunzip on,让 Nginx 先解压、Lua 审计、再压缩发出去。CPU 双倍开销不说,filter 顺序还可能冲突。
3. 性能是硬伤
我们在同硬件上做过对比(openEuler 22.03, i5-10400):
| 方案 | RPS | 相对原生 |
|---|---|---|
| 原生 Nginx | 68,392 | 100% |
| OpenResty + Lua 审计 | 19,387 | 28% |
| nginx-flowlens (TLV) | 32,422 | 47% |
Lua 的字符串不可变性意味着每次 body_filter 拼接都要分配新内存。1MB 的响应体,几十个 chunk 下来,GC 压力直接爆炸。我们实测过,大文件场景下 Lua 方案的延迟能到秒级。
4. 维护成本
C 模块 crash 是段错误,coredump 一看就知道问题在哪。Lua 异常可能被 pcall 默默吞掉,线上出了问题你都不知道审计日志漏了没。
我们做了什么
nginx-flowlens 是一个 Nginx 原生 C 模块,只做一件事:在网关层完整捕获 HTTP 请求/响应对,输出结构化审计日志。
编译进 Nginx 二进制即可,两行配置启用:
nginx
inspect on;
inspect_log /var/log/nginx/inspect.log;
核心设计:在正确的位置插桩
NGX_HTTP_ACCESS_PHASE → 捕获请求头 + 读请求体
top_header_filter → 捕获响应头(gzip 之前)
top_body_filter → 累积原始响应体(gzip 之前)
log handler → 序列化为 TLV/JSON,写入日志
关键点在于 top_body_filter:我们在 gzip/brotli 压缩之前注册了 filter,所以拿到的是原始 body,不管客户端收到的是不是压缩后的。
为什么用 TLV 作为默认格式
审计日志是写密集场景。JSON 虽然人可读,但序列化开销太大:
| 格式 | 小请求 RPS | 开销 |
|---|---|---|
| baseline | 32,441 | --- |
| TLV | 29,389 | ~9% |
| JSON | 2,811 | ~91% |
TLV 是紧凑二进制格式,序列化速度约为 JSON 的 10 倍。日常调试可以用 JSON,生产环境默认 TLV,需要分析时用工具转换:
bash
python3 tools/tlv2json.py -i inspect.log -o audit.jsonl
子请求过滤
Nginx 的 auth_request、SSI 等内部机制会产生子请求。flowlens 通过 r != r->main 天然过滤,只审计用户原始请求,不会把内部鉴权请求混进审计日志。
适用场景
| 场景 | 为什么需要 |
|---|---|
| 等保/PCI-DSS 合规 | 三级等保明确要求"应能够记录应用系统的运行状态和用户行为,包括用户 ID、时间、事件类型、操作结果等"。没有 body 的日志等于半成品。 |
| API 全链路追踪 | 当线上出现"某个请求返回了错误但抓包没抓到"时,审计日志里有完整的请求/响应对,可以直接 replay。 |
| 安全取证 | 疑似攻击请求进来时,安全团队需要看到完整的请求内容,包括 POST body 里的 payload。 |
| 数据变更审计 | 金融场景的转账、支付接口,需要留存完整的请求证据。 |
不是"比 OpenResty 快",而是"没有 OpenResty 也能做"
很多人听到"Nginx C 模块"的第一反应是:「为什么要自己写 C,OpenResty 不香吗?」
香不香取决于约束条件。如果你的环境允许装 LuaJIT、允许动态执行、对性能不敏感,OpenResty 确实够用。
但如果你在以下约束下工作:
-
安全基线禁止网关引入 Lua VM
-
等保要求审计系统自身可静态源码审查
-
网关层性能预算有限(不能容忍 -70% RPS)
-
需要捕获 gzip 前的原始 body
那 OpenResty 本来就不是可选项。flowlens 的价值是从 0 到 1------让原生 Nginx 拥有原本只有外挂方案才能做到的完整审计能力。
快速体验
bash
# 克隆项目
git clone https://github.com/kumustone/nginx-flowlens.git
cd nginx-flowlens
# 一键启动开发环境(无需 root)
./run-dev.sh install
# 发一条测试请求
curl -s -X POST http://localhost:19099/ -d '{"test":true}'
# 查看审计日志(TLV → JSON)
python3 tools/tlv2json.py -i .nginx-dev/logs/inspect.log
输出示例:
json
{
"timestamp": "2026-04-21T09:40:59.628Z",
"client_ip": "127.0.0.1",
"request": {
"method": "POST", "uri": "/",
"headers": {"Content-Type": "application/json"},
"body": "eyJ0ZXN0Ijp0cnVlfQ=="
},
"response": {
"status": 200,
"headers": {"Content-Type": "text/html"},
"body": "..."
}
}
项目地址:https://github.com/kumustone/nginx-flowlens
License:Apache 2.0