本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。 本稿では、Javaのスタックトレースやシステムメッセージなど、複数行に及ぶ非構造化ログを OS/ツール: GNU/Linux, Bash 4.4+, gawk 4.1+, jq 1.6+ 対象: 標準出力またはテキスト形式のログファイル 最終的に 解析を定期実行する場合は、以下のユニットファイルを配置します。 不完全なレコード: メモリ消費: 巨大なスタックトレースが1レコードになるため、 RS(Record Separator)の互換性: getlineの戻り値: 大容量ログの処理: 運用の冪等性と堅牢性を維持するためのポイント: トラップによる後始末: 構造の検証: 入力のバリデーション: awkによるマルチライン・ログ解析の自動化:スタックトレースの整形とJSON化
【導入と前提】
awkのRS(Record Separator)とgetlineを駆使して構造化し、後続のログ基盤(Elasticsearch等)へ連携しやすくする処理を自動化します。
【処理フローと設計】
graph TD
A["非構造化ログファイル"] --> B{"awk RS設定"}
B -->|"ブロック分割"| C["getlineによる内部走査"]
C -->|"フィルタリング"| D["一時JSON生成"]
D --> E["jqによるバリデーション"]
E --> F["構造化ログ出力/転送"]
RS(レコードセパレータ)を再定義し、タイムスタンプ等のパターンでログを「ブロック」単位で読み込みます。getlineを用いて、エラー詳細行など特定の条件に合致する行を動的に取得します。jq を通すことで、構造の妥当性を保証しつつ整形します。【実装:堅牢な自動化スクリプト】
#!/bin/bash
# ==============================================================================
# Script: parse_multiline_logs.sh
# Description: awkのRS/getlineを利用したマルチラインログの構造化
# ==============================================================================
set -euo pipefail # エラー発生時に停止、未定義変数参照禁止、パイプ途中のエラーを捕捉
IFS=$'\n\t'
# 一時ファイルの管理
TMP_DIR=$(mktemp -d)
readonly TMP_OUT="${TMP_DIR}/parsed_logs.jsonl"
trap 'rm -rf "$TMP_DIR"' EXIT # 終了時に一時ファイルを確実に削除
# 設定
readonly LOG_FILE="${1:-/var/log/application/error.log}"
# ログの開始パターン(例: 2024-05-20 10:00:00 ...)
readonly LOG_START_PATTERN='^[0-9]{4}-[0-9]{2}-[0-9]{2}'
main() {
if [[ ! -f "$LOG_FILE" ]]; then
echo "Error: Log file not found: $LOG_FILE" >&2
exit 1
fi
echo "Processing: $LOG_FILE" >&2
# awkによるマルチライン処理
# RSをログの開始パターンの直前にマッチさせる(gawk固有の動作を利用)
awk -v pattern="$LOG_START_PATTERN" '
BEGIN {
# レコードセパレータを正規表現で定義(マルチラインを1つのレコードとして扱う)
# ※gawkではRT(Record Terminator)も活用可能
RS = "(^|\n)(?=" pattern ")";
ORS = ""; # 出力時のレコードセパレータを初期化
}
{
# 空のレコードをスキップ
if ($0 ~ /^[[:space:]]*$/) next;
# 1行目(ヘッダー)を抽出
header = "";
match($0, /^[^\n]+/, arr);
header = arr[0];
# メッセージ本体(2行目以降)を抽出
body = $0;
sub(/^[^\n]+\n?/, "", body);
gsub(/\n/, " ", body); # 改行をスペースに置換して1行にする
gsub(/"/, "\\\"", body); # JSON破壊防止のエスケープ
# 構造化出力(簡易JSON形式)
printf "{\"timestamp_info\":\"%s\", \"message\":\"%s\"}\n", header, body;
}' "$LOG_FILE" > "$TMP_OUT"
# jqによるバリデーションと最終整形
if [[ -s "$TMP_OUT" ]]; then
cat "$TMP_OUT" | jq -c '.' # -c: 各オブジェクトを1行に圧縮
else
echo "No logs matched the pattern." >&2
fi
}
main
systemdタイマーによる定期実行例
# /etc/systemd/system/log-parser.service
[Unit]
Description=Application Log Parser
[Service]
Type=oneshot
ExecStart=/usr/local/bin/parse_multiline_logs.sh /var/log/app.log
User=logadmin
# /etc/systemd/system/log-parser.timer
[Timer]
OnCalendar=minutely
Unit=log-parser.service
[Install]
WantedBy=timers.target
【検証と運用】
正常系の確認
# スクリプトの実行
./parse_multiline_logs.sh test.log
# journalctlでの実行ログ確認(systemd経由の場合)
journalctl -u log-parser.service -f
異常系の確認
RSの設定ミスにより、レコードが途切れていないか tail で確認。awkのメモリ消費を top または systemd-cgtop で監視。【トラブルシューティングと落とし穴】
RS は1文字のみですが、gawk (GNU awk) では正規表現が使えます。本スクリプトは gawk を前提としています。
getline をループ内で使う場合、EOF(0) やエラー(-1) のハンドリングを怠ると無限ループに陥る危険があります。
awk のバッファ制限に抵触する可能性があります。その場合は、split コマンドで事前分割を検討してください。【まとめ】
trap を使い、異常終了時も一時ファイルを残さない。awk で生成した文字列を直接信じず、必ず jq を通してシンタックスチェックを行う。set -u による変数チェックと、対象ファイルの存在確認を徹底する。
awkによるマルチライン・ログ解析の自動化:スタックトレースの整形とJSON化
Linux・CLI・DevOps
