JSONL 结构化日志

使用 JSONL(JSON Lines)格式进行结构化日志的综合指南。学习与 ELK Stack、Fluentd、CloudWatch、GCP Logging 和 Azure Monitor 的集成,附生产级代码示例。

最后更新:2026年2月

为什么使用 JSONL 做结构化日志?

传统的纯文本日志人类可读,但机器几乎无法可靠解析。一行像 '2024-01-15 ERROR User login failed for admin' 这样的日志将严重级别、事件类型和用户名埋在非结构化字符串中。每个日志聚合工具都不得不依赖脆弱的正则表达式来提取信息,任何日志格式的变更都会破坏管道。

结构化日志通过将每条日志条目作为具有明确字段的 JSON 对象输出来解决这个问题。JSONL(JSON Lines)更进一步:每行一个 JSON 对象,没有包裹数组,没有尾随逗号。这使得 JSONL 日志易于追加、可流式传输,并且所有主流日志平台都能轻松解析。在本指南中,您将学习如何从 Python、Node.js 和 Go 生成 JSONL 日志,以及如何将它们发送到 ELK、Fluentd 和三大主要云平台。

与 CSV 或 XML 等其他结构化格式相比,JSONL 在灵活性、工具支持和人类可读性之间提供了最佳平衡。字段可以嵌套,类型得以保留,而且您永远不需要预先定义固定的 Schema。这就是为什么 JSONL 已成为现代基础设施中结构化日志的事实标准。

各语言日志库

大多数现代日志库原生支持结构化 JSON 输出,或通过简单的配置变更即可实现。以下是 Python、Node.js 和 Go 的推荐库,每个都能生成兼容 JSONL 的输出。

Python: structlog

推荐

structlog 是 Python 领先的结构化日志库。它包装了标准 logging 模块,默认输出 JSON。其处理器管道可让您在序列化之前增强、过滤和转换日志条目。

Python: structlog
import structlog
# Configure structlog for JSONL output
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.BoundLogger,
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
log = structlog.get_logger()
# Each call produces one JSONL line
log.info("user.login", user_id="u-42", ip="10.0.0.1")
# {"event":"user.login","user_id":"u-42","ip":"10.0.0.1","level":"info","timestamp":"2026-02-15T08:30:00Z"}
log.error("db.connection_failed", host="db-primary", retries=3)
# {"event":"db.connection_failed","host":"db-primary","retries":3,"level":"error","timestamp":"2026-02-15T08:30:01Z"}

Node.js: pino

最快

pino 是最快的 Node.js 日志库,默认零配置输出 JSONL。它通过异步写入和最小化的序列化路径实现低开销。配套的 pino-pretty 包为本地开发提供人类可读的输出。

Node.js: pino
import pino from 'pino';
// pino outputs JSONL by default
const log = pino({
level: 'info',
timestamp: pino.stdTimeFunctions.isoTime,
});
// Each call produces one JSONL line
log.info({ userId: 'u-42', ip: '10.0.0.1' }, 'user.login');
// {"level":30,"time":"2026-02-15T08:30:00.000Z","userId":"u-42","ip":"10.0.0.1","msg":"user.login"}
log.error({ host: 'db-primary', retries: 3 }, 'db.connection_failed');
// {"level":50,"time":"2026-02-15T08:30:01.000Z","host":"db-primary","retries":3,"msg":"db.connection_failed"}
// Child loggers inherit context
const reqLog = log.child({ requestId: 'req-abc-123' });
reqLog.info({ path: '/api/users' }, 'request.start');

Go: zerolog

零内存分配

zerolog 是 Go 的零内存分配 JSON 日志库,专为高吞吐量服务中的最大性能而设计。它直接写入 io.Writer 而无需中间分配,非常适合延迟敏感的应用。其链式 API 既优雅又高效。

Go: zerolog
package main
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Configure JSONL output to stdout
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
// Each call produces one JSONL line
log.Info().
Str("user_id", "u-42").
Str("ip", "10.0.0.1").
Msg("user.login")
// {"level":"info","user_id":"u-42","ip":"10.0.0.1","time":1739608200,"message":"user.login"}
log.Error().
Str("host", "db-primary").
Int("retries", 3).
Msg("db.connection_failed")
}

ELK Stack 集成

ELK Stack(Elasticsearch、Logstash、Kibana)是部署最广泛的开源日志管理平台。JSONL 日志无缝集成,因为 Logstash 和 Filebeat 可以原生解析 JSON,无需复杂的 grok 模式。

Filebeat 是轻量级日志收集器,它监视您的 JSONL 日志文件并将其转发到 Logstash 或 Elasticsearch。将 json.keys_under_root 设为 true 可将 JSON 字段提升为 Elasticsearch 的顶层字段,使其可直接在 Kibana 中搜索。

Filebeat 配置
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.jsonl
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
json.overwrite_keys: true
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
index: "app-logs-%{+yyyy.MM.dd}"
# Optional: send to Logstash instead
# output.logstash:
# hosts: ["logstash:5044"]

如果您需要在索引之前转换或丰富日志,Logstash 位于 Filebeat 和 Elasticsearch 之间。json 编解码器自动解析每行 JSONL。使用 mutate 过滤器重命名字段、添加标签或在存储前移除敏感数据。

Logstash 管道
# logstash.conf
input {
beats {
port => 5044
codec => json
}
}
filter {
# Parse timestamp from the JSON field
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
# Add environment tag
mutate {
add_field => { "environment" => "production" }
remove_field => ["agent", "ecs", "host"]
}
# Route by log level
if [level] == "error" or [level] == "fatal" {
mutate { add_tag => ["alert"] }
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}

Fluentd 与 Fluent Bit

Fluentd 及其轻量级兄弟 Fluent Bit 是 CNCF 毕业项目,在 Kubernetes 环境中广泛使用。两者都原生处理 JSONL 日志,可以将其转发到几乎任何目标,从 Elasticsearch 到 S3 再到云原生日志服务。

Fluent Bit 是 Kubernetes 和资源受限环境中首选的日志收集器。它仅消耗约 450KB 内存,每秒可处理数十万条记录。json 解析器无需任何自定义配置即可处理 JSONL 输入。

Fluent Bit 配置
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/app/*.jsonl
Parser json
Tag app.*
Refresh_Interval 5
[FILTER]
Name modify
Match app.*
Add cluster production-us-east-1
Add service my-api
[OUTPUT]
Name es
Match app.*
Host elasticsearch
Port 9200
Index app-logs
Type _doc
Logstash_Format On

Fluentd 与 Fluent Bit 相比提供了更丰富的插件生态系统,拥有超过 500 个社区插件。当您需要高级路由、缓冲或转换时使用它。in_tail 插件配合 format json 原生读取 JSONL 文件。

Fluentd 配置
# fluentd.conf
<source>
@type tail
path /var/log/app/*.jsonl
pos_file /var/log/fluentd/app.pos
tag app.logs
<parse>
@type json
time_key timestamp
time_format %Y-%m-%dT%H:%M:%S%z
</parse>
</source>
<filter app.logs>
@type record_transformer
<record>
hostname "#{Socket.gethostname}"
environment production
</record>
</filter>
<match app.logs>
@type elasticsearch
host elasticsearch
port 9200
index_name app-logs
logstash_format true
<buffer>
@type memory
flush_interval 5s
chunk_limit_size 5m
</buffer>
</match>

云日志平台

三大主要云平台都接受 JSONL 格式的日志,并自动将 JSON 字段解析为可搜索、可过滤的属性。这消除了运行自己的日志基础设施的需要,同时提供相同的结构化查询能力。

AWS CloudWatch Logs

AWS

CloudWatch Logs 自动解析 JSON 日志条目,使每个字段都可通过 CloudWatch Logs Insights 查询。当您的应用在 ECS、Lambda 或配有 CloudWatch 代理的 EC2 上运行时,JSONL 输出无需额外配置即可作为结构化数据被索引。使用 Logs Insights 类 SQL 语法可在数秒内查询数百万条日志。

AWS CloudWatch Logs
# CloudWatch Logs Insights query examples
# Find all errors in the last hour
fields @timestamp, level, event, user_id
| filter level = "error"
| sort @timestamp desc
| limit 100
# Aggregate error counts by event type
fields event
| filter level = "error"
| stats count(*) as error_count by event
| sort error_count desc
# P99 latency by endpoint
fields path, duration_ms
| filter ispresent(duration_ms)
| stats pct(duration_ms, 99) as p99 by path
| sort p99 desc

GCP Cloud Logging

GCP

Google Cloud Logging(前身为 Stackdriver)将 JSON 负载视为具有一流支持的结构化日志条目。当您在 Cloud Run、GKE 或 Cloud Functions 上将 JSONL 写入 stdout 时,日志代理将每行解析为结构化 LogEntry。severity 字段直接映射到 Cloud Logging 的严重级别,jsonPayload 字段变为可索引和可搜索的。

GCP Cloud Logging
# Python example for Cloud Run / Cloud Functions
import json
import sys
def log(severity: str, message: str, **fields):
"""Write a JSONL log entry compatible with GCP Cloud Logging."""
entry = {
"severity": severity.upper(), # Maps to Cloud Logging severity
"message": message,
**fields,
}
print(json.dumps(entry), file=sys.stdout, flush=True)
# GCP automatically parses these as structured logs
log("INFO", "user.login", user_id="u-42", ip="10.0.0.1")
log("ERROR", "db.timeout", host="db-primary", latency_ms=5200)
# Query in Cloud Logging:
# jsonPayload.user_id = "u-42"
# severity >= ERROR

Azure Monitor Logs

Azure

Azure Monitor 通过 Azure Monitor Agent 或直接 API 导入接收 JSONL 日志。Application Insights 自动解析 JSON 跟踪和自定义事件。KQL(Kusto 查询语言)提供了强大的结构化日志数据查询能力,支持聚合、时间序列分析和异常检测。

Azure Monitor Logs
// KQL queries for JSONL logs in Azure Monitor
// Find recent errors
AppTraces
| where SeverityLevel >= 3
| extend parsed = parse_json(Message)
| project TimeGenerated, parsed.event, parsed.user_id,
parsed.error_message
| order by TimeGenerated desc
| take 100
// Error rate over time (5-minute buckets)
AppTraces
| where SeverityLevel >= 3
| summarize ErrorCount = count() by bin(TimeGenerated, 5m)
| render timechart
// Top error events
AppTraces
| where SeverityLevel >= 3
| extend parsed = parse_json(Message)
| summarize Count = count() by tostring(parsed.event)
| top 10 by Count

JSONL 日志最佳实践

在所有服务中遵循一致的规范使 JSONL 日志更易于查询、关联和告警。这些最佳实践来自每天处理数十亿条日志的生产系统。

使用一致的日志级别

采用标准的严重级别并在所有服务中一致使用。最常见的约定使用五个级别:DEBUG 用于开发诊断,INFO 用于正常操作,WARN 用于可恢复的问题,ERROR 用于需要关注的故障,FATAL 用于不可恢复的崩溃。在导入时将数字级别(如 pino 的 10-60 刻度)映射为这些字符串名称,以实现统一查询。

使用一致的日志级别
{"level":"debug","event":"cache.miss","key":"user:42","timestamp":"2026-02-15T08:30:00Z"}
{"level":"info","event":"request.completed","method":"GET","path":"/api/users","status":200,"duration_ms":45,"timestamp":"2026-02-15T08:30:01Z"}
{"level":"warn","event":"rate_limit.approaching","client_id":"c-99","current":950,"limit":1000,"timestamp":"2026-02-15T08:30:02Z"}
{"level":"error","event":"payment.failed","order_id":"ord-123","error":"card_declined","timestamp":"2026-02-15T08:30:03Z"}
{"level":"fatal","event":"startup.failed","reason":"missing DATABASE_URL","timestamp":"2026-02-15T08:30:04Z"}

标准化字段名称

为所有服务的通用字段定义共享 Schema。字段名使用 snake_case,时间戳使用 ISO 8601,关联标识符使用一致的命名。至少每条日志条目应包含:timestamp、level、event(机器可读的事件名称)和 service。请求范围的条目应包含 request_id 和 user_id 以便追踪。

标准化字段名称
// Recommended field schema
{
"timestamp": "2026-02-15T08:30:00.000Z", // ISO 8601, always UTC
"level": "info", // debug|info|warn|error|fatal
"event": "order.created", // dot-notation event name
"service": "order-api", // emitting service
"version": "1.4.2", // service version
"request_id": "req-abc-123", // correlation ID
"trace_id": "4bf92f3577b34da6", // distributed trace ID
"user_id": "u-42", // authenticated user
"duration_ms": 120, // numeric, not string
"order_id": "ord-789", // domain-specific context
"items_count": 3 // domain-specific context
}

性能注意事项

结构化日志为每次日志调用增加了序列化开销。在高吞吐量服务中,这可能变得显著。使用异步日志(pino、zerolog)将序列化从热路径移开。避免在 INFO 级别记录大型对象或请求/响应正文。对高频调试日志使用采样。写入本地文件并让边车(Filebeat、Fluent Bit)处理传输,而不是在日志路径中同步发送网络请求。

使用异步日志库(pino、zerolog),避免在 JSON 序列化期间阻塞主线程。

永远不要在 INFO 级别记录完整的请求或响应正文。使用 DEBUG 级别,仅在排查问题时启用。

对高频事件进行采样。每 100 个健康检查请求记录 1 个,而非全部记录。

将日志写入本地文件,使用 Filebeat 或 Fluent Bit 异步传输。避免在日志路径中进行同步网络调用。

设置最大日志行大小(如 16KB),防止过大的条目堵塞管道。

使用 logrotate 或内置的库轮转功能进行日志文件轮转,防止磁盘耗尽。

在线验证您的 JSONL 日志

使用我们免费的浏览器工具检查、验证和转换 JSONL 日志文件。无需上传,所有处理在本地完成。

在浏览器中检查 JSONL 日志

查看、搜索和验证最大 1GB 的 JSONL 日志文件。即时加载,客户端处理,100% 私密。

常见问题

JSONL 日志 — 结构化日志、ELK/Fluentd 与云端摄取 | jsonl.co