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
【検証と運用】
正常系の確認:
jqコマンドを用いて、抽出されたJSONが期待通りか確認します。cat error_report.json | jq '.[0]' # 期待される出力: { "timestamp": "[...]", "error_summary": "Java.lang.NullPointerException at..." }実行ログの確認: スクリプトを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グループやログ参照専用ユーザーを作成して実行してください。
【まとめ:運用の冪等性を維持するポイント】
入力バリデーション:
set -uにより未定義環境変数を排除し、処理開始前にファイル存在確認を徹底する。一時ファイルの管理: 中間生成物が必要な場合は
mktempを使用し、trapで確実に削除する(本例ではパイプ処理により回避)。スキーマの固定:
jqによる最終整形を行うことで、後続の監視ツール(Datadog等)がパースエラーを起こさない安定したデータ構造を提供する。


コメント