本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
awkによる複数行スタックトレースの構造化抽出:ログ解析の自動化手法
【導入と前提】
システム障害時、JavaやPythonのスタックトレースのように1つのイベントが複数行に渡るログを、特定のIDやタイムスタンプに基づき単一のレコードとして抽出・整形するオペレーションを自動化します。
前提OS: GNU/Linux (Ubuntu 22.04+, RHEL 8+)
前提ツール:
gawk(GNU awk),jq(JSON整形用),bash4.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つのポイント:
境界定義の明確化:
RSには単一の文字だけでなく正規表現(gawk拡張)を活用し、ログの区切りを一意に特定する。パイプラインの安全確保:
set -o pipefailを用い、awkやjqの途中の失敗を見逃さない。構造化出力の徹底: 後続のツール(Elasticsearch, Fluentd等)が処理しやすいよう、
awk内で無理に整形せずjqを介してクリーンなJSONを出力する。
