awkによるマルチライン・ログ解析の自動化:スタックトレースの整形とJSON化

Tech

<meta_info: >

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

awkによるマルチライン・ログ解析の自動化:スタックトレースの整形とJSON化

【導入と前提】

本稿では、Javaのスタックトレースやシステムメッセージなど、複数行に及ぶ非構造化ログをawkRS(Record Separator)とgetlineを駆使して構造化し、後続のログ基盤(Elasticsearch等)へ連携しやすくする処理を自動化します。

  • OS/ツール: GNU/Linux, Bash 4.4+, gawk 4.1+, jq 1.6+

  • 対象: 標準出力またはテキスト形式のログファイル

【処理フローと設計】

graph TD
    A["非構造化ログファイル"] --> B{"awk RS設定"}
    B -->|ブロック分割| C["getlineによる内部走査"]
    C -->|フィルタリング| D["一時JSON生成"]
    D --> E["jqによるバリデーション"]
    E --> F["構造化ログ出力/転送"]
  1. RS(レコードセパレータ)を再定義し、タイムスタンプ等のパターンでログを「ブロック」単位で読み込みます。

  2. getlineを用いて、エラー詳細行など特定の条件に合致する行を動的に取得します。

  3. 最終的に 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 で確認。

  • メモリ消費: 巨大なスタックトレースが1レコードになるため、awkのメモリ消費を top または systemd-cgtop で監視。

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

  1. RS(Record Separator)の互換性:

    • POSIX awkでは RS は1文字のみですが、gawk (GNU awk) では正規表現が使えます。本スクリプトは gawk を前提としています。
  2. getlineの戻り値:

    • getline をループ内で使う場合、EOF(0) やエラー(-1) のハンドリングを怠ると無限ループに陥る危険があります。
  3. 大容量ログの処理:

    • 1つのレコードがあまりに巨大(数GB)な場合、awk のバッファ制限に抵触する可能性があります。その場合は、split コマンドで事前分割を検討してください。

【まとめ】

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

  1. トラップによる後始末: trap を使い、異常終了時も一時ファイルを残さない。

  2. 構造の検証: awk で生成した文字列を直接信じず、必ず jq を通してシンタックスチェックを行う。

  3. 入力のバリデーション: set -u による変数チェックと、対象ファイルの存在確認を徹底する。

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

コメント

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