使用 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 codec 自動解析每個 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
與 Fluent Bit 相比,Fluentd 提供更豐富的外掛生態系統,擁有超過 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 層級記錄大型物件或請求/回應本文。對高流量除錯日誌使用取樣。寫入本機檔案並讓 sidecar(Filebeat、Fluent Bit)處理傳送,而不是同步透過網路發送日誌。
使用非同步日誌器(pino、zerolog)避免在 JSON 序列化期間阻塞主執行緒。
永遠不要在 INFO 層級記錄完整的請求或回應本文。使用 DEBUG 層級,僅在排錯時啟用。
對高流量事件進行取樣。記錄每 100 個健康檢查請求中的 1 個,而不是全部記錄。
將日誌寫入本機檔案,使用 Filebeat 或 Fluent Bit 非同步傳送。避免在日誌路徑中進行同步網路呼叫。
設定最大日誌行大小(例如 16KB),防止過大的條目阻塞管線。
使用 logrotate 或函式庫內建的輪替功能輪替日誌檔案,防止磁碟空間耗盡。
線上驗證你的 JSONL 日誌
使用我們的免費瀏覽器工具檢查、驗證和轉換 JSONL 日誌檔案。無需上傳,所有處理在本機進行。