受け取ったログに三つの形式が混ざっていた
目標
形式が互いに異なる3つのログファイルを、1行1レコードのNDJSONに正規化し、複数行の例外を1つのレコードにまとめ、パースできなかった行を隔離したうえで、3つを共通スキーマにまとめます。
なぜ重要なのか
現場で最初に受け取るログの束は、整っていません。形式が混ざったままgrep -cを実行すると、例外1件がエラー10件として数えられ、その数字がそのまま報告書に載ります。時刻の表記もファイルごとに違うので、文字列で並べ替えると時間順になりません。そのため、分析の前に「何が1つのレコードか」と「時刻をどの文字列に固定するか」を先に決める必要があります。この決定はコードではなく契約であり、ドキュメントに残らなければ、次の人が同じファイルで違う数字を出します。
ステップ
/root/norm/gen_norm.pyを作成して実行し、/root/norm/raw/の下にapp.log・gateway.log・sys.logを作ってください。/root/norm/counts.jsonに、物理行数と論理レコード数を分けて書いてください。/root/norm/app.ndjson: アプリケーションログを1行1レコードにまとめてください。/root/norm/gw.ndjson: Combinedアクセスログを構造化してください。/root/norm/sys.ndjson: RFC 5424のsyslogを構造化してください。/root/norm/rejects.ndjson: パースできなかった行を、原文と行番号で隔離してください。/root/norm/all.ndjson: 3つを共通スキーマにまとめて、時間順に並べてください。/root/norm/norm_report.md: 正規化のルールと捨てた行を、報告書として残してください。
参考
- すべての
tsはUTCに変換して、2026-03-05T05:22:31.118Zの形で書きます。日付と時刻の間に大文字のT、ミリ秒3桁、末尾に大文字のZです。 - Pythonの
datetime.strptimeは、%zで+09:00と+0900の両方を読めます。Combinedログの時刻は%d/%b/%Y:%H:%M:%S %zで読めますが、ロケールがCのときに初めてMarが解釈されます。 - syslogの
<134>はPRIです。facilityは8で割った商、severityは余りです。 - よくあるミスは、スタックトレースの行を独立したレコードとして数えること、応答時間を秒単位の実数のままにすること、パースできなかった行を黙って飛ばすこと、時刻をUTCに変換せず文字列で並べ替えることの4つです。
- このラボの成果物は、すべて
/root/norm/の下に集めます。セッションが終わると消えるので、重要なものは画面に残しておいてください。
顧客企業のログの束を再現する
/root/norm/gen_norm.pyを作成して実行し、/root/normディレクトリの下に、app.log(91行)・gateway.log(60行)・sys.log(30行)を作ってください。
3つのファイルは、それぞれ別のプログラムが書いたものなので、形式が違います。app.logは例外が複数行なので、行数がレコード数より多く、sys.logは数行が送信中に切れています。まず/root/norm/rawを作り、その中に3つのファイルを書きます。
行数とレコード数を分けて数える
/root/norm/counts.jsonに、app_lines・app_records・gateway_records・syslog_lines・syslog_records・rejected_lines・total_recordsを書いてください。total_recordsは、3つのファイルで残ったレコードの合計です。
app.logでは、時刻で始まる行だけがレコードの先頭です。sys.logは30行ですが、RFC 5424のヘッダーを備えていない行が混ざっているので、レコード数のほうが少なくなります。2つの数字が違うという事実そのものが、このステップの答えです。
複数行の例外を1つのレコードにまとめる
/root/norm/app.ndjsonに、レコードごとにts(UTC・Z)・level・logger・msg・stack_linesを入れて、1行に1つずつ書いてください。stack_linesは、そのレコードに付属する追加の行数です。
行が時刻で始まれば新しいレコード、そうでなければ直前のレコードの続きです。このルール1つで、at …のフレームとCaused by:が自動的に前に付きます。+09:00は%zで読め、UTCへの変換はastimezone(timezone.utc)です。
アクセスログを構造化する
/root/norm/gw.ndjsonに、ts(UTC・Z)・method・path・status(整数)・bytes(整数)・rt_ms(ミリ秒の整数)を入れて、1行に1つずつ書いてください。
Combinedログの時刻は、角括弧の中に05/Mar/2026:14:22:31 +0900の形であります。%d/%b/%Y:%H:%M:%S %zで読め、%bはCロケールでMarを解釈します。最後の列の応答時間は秒単位の実数なので、1000を掛けて丸めます。
PRIをfacilityとseverityに分ける
/root/norm/sys.ndjsonに、ts(UTC・Z)・host・app・facility(整数)・severity(整数)・msgid・msgを入れて、1行に1つずつ書いてください。パースできなかった行は、ここには入れません。
RFC 5424の1行は、<PRI>1 TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSGです。PRIの中の数字1つが、2つを含んでいます。8で割った商と余りです。ヘッダーを備えていない行は今は飛ばして、次のステップで別に集めます。
パースできなかった行を捨てずに隔離する
/root/norm/rejects.ndjsonに、パースできなかった行を、src_file・line_no(1から)・raw(原文のまま)・reasonで残してください。
パーサーが読めなかった行をcontinueで飛ばすと、損失がどこにも残りません。パースの失敗は例外的な状況ではなく、1種類の結果です。行番号は元のファイルで1から数え、rawは手を加えていない原文のままでなければ、あとでたどり直せません。
3つの原本を共通スキーマにまとめる
/root/norm/all.ndjsonに、ts・source(app|gateway|syslog)・severity(ERROR|WARN|INFO)・messageを入れて、tsの昇順で書いてください。アクセスログは5xxがERROR、4xxがWARN、それ以外がINFOで、syslogはseverityの数字が3以下ならERROR、4ならWARN、それ以外がINFOです。
前のステップで作った3つのNDJSONを読んで書けば、もう一度パースする必要はありません。severityを3つの値に畳むことが、このステップの核心です。原本ごとに等級の体系が違うので、そのままでは比較できません。時刻をすでに同じ表記に固定してあるので、並べ替えは文字列のソートで済みます。
正規化のルールをドキュメントに残す
/root/norm/norm_report.mdに、## 무엇이 섞여 있었나、## 어떻게 한 줄 한 레코드로 만들었나、## 버린 줄과 그 이유、## 다음에 받을 때의 요구사항(韓国語の見出しは、順に「何が混ざっていたか」「どのように1行1レコードにしたか」「捨てた行とその理由」「次に受け取るときの要件」という意味です)の4つの節で書いてください。全レコード数・app.logの物理行数・隔離した行数を、数字で含める必要があります。
正規化のルールは、コードではなく契約です。どの行をレコードの開始と見たか、時刻をどう固定したか、読めなかった行をどこに置いたかを書いておかないと、次の人が同じファイルで違う数字を出します。数字は、counts.jsonから取り出して使ってください。