分散システムのトラブルシューティングを加速する:awkによる複数行ログの自動抽出と構造化

Tech

{“status”: “production-ready”, “engine”: “gemini-1.5-pro”, “focus”: “SRE/Log-Analysis”, “technique”: [“awk-RS”, “awk-getline”, “structured-logging”]}

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

分散システムのトラブルシューティングを加速する:awkによる複数行ログの自動抽出と構造化

【導入と前提】 JavaスタックトレースやSQLスロークエリ等、複数行にわたる非構造化ログを特定のパターンで抽出し、JSON形式へ整形して監視基盤へ転送する処理を自動化します。

  • 前提環境: GNU/Linux (Ubuntu/RHEL), gawk (GNU awk) 4.0+, jq, systemd

【処理フローと設計】

graph TD
A["ログファイル監視"] --> B{"awkによるレコード分離"}
B -->|RS: レコード区切り| C["複数行パターンの抽出"]
C -->|getline: 次行結合| D["一時オブジェクト生成"]
D -->|jq| E["構造化JSON出力"]
E --> F["監視基盤/外部通知"]

この設計では、RS (Record Separator) を正規表現で定義することで、1つのログエントリ(タイムスタンプから次のタイムスタンプまで)を単一の「レコード」として扱います。

【実装:堅牢な自動化スクリプト】 以下のスクリプトは、アプリケーションログからエラー発生時のスタックトレースを含むコンテキストを抽出し、構造化します。

#!/bin/bash


# log_processor.sh - 複数行ログの抽出とJSON化

set -euo pipefail # エラー発生時に停止、未定義変数参照禁止、パイプエラーの伝播
IFS=$'\n\t'

# 設定

LOG_FILE="/var/log/app/production.log"
OUTPUT_JSON="/var/log/app/error_report.json"
TMP_FILE=$(mktemp)

# クリーンアップ処理

trap 'rm -f "$TMP_FILE"' EXIT

echo "Starting log extraction at $(date)"

# gawkによる複数行抽出処理


# RS (Record Separator) にタイムスタンプのパターンを指定し、1エントリを1レコードとして処理

gawk '
BEGIN {

    # 2024-05-20 10:00:00 のような形式の行頭を区切り文字に設定 (gawk拡張)

    RS = "\n[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"
}
/ERROR|CRITICAL/ {

    # RT (Record Terminator) は一致した区切り文字を保持している

    timestamp = gensub(/^\n/, "", "g", RT)
    content = $0

    # 特殊なパターンがあった場合、getlineでさらに後続を読み込む例

    if (content ~ /SpecificException/) {

        # 必要に応じて追加行を取得可能


        # getline extra_line


        # content = content "\n" extra_line

    }

    # JSONの中間形式として整形出力

    printf "%s\t%s\n", (timestamp == "" ? "INIT_START" : timestamp), content
}' "$LOG_FILE" > "$TMP_FILE"

# jqによるJSON構造化とファイル出力

cat "$TMP_FILE" | jq -R -s '
  split("\n") | map(select(length > 0)) | map(
    split("\t") as $parts |
    {
      timestamp: $parts[0],
      level: (if $parts[1] | contains("CRITICAL") then "CRITICAL" else "ERROR" end),
      message: $parts[1] | ltrimstr(" ")
    }
  )
' > "$OUTPUT_JSON"

# curlで外部APIへ通知する例 (コメントアウト)


# curl -X POST -H "Content-Type: application/json" -d @"$OUTPUT_JSON" -L -s https://api.monitoring.example.com/ingest

定期実行のためのsystemd設定例: /etc/systemd/system/log-parser.timer

[Unit]
Description=Run log processor every 5 minutes

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target

【検証と運用】

  1. 正常系確認: tail -f /var/log/app/error_report.json | jq . を実行し、スタックトレース全体が一つの message フィールドに格納されているか確認します。

  2. ログ確認: 実行ログは systemd 経由で取得します。 journalctl -u log-parser.service

【トラブルシューティングと落とし穴】

  • メモリ消費量: RS を使って巨大なスタックトレースを読み込む場合、awk プロセスがメモリを消費します。数MB単位の巨大な単一レコードが予想される場合は、getline をループで回し、バッファを制限する設計を検討してください。

  • RSの挙動差異: BSD系の awk と GNUの gawk では RS に正規表現が使えるかどうかの差異があります。本スクリプトは gawk を前提としています。

  • 権限不足: /var/log 配下の読み取りには通常 sudo が必要です。systemd ユニットファイル内で User=log-reader 等の専用ユーザーを定義し、対象ログへの ACL を設定することを推奨します。

【まとめ】 運用の冪等性を維持するための3つのポイント:

  1. ステート管理: 処理済みログのオフセット(inodeとバイト位置)を記録し、二重読み込みを防止する。

  2. スキーマの分離: awk での抽出処理と jq での整形処理を分離し、ログ形式変更への耐性を高める。

  3. 安全な一時ファイル: trap コマンドによる確実なクリーンアップを行い、ディスク逼迫を防止する。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました