mtailによるログからのPrometheusメトリクス生成と公開

Googleが開発したmtailを使い、ログファイルからPrometheus形式のメトリクスを生成・公開する方法を解説。mtailプログラムの書き方とDocker構成例も紹介します。

概要

mtail は、Googleが開発したオープンソースのログパースツールです。ログファイルから特定のパターンに一致する行を抽出し、それに基づいてメトリクスを生成し、/metrics エンドポイントにPrometheus形式で公開することができます。これにより、既存のログからビジネスメトリクスやシステムの状態をPrometheusで収集・監視することが可能になります。

mtailの使い方

mtailを利用するには、mtailプログラム(デーモン)を起動し、監視対象のログファイルと、メトリクスを定義するmtailプログラム(設定ファイル)を指定する必要があります。

mtailプログラム(設定ファイル)の例: sample.mtail

mtailプログラムは、Go言語に似た独自の構文で記述されます。

# メトリクスの定義と初期化
# 'errors_total' という名前のカウンタータイプのメトリクスを定義
counter errors_total

# ログファイル内のパターンマッチングルール
# 各行が「error」を含む場合に、errors_totalカウンターをインクリメント
/error/ {
  errors_total++
}

# 例: HTTPリクエストのステータスコードをカウント
# 正規表現でステータスコードをキャプチャし、ラベルとして使用
/^HTTP\/1\.[01] (\d{3})/ {
  http_requests_total[$1]++
}
counter http_requests_total by status_code
  • counter errors_total: Prometheusで使用するメトリクスを定義しています。counter はカウンタータイプのメトリクスで、値は単調増加します。
  • /error/ { errors_total++ }: ログの各行が正規表現 /error/ にマッチした場合に、errors_total カウンターを1増加させます。
  • counter http_requests_total by status_code: http_requests_total というカウンターメトリクスを定義し、status_code というラベルを持つことを示しています。
  • /^HTTP\/1\.[01] (\d{3})/ { http_requests_total[$1]++ }: HTTPアクセスログからステータスコード(例: 200, 404, 500)を抽出し、そのステータスコードをラベルとして http_requests_total カウンターをインクリメントします。

Dockerでmtailを起動する

mtailをDockerコンテナとして実行することで、環境構築の手間を省き、簡単にデプロイできます。

# ベースイメージとしてCentOS 7を使用
FROM centos:7

# wgetをインストールし、キャッシュをクリア
RUN yum install -y wget && yum clean all

# 作業ディレクトリを/tmpに設定
WORKDIR /tmp

# mtailのバイナリをダウンロードし、展開、実行権限を付与
# リリースバージョンは適宜最新のものに更新してください
RUN wget -O mtail.tar.gz https://github.com/google/mtail/releases/download/v3.0.0-rc52/mtail_3.0.0-rc52_linux_amd64.tar.gz && \
    tar xzvf mtail.tar.gz && \
    chmod +x mtail

# コンテナ起動時に実行されるコマンド
# -progs: mtailプログラム(設定ファイル)のパスを指定
# -logs: mtailが監視するログファイルのパスを指定
# 例: CMD ["/tmp/mtail", "-progs", "/etc/mtail/sample.mtail", "-logs", "/var/log/nginx/access.log"]
CMD ["/tmp/mtail", "-progs", "/path/to/sample.mtail", "-logs", "/path/to/logfile"]

# mtailがメトリクスを公開するデフォルトポート (Prometheusがスクレイピングするポート)
EXPOSE 3903

CMD 命令の引数

  • -progs /path/to/sample.mtail: mtailが使用する設定ファイル(mtailプログラム)のパスを指定します。コンテナ内に配置したmtailプログラムのパスを指定してください。
  • -logs /path/to/logfile: mtailが監視するログファイルのパスを指定します。mtailはこのログファイルを監視し、設定に定義されたパターンに一致する行を検出して対応するメトリクスを生成します。このログファイルは、Dockerのボリュームマウントなどを使ってコンテナ内に提供する必要があります。

このDockerイメージをビルドし、実行することで、mtailがログを監視し、Prometheusが収集可能なメトリクスを公開するようになります.

ベンチマーク: 正規表現ベースのログ解析はどれくらい速いか

mtailの核心は「ログの各行を正規表現でパースし、マッチした値をメトリクスに変換する」という処理です。この処理自体のスループットがどの程度出るのか、Pythonで模擬実装して実測してみます。

ベンチマーク条件

Apache combined log風の合成アクセスログを50万行(63.6MB)生成し、各行からHTTPステータスコードを抽出して件数を集計する処理を3通りの実装で計測しました。

  1. naive_regex: re.search(パターン文字列, line) を毎行呼び出す(コンパイル済みオブジェクトを明示的に保持しない書き方)
  2. compiled_regex: re.compile() で事前コンパイルしたパターンオブジェクトの .search(line) を使う
  3. str_split: 正規表現を使わず、ログのフォーマットが固定であることを前提に str.split('"') で該当フィールドを取り出す
import re
import time

STATUS_RE_STR = r'"\s(\d{3})\s\d+\s"'
STATUS_RE_COMPILED = re.compile(STATUS_RE_STR)

def approach_naive_regex(lines):
    counts = {}
    for line in lines:
        m = re.search(STATUS_RE_STR, line)
        if m:
            status = m.group(1)
            counts[status] = counts.get(status, 0) + 1
    return counts

def approach_compiled_regex(lines):
    counts = {}
    search = STATUS_RE_COMPILED.search
    for line in lines:
        m = search(line)
        if m:
            status = m.group(1)
            counts[status] = counts.get(status, 0) + 1
    return counts

def approach_str_split(lines):
    counts = {}
    for line in lines:
        # "IP - - [ts] "METHOD PATH PROTO" STATUS SIZE "REF" "UA" RESPMS
        parts = line.split('"')
        status = parts[2].split()[0]
        counts[status] = counts.get(status, 0) + 1
    return counts

各実装を同一ファイルに対して3回ずつ実行し、最速値(best-of-3)から lines/secMB/sec を算出しました(time.perf_counter() で計測)。

実測結果

実装実行時間 (best-of-3)スループット
naive_regex(毎回 re.search0.304 秒約164万 lines/sec(209 MB/sec)
compiled_regex(事前コンパイル)0.225 秒約222万 lines/sec(283 MB/sec、naive比 1.35倍
str_split(正規表現なし)0.242 秒約207万 lines/sec(263 MB/sec、naive比 1.26倍

正規表現ベースのログ解析スループット比較

3実装とも集計結果(ステータスコード別件数、レスポンスタイム合計)は完全に一致することを確認済みです。

考察

事前コンパイルした正規表現は、毎回 re.search を呼ぶ素朴な実装より 1.35倍速い という結果になりました。ただし、Pythonの re モジュールは内部的に直近使用したパターン文字列を最大512件までキャッシュしているため、「コンパイルなし」でも2回目以降の呼び出しではキャッシュされたコンパイル済みオブジェクトが再利用されます。それでも今回の計測では差が出ており、キャッシュ参照そのもののオーバーヘッド(辞書ルックアップ)が無視できないことがわかります。

一方で、正規表現を使わない str.split ベースの実装は、事前コンパイル済み正規表現より やや遅い(naive比1.26倍、compiled比では約0.93倍)という結果でした。今回のログフォーマットでは " によるsplitと複数回のトークン抽出が必要だったため、正規表現1回のマッチに対してPython関数呼び出しのオーバーヘッドが積み重なったと考えられます。「正規表現を避ければ必ず速くなる」というわけではなく、フォーマットの複雑さと抽出方法の組み合わせ次第で結果は変わる、というのが今回の実測から言える正直な結論です。

mtailの実装(Go言語)はこのような文字列処理をより低レベルに最適化していますが、「ログ1行ごとに正規表現マッチを行い、キャプチャした値をメトリクスに変換する」という処理コストの大枠は、この簡易ベンチマークが示す通りの数百万lines/sec程度のオーダーで捉えることができます。