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。其处理器管道可让您在序列化之前增强、过滤和转换日志条目。
import structlog# Configure structlog for JSONL outputstructlog.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 linelog.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 包为本地开发提供人类可读的输出。
import pino from 'pino';// pino outputs JSONL by defaultconst log = pino({level: 'info',timestamp: pino.stdTimeFunctions.isoTime,});// Each call produces one JSONL linelog.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 contextconst reqLog = log.child({ requestId: 'req-abc-123' });reqLog.info({ path: '/api/users' }, 'request.start');
Go: zerolog
零内存分配zerolog 是 Go 的零内存分配 JSON 日志库,专为高吞吐量服务中的最大性能而设计。它直接写入 io.Writer 而无需中间分配,非常适合延迟敏感的应用。其链式 API 既优雅又高效。
package mainimport ("os""github.com/rs/zerolog""github.com/rs/zerolog/log")func main() {// Configure JSONL output to stdoutzerolog.TimeFieldFormat = zerolog.TimeFormatUnixlog.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()// Each call produces one JSONL linelog.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.ymlfilebeat.inputs:- type: logenabled: truepaths:- /var/log/app/*.jsonljson.keys_under_root: truejson.add_error_key: truejson.message_key: messagejson.overwrite_keys: trueoutput.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.confinput {beats {port => 5044codec => json}}filter {# Parse timestamp from the JSON fielddate {match => ["timestamp", "ISO8601"]target => "@timestamp"}# Add environment tagmutate {add_field => { "environment" => "production" }remove_field => ["agent", "ecs", "host"]}# Route by log levelif [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 输入。
[SERVICE]Flush 5Daemon OffLog_Level infoParsers_File parsers.conf[INPUT]Name tailPath /var/log/app/*.jsonlParser jsonTag app.*Refresh_Interval 5[FILTER]Name modifyMatch app.*Add cluster production-us-east-1Add service my-api[OUTPUT]Name esMatch app.*Host elasticsearchPort 9200Index app-logsType _docLogstash_Format On
Fluentd 与 Fluent Bit 相比提供了更丰富的插件生态系统,拥有超过 500 个社区插件。当您需要高级路由、缓冲或转换时使用它。in_tail 插件配合 format json 原生读取 JSONL 文件。
# fluentd.conf<source>@type tailpath /var/log/app/*.jsonlpos_file /var/log/fluentd/app.postag app.logs<parse>@type jsontime_key timestamptime_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 elasticsearchhost elasticsearchport 9200index_name app-logslogstash_format true<buffer>@type memoryflush_interval 5schunk_limit_size 5m</buffer></match>
云日志平台
三大主要云平台都接受 JSONL 格式的日志,并自动将 JSON 字段解析为可搜索、可过滤的属性。这消除了运行自己的日志基础设施的需要,同时提供相同的结构化查询能力。
AWS CloudWatch Logs
AWSCloudWatch Logs 自动解析 JSON 日志条目,使每个字段都可通过 CloudWatch Logs Insights 查询。当您的应用在 ECS、Lambda 或配有 CloudWatch 代理的 EC2 上运行时,JSONL 输出无需额外配置即可作为结构化数据被索引。使用 Logs Insights 类 SQL 语法可在数秒内查询数百万条日志。
# CloudWatch Logs Insights query examples# Find all errors in the last hourfields @timestamp, level, event, user_id| filter level = "error"| sort @timestamp desc| limit 100# Aggregate error counts by event typefields event| filter level = "error"| stats count(*) as error_count by event| sort error_count desc# P99 latency by endpointfields path, duration_ms| filter ispresent(duration_ms)| stats pct(duration_ms, 99) as p99 by path| sort p99 desc
GCP Cloud Logging
GCPGoogle Cloud Logging(前身为 Stackdriver)将 JSON 负载视为具有一流支持的结构化日志条目。当您在 Cloud Run、GKE 或 Cloud Functions 上将 JSONL 写入 stdout 时,日志代理将每行解析为结构化 LogEntry。severity 字段直接映射到 Cloud Logging 的严重级别,jsonPayload 字段变为可索引和可搜索的。
# Python example for Cloud Run / Cloud Functionsimport jsonimport sysdef 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 logslog("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
AzureAzure Monitor 通过 Azure Monitor Agent 或直接 API 导入接收 JSONL 日志。Application Insights 自动解析 JSON 跟踪和自定义事件。KQL(Kusto 查询语言)提供了强大的结构化日志数据查询能力,支持聚合、时间序列分析和异常检测。
// KQL queries for JSONL logs in Azure Monitor// Find recent errorsAppTraces| 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 eventsAppTraces| 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 日志文件。无需上传,所有处理在本地完成。