awkによる複数行スタックトレースの構造化抽出:ログ解析の自動化手法

Tech

{ “category”: “DevOps/SRE”, “topic”: “awk_multi_line_log_processing”, “tools”: [“awk”, “jq”, “bash”], “tags”: [“RS”, “getline”, “log_analysis”, “automation”] }

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

awkによる複数行スタックトレースの構造化抽出:ログ解析の自動化手法

【導入と前提】

システム障害時、JavaやPythonのスタックトレースのように1つのイベントが複数行に渡るログを、特定のIDやタイムスタンプに基づき単一のレコードとして抽出・整形するオペレーションを自動化します。

  • 前提OS: GNU/Linux (Ubuntu 22.04+, RHEL 8+)

  • 前提ツール: gawk (GNU awk), jq (JSON整形用), bash 4.x以降

【処理フローと設計】

graph TD
A["非構造化マルチラインログ"] --> B{"awk: RS/ORSの再定義"}
B --> C["レコード境界の識別"]
C --> D{"getlineによるコンテキスト取得"}
D --> E["構造化データ生成 JSON/CSV"]
E --> F["jqによるフィルタリング・出力"]

RS(入力レコードセパレータ)を空文字""に設定することで「空行区切り」のパラグラフモードとして処理し、さらに詳細な制御が必要な場合にgetlineを用いて動的に次行を読み込みます。

【実装:堅牢な自動化スクリプト】

以下のスクリプトは、特定のキーワード(例:ERROR)を含む複数行のスタックトレースを抽出し、JSON形式へ変換して出力する例です。

#!/bin/bash

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


# ログ抽出・整形スクリプト


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

set -euo pipefail # エラー発生時に即停止、未定義変数参照禁止、パイプエラーの伝播
trap 'echo "Error at line $LINENO"' ERR

# 入力ファイルの存在確認

readonly LOG_FILE="${1:-/var/log/app/error.log}"
if [[ ! -f "$LOG_FILE" ]]; then
    echo "Error: File $LOG_FILE not found." >&2
    exit 1
fi

# 複数行ログの抽出とJSON化


# RS="": 空行をレコードの区切りとして扱う(パラグラフモード)


# ORS="": 出力レコードセパレータを制御


# getline: 特定のパターンに一致した場合、さらに詳細な情報を次行から取得

awk '
BEGIN {
    RS = "";      # 空行をレコード区切りとする
    FS = "\n";    # レコード内では改行をフィールド区切りとする
}
/ERROR/ {

    # レコードの先頭行(1番目のフィールド)をタイトルとして取得

    title = $1;

    # メッセージの蓄積

    msg = "";
    for(i=2; i<=NF; i++) {
        msg = msg $i " ";
    }

    # 外部変数や次行の読み込みが必要な場合のgetline例


    # "date" | getline current_time; # 外部コマンド結果の取得

    # JSONライクな形式で出力(後段のjqで整形)

    printf "{\"timestamp\":\"%s\", \"level\":\"ERROR\", \"summary\":\"%s\", \"detail\":\"%s\"}\n", 
            strftime("%Y-%m-%dT%H:%M:%S"), title, msg;
}' "$LOG_FILE" | jq -s '.' # -s: 入力を一つの配列にまとめる

Systemd Timerによる定期実行例

抽出結果を定期的に監視ツールへ転送するための設定ファイル例です。

# /etc/systemd/system/log-watcher.service

[Unit]
Description=Extract Multi-line Error Logs

[Service]
Type=oneshot
ExecStart=/usr/local/bin/log-extract.sh /var/log/app/sys.log
StandardOutput=append:/var/log/app/extracted_errors.json
User=loguser

# /etc/systemd/system/log-watcher.timer

[Timer]
OnCalendar=*:0/5
Unit=log-watcher.service

[Install]
WantedBy=timers.target

【検証と運用】

1. 正常系の確認

作成したスクリプトが正しくJSONを吐き出すか確認します。

./log-extract.sh test.log | jq -r '.[0].summary'

2. ログの確認(journalctl)

Systemd経由で実行している場合、標準エラー出力はjournalctlで確認可能です。

# 実行ログの確認

journalctl -u log-watcher.service -n 50 --no-pager

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

  • 権限問題: /var/log 以下のファイルを読む際、スクリプト実行ユーザーに読み取り権限がないと失敗します。setfacl で特定のユーザーに読み取り権限を付与するか、sudo 実行を検討してください。

  • メモリ消費: RS="" で巨大なログファイルを処理する場合、空行がないとファイル全体が1レコードとしてメモリに読み込まれるリスクがあります。巨大なファイルには RS="^20[0-9]{2}" (日付等のパターン)で区切るなどの工夫が必要です。

  • getlineの戻り値: getline をループ内で使用する際は、戻り値(1:成功, 0:EOF, -1:エラー)を必ずチェックし、無限ループを防止してください。

    • 例: while ((getline var < "file") > 0) { ... }

【まとめ】

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

  1. 境界定義の明確化: RS には単一の文字だけでなく正規表現(gawk拡張)を活用し、ログの区切りを一意に特定する。

  2. パイプラインの安全確保: set -o pipefail を用い、awkjq の途中の失敗を見逃さない。

  3. 構造化出力の徹底: 後続のツール(Elasticsearch, Fluentd等)が処理しやすいよう、awk 内で無理に整形せず jq を介してクリーンなJSONを出力する。

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

コメント

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