适用对象:后端/平台工程师、数据工程师、SRE
场景:Web 访问日志、API 网关、容器 stdout、实时/离线 ETL
先给结论
- 每行一个 JSON 对象(NDJSON/JSON Lines) 是面向流的天然格式:边界清晰、内存友好、易切分、易回放。
- 对比「漂亮打印的多行 JSON」或「一大坨 JSON 数组」:NDJSON 更抗截断、粘包、日志轮转、单条体积异常等生产环境常见问题。
- 在 Nginx 上启用 escape=json的log_format配置,配合 Fluent Bit 的tail + json parser,可以无痛接入 ES/OpenSearch、Kafka、S3/Loki 等下游。
为什么 NDJSON 更好用
- 记录边界天然可见:\n即一条。切分、并行处理、断点续传都简单(tail -F | split | parallel)。
- 流式处理:一次一条进内存,ETL/校验/聚合对超大日志文件更友好,不用整块解析。
- 容错性:单条损坏不拖累整批(不像数组里某一条坏数据会让整体 JSON 失效)。
- 生态配套:jq | awk | sed | grep直接上手;Fluent Bit / Logstash / Vector / Beam / Spark 都天然支持行分隔输入。
- 平台现实:日志轮转、容器重启、Sidecar 复位时,按行 checkpoint 更稳(Fluent Bit 自带 offset DB)。
类比:持续出厂的零件“单件质检”比“最后把所有件焊起来再一起质检”更容易定位问题与回滚。
字段与约定(推荐模板)
- 时间戳:ts_unix(秒)或ts_unix_ms(毫秒,数字)——避免字符串解析差异。
- 幂等/追踪:request_id、trace_id、span_id。
- 请求上下文:method、path、status、latency_ms、bytes_sent、upstream_addr。
- 环境/归属:service、env、host、pod。
- 类型稳定:同一字段永远一种类型(避免 ES 映射冲突)。
- 编码:UTF-8、LF(\n),不要 BOM。
- 最大行长:限制在可接收上限内(如 32KB/64KB),异常字段做截断与 truncated=true标记。
示例(单行):
| 1 | {"ts_unix_ms": 1724554834123, "service":"web", "env":"prod", "request_id":"b0f9...", "method":"GET", "path":"/api/v1/items", "status":200, "latency_ms":12.7, "bytes_sent":5321, "upstream_addr":"10.0.0.12:8080", "ip":"203.0.113.10", "ua":"Mozilla/5.0"} | 
在 Nginx 落地:开启 NDJSON 访问日志
需求:一条请求产出一行合法 JSON,自动转义引号/换行,字段类型尽量是数字。
1)定义 NDJSON 日志格式
| 1 | # nginx.conf/http{} | 
说明:
- escape=json会把引号/换行等危险字符转义,确保一行一个对象。
- 用 $msec(秒.毫秒)乘以 1000 得到ts_unix_ms(数字)。
- 业务可按需加字段,如 route、tenant_id、traceparent($http_traceparent)。
2)确保 request_id
| 1 | # 在 server 或 http 作用域 | 
在 Fluent Bit 落地:采集、解析、投递
1)输入:Tail NDJSON 文件
| 1 | [INPUT] | 
2)解析器:JSON + 时间
如果你用了 ts_unix_ms(数字),推荐直接沿用日志里的时间,不做字符串解析。
| 1 | [PARSER] | 
说明:
ts_unix_ms是“毫秒整数”,而%s.%L期望“秒.毫秒”。两种方式二选一:
- 方式 A(简单):把 Nginx 写成
"ts":"$msec"(例如 1724554834.123),解析用%s.%L。- 方式 B(整数毫秒):继续写
"ts_unix_ms":$msec*1000,并 不设置Time_Key(由 Fluent Bit 赋值采集时间),或使用 Lua Filter把毫秒转为秒.毫秒再赋给time。多数场景方式 A 更省事。
示例(方式 A,更推荐):
| 1 | "log_format" 里写 -> '"ts":"$msec", ...' | 
3)可选:规范/补充字段
| 1 | [FILTER] | 
4)输出:示例到 OpenSearch / Kafka / Loki
OpenSearch/Elasticsearch
| 1 | [OUTPUT] | 
Kafka
| 1 | [OUTPUT] | 
Loki
| 1 | [OUTPUT] | 
ETL 实战:一条命令解决 80% 需求
筛选 5xx
| 1 | cat access.ndjson | jq -c 'select(.status >= 500)' | 
按 path 聚合 P95 延迟(示意,小文件可用)
| 1 | cat access.ndjson \ | 
裁剪字段/脱敏
| 1 | jq -c '{ts, method, path, status, latency_ms, request_id}' | 
重放到 Kafka
| 1 | cat access.ndjson | kafka-console-producer --broker-list ... --topic nginx.access | 
常见问题
- 多行内容(如后端异常栈)
- 在 Nginx 访问日志基本无此问题;应用日志务必把换行转义为 \n。
- 若必须多行,使用 Fluent Bit Multiline Parser,但那就不再是 NDJSON 了。
 
- 在 Nginx 访问日志基本无此问题;应用日志务必把换行转义为 
- 字段类型不稳定
- status/- bytes_sent/- latency_ms均应为数字。不要今天- "200"明天- 200。
- 为 ES/OpenSearch 提前准备 Index Template(禁用 dynamic 或显式 mapping)。
 
- 时间解析失败
- 最稳妥:日志里放 "$msec"(秒.毫秒),解析用%s.%L。
- ISO 8601 的 +08:00时区冒号有时会踩 parser 兼容坑。
 
- 最稳妥:日志里放 
- 超长行
- Nginx 层截断异常字段并打 truncated=true标签;Fluent Bit 开启Skip_Long_Lines On+ 监控告警。
 
- Nginx 层截断异常字段并打 
- CRLF/编码问题
- 统一 LF与 UTF-8,无 BOM。跨平台复制文件要注意\r。
 
- 统一 
- 日志轮转与丢失
- 使用 Fluent Bit DB持久化 offset;Rotate_Wait给下游一点时间;避免copytruncate带来的竞争,优先create模式。
 
- 使用 Fluent Bit 
用 JSON Schema 校验格式
示例(片段):
| 1 | { | 
在 CI/ETL 入口用 ajv/jsonschema 批量校验,避免“幽灵字段”与类型漂移。
小结
- NDJSON 把「日志是流」这件事落到了最朴素的工程抽象:一行一个事件。
- Nginx escape=json+ Fluent Bittail + json parser的组合,既能快速上线,又为后续扩展(ES/Kafka/S3/Loki)留足余地。
- 把时间与类型定死、限制行长、规范字段命名,你的日志与 ETL 就会少大半“灵异事件”。
 
                    
                     
           
                