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)はこれをさらに一歩進めます:1行に1つの JSON オブジェクト、ラッピング配列なし、末尾カンマなし。これにより JSONL ログは追記しやすく、ストリーミング可能で、すべての主要ログプラットフォームで簡単にパースできます。このガイドでは、Python、Node.js、Go から JSONL ログを生成する方法と、ELK、Fluentd、3大クラウドプロバイダーへの送信方法を学びます。
CSV や XML などの他の構造化フォーマットと比較して、JSONL は柔軟性、ツールサポート、人間の可読性の最適なバランスを提供します。フィールドはネスト可能で、型は保持され、固定スキーマを事前に定義する必要がありません。これが 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 ログファイルを tail し、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 は、Kubernetes 環境で広く使用されている CNCF 卒業プロジェクトです。両方とも 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>
クラウドログプラットフォーム
3大クラウドプロバイダーはすべて 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 Query Language)は集計、時系列分析、異常検知をサポートする強力な構造化ログデータのクエリを提供します。
// 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 ログのクエリ、相関分析、アラート設定が容易になります。これらのベストプラクティスは、毎日数十億のログエントリを処理する本番システムから導き出されたものです。
一貫したログレベルの使用
標準的な重要度スケールを採用し、すべてのサービスで一貫して使用します。最も一般的な規約では5つのレベルを使用します:開発診断用の 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"}
フィールド名の標準化
すべてのサービスで共通フィールドの共有スキーマを定義します。フィールド名には 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 ログファイルの確認、検証、変換ができます。アップロード不要、すべての処理はローカルで行われます。