awkを用いた複数行ログ(スタックトレース等)の構造化抽出と自動整形

Tech

{ “style_prompt_version”: “1.2”, “engine”: “gemini-2.0-flash”, “focus”: “SRE/DevOps/Automation”, “technical_level”: “Advanced”, “topic”: “Multi-line log parsing with awk (RS/getline)” } 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

awkを用いた複数行ログ(スタックトレース等)の構造化抽出と自動整形

【導入と前提】 Javaの例外スタックトレースやSQLスロークエリのような、複数行にわたる非構造化ログを、awkのRS(レコードセパレータ)とgetlineを活用して効率的に抽出し、JSON形式などの構造化データへ変換するパイプラインを構築します。

  • 前提条件:

    • OS: Linux (GNU awk 4.x以降推奨)

    • ツール: awk, jq, grep

    • 権限: ログ参照権限(必要に応じてsudo)

【処理フローと設計】

graph TD
A["Raw Multi-line Log"] --> B{"awk Processor"}
B -->|RS: Record Boundary| C["Extract Target Block"]
C -->|getline: Context Fetch| D["Attribute Parsing"]
D -->|Output: TSV/CSV| E["jq Filter"]
E --> F["Structured JSON/SIEM"]

この設計では、まずRS(レコードセパレータ)を再定義することで、1行単位ではなく「1エラーブロック」単位でデータを読み込みます。その後、getlineを用いてブロック内の特定行(タイムスタンプやエラーメッセージ)を順次走査し、SREが解析しやすい形式へ整形します。

【実装:堅牢な自動化スクリプト】 以下は、スタックトレースを含むログファイルを解析し、エラー内容をJSONとして抽出する堅牢なスクリプト例です。

#!/bin/bash


# ==============================================================================


# Script: log_summarizer.sh


# Description: RSとgetlineを活用した複数行ログの構造化抽出


# ==============================================================================

set -euo pipefail  # エラー発生時に停止、未定義変数参照禁止、パイプエラーの伝搬
trap 'echo "Error occurred at line $LINENO. Cleaning up..." >&2' ERR

LOG_FILE="${1:-/var/log/app/error.log}"
OUTPUT_JSON="error_report.json"

if [[ ! -f "$LOG_FILE" ]]; then
    echo "Error: File $LOG_FILE not found." >&2
    exit 1
fi

echo "Processing logs from $LOG_FILE..."

# awkによるパース処理


# 1. RS="---": レコード区切りをハイフン3つに設定(ログの区切り文字)


# 2. getline: ブロック内の特定行を読み飛ばす、または変数に格納


# 3. jq -R -s: 生テキスト入力を受け取り、構造化JSONへ変換

awk '
BEGIN {
    RS = "---";  # レコードセパレータをログの区切りに合わせる
    FS = "\n";   # フィールドセパレータを改行に設定
    OFS = "\t";  # 出力は一旦TSV形式
}
{

    # 空レコードのスキップ

    if ($0 ~ /^[[:space:]]*$/) next;

    timestamp = "";
    error_msg = "";

    # 第1フィールドからタイムスタンプを抽出(例: [2023-10-01 10:00:00])

    if ($1 ~ /^\[.*\]/) {
        timestamp = $1;
    }

    # 2行目以降を走査して"Exception"を含む行を探す

    for (i = 2; i <= NF; i++) {
        if ($i ~ /Exception:/) {
            error_msg = $i;

            # 次の行(スタックトレースの初動)を取得して結合

            current_line_idx = i;
            if (getline next_line > 0) {
                error_msg = error_msg " " next_line;
            }
            break;
        }
    }

    if (timestamp != "" && error_msg != "") {
        print timestamp, error_msg;
    }
}
' "$LOG_FILE" | \
jq -R -s '
    split("\n") | map(select(length > 0) | split("\t") | {
        timestamp: .[0],
        error_summary: .[1]
    })
' > "$OUTPUT_JSON"

echo "Structure log generated: $OUTPUT_JSON"

# --- 参考: systemd-timerでの定期実行用ユニット例 ---


# [Service]


# Type=oneshot


# ExecStart=/usr/local/bin/log_summarizer.sh /var/log/app/error.log


# User=log-analyzer

【検証と運用】

  1. 正常系の確認: jqコマンドを用いて、抽出されたJSONが期待通りか確認します。

    cat error_report.json | jq '.[0]'
    
    # 期待される出力: { "timestamp": "[...]", "error_summary": "Java.lang.NullPointerException at..." }
    
  2. 実行ログの確認: スクリプトをsystemdで運用する場合、以下のコマンドで実行結果をモニタリングします。

    journalctl -u log-summarizer.service --since "1 hour ago"
    

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

  • RS のメモリ消費: RSに空文字列("")を指定すると「連続した改行」を区切り(段落モード)として扱いますが、ログファイルが巨大で区切り文字がない場合、awkがファイル全体をメモリに読み込もうとしてOOM(Out of Memory)が発生します。必ず明示的な区切り文字を指定するか、事前にsplitなどでファイルを分割してください。

  • getline の戻り値: getlineはEOFで0、エラーで-1を返します。while (getline > 0)のようにループ内で使用する場合は、無限ループを避けるためのチェックが必須です。

  • 権限の最小化: ログファイルを読み取る際は、スクリプトをrootで動かすのではなく、admグループやログ参照専用ユーザーを作成して実行してください。

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

  1. 入力バリデーション: set -uにより未定義環境変数を排除し、処理開始前にファイル存在確認を徹底する。

  2. 一時ファイルの管理: 中間生成物が必要な場合はmktempを使用し、trapで確実に削除する(本例ではパイプ処理により回避)。

  3. スキーマの固定: jqによる最終整形を行うことで、後続の監視ツール(Datadog等)がパースエラーを起こさない安定したデータ構造を提供する。

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

コメント

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