はじめに

この記事の冒頭で書いたように、私のグループではセクション内の運用工数削減を目的としたツールを開発しています。現状使える状態のツールは 2 つあります。

ツール リポジトリ 用途 ライセンス
tlc3 GitHub Web サイトにアクセスし、TLS 証明書の情報 (期限日など) を取得する MIT
alpen GitHub ログファイルを解析し、JSON などの構造化フォーマットに変換する MIT

今回は alpen について紹介します。ちなみにこの名前は Acess log parser/encoder の略からきています。

なおこのツールについては以下記事でも紹介しています。重複する部分もありますが、機能を拡張したので再度取り上げています。

拡張された機能としては以下のとおりです。AWS に限定されず、通常の Apache CLF などもサポートしました。

  • Apache common/combined log format (with virtual host) のサポート
  • LTSV format のサポート
  • ヘッダー付き TSV を出力できるようにした
  • 行番号の有無を指定できるようにした

概要

様々なアクセスログを解析して構造化フォーマットに変換するツールです。手元でログを解析してスプシに貼り付けてゴニョゴニョしなければならない、などの状況で使えます。以下のような特徴があります。

  • GZIP を直接読める
  • ZIP の中身を一括で直接読める
  • ZIP の中身を読む時は対象を Glob パターンでフィルタできる
  • 指定した行番号の読み込みをスキップできる (ヘッダーを読み飛ばしたりできる)
  • 行番号の出力有無を制御できる
  • メタデータとして集計情報を出せる
  • LTSV をサポートしており、ラベルをそのままフィールド名として他の構造化フォーマットに変換できる
  • ランタイムを内包したシングルバイナリなので、実行ファイルを置くだけで動く

メタデータには以下情報が含まれます。

  • 読み込んだ行数
  • マッチした行数
  • マッチしなかった行数
  • スキップした行数
  • マッチしなかった行の番号と内容を保持した JSON 配列

内部的に複数の正規表現に順番にマッチさせることで以下の仕様を実現しています。

  • S3 や CLB の場合は歴史的経緯でログフォーマットが拡張されているが、最近のフォーマットから順に照合する
  • Apache common/combine log format (with virtual host) の場合 common でも combine でもマッチする
  • Apache common/combine log format (with virtual host) の場合デリミタがスペースでもタブでもマッチする

サポートしている入出力については以下の通りです。

入力

サブコマンド 入力フォーマット
clf Apache common/combined log format
clfv Apache common/combined log format with virtual host
s3 Amazon S3 access log format
cf Amazon CloudFront access log format
alb AWS Application Load Balancer access log format
nlb AWS Network Load Balancer access log format
clb AWS Classic Load Balancer access log format
ltsv LTSV format

出力

--output,-o オプションに渡す文字列 出力フォーマット
json NDJSON (newline-delimited JSON) 形式、デフォルト
pretty-json インデント付きの NDJSON
text key=value ペア
ltsv Labeled Tab-separated Values
tsv --header,-H オプションでヘッダー有無を制御可能

インストール

Mac であれば homebrew でインストールできます。

brew install nekrassov01/tap/alpen

Windows, Linux の場合はリリースページからバイナリをダウンロードし、パスの通ったディレクトリに放り込んでください。

ヘルプ

NAME:
   alpen - Access log parser/encoder CLI

USAGE:
   alpen [global options] command [command options] [arguments...]

VERSION:
   0.0.18

DESCRIPTION:
   A cli application for parsing various access logs

COMMANDS:
   clf   Parses apache common/combined log format
   clfv  Parses apache common/combined log format with vhost
   s3    Parses S3 access logs
   cf    Parses CloudFront access logs
   alb   Parses ALB access logs
   nlb   Parses NLB access logs
   clb   Parses CLB access logs
   ltsv  Parses LTSV format logs

GLOBAL OPTIONS:
   --completion value, -c value  select a shell to display completion scripts: bash|zsh|pwsh
   --help, -h                    show help
   --version, -v                 print the version

サブコマンドはすべて同じオプションを持っており、以下のような形です。

NAME:
   alpen clf - Parses apache common/combined log format

USAGE:
   alpen clf

DESCRIPTION:
   Parses apache common/combined log format and converts them to structured formats

OPTIONS:
   --input value, -i value                            input from string
   --file-path value, -f value                        input from file path
   --gzip-path value, -g value                        input from gzip file path
   --zip-path value, -z value                         input from zip file path
   --output value, -o value                           select output format: json|pretty-json|text|ltsv|tsv (default: "json")
   --skip value, -s value [ --skip value, -s value ]  skip records by index
   --metadata, -m                                     enable metadata output (default: false)
   --line-number, -l                                  set line number at the beginning of the line (default: false)
   --header, -H                                       set header: avairable for tsv output (default: false)
   --glob-pattern value, -G value                     filter glob pattern: available for parsing zip only (default: "*")
   --help, -h                                         show help

使用例

Apache common/combined log format を題材として使い方を説明します。上 3 行が combined で残りが common です。

$ cat clf.log
192.168.1.1 - frank [10/Dec/2023:13:55:36 +0200] "GET /apache_pb.gif HTTP/1.1" 200 2326 "http://www.example.com/start.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
203.0.113.45 - - [10/Dec/2023:13:57:18 +0200] "POST /login.php HTTP/1.1" 302 4523 "http://www.example.com/login" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
198.51.100.34 - - [10/Dec/2023:13:59:01 +0200] "GET /products HTTP/1.1" 404 1749 "http://www.example.com/store" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"
192.0.2.62 - jill [10/Dec/2023:14:02:23 +0200] "GET /images/logo.png HTTP/1.1" 200 2048
172.16.254.1 - - [10/Dec/2023:14:04:56 +0200] "GET /news/article.html HTTP/1.1" 200 980

ログの渡し方

文字列で渡す場合、--input,-i オプションを使います。大抵の場合はパスを指定して渡すと思うので、手元で使う場合はあまり出番はないかもしれません。シェルスクリプトに組み込む場合などに有用かもしれません。

$ alpen clf -i '172.16.254.1 - - [10/Dec/2023:14:04:56 +0200] "GET /news/article.html HTTP/1.1" 200 980'
{"remote_host":"172.16.254.1","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:14:04:56 +0200]","method":"GET","request_uri":"/news/article.html","protocol":"HTTP/1.1","status":"200","size":"980"}

ファイルで渡す場合、--file-path,-f オプションを使います。

$ alpen clf -f clf.log
{"remote_host":"192.168.1.1","remote_logname":"-","remote_user":"frank","datetime":"[10/Dec/2023:13:55:36 +0200]","method":"GET","request_uri":"/apache_pb.gif","protocol":"HTTP/1.1","status":"200","size":"2326","referer":"http://www.example.com/start.html","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"}
{"remote_host":"203.0.113.45","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:57:18 +0200]","method":"POST","request_uri":"/login.php","protocol":"HTTP/1.1","status":"302","size":"4523","referer":"http://www.example.com/login","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
{"remote_host":"198.51.100.34","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:59:01 +0200]","method":"GET","request_uri":"/products","protocol":"HTTP/1.1","status":"404","size":"1749","referer":"http://www.example.com/store","user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"}
{"remote_host":"192.0.2.62","remote_logname":"-","remote_user":"jill","datetime":"[10/Dec/2023:14:02:23 +0200]","method":"GET","request_uri":"/images/logo.png","protocol":"HTTP/1.1","status":"200","size":"2048"}
{"remote_host":"172.16.254.1","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:14:04:56 +0200]","method":"GET","request_uri":"/news/article.html","protocol":"HTTP/1.1","status":"200","size":"980"}

GZIP ファイルをそのまま渡す場合は --gzip-path,-g オプションを使います。

$ alpen clf -g clf.log.gz
# 同じ出力

ZIP アーカイブを --zip-path,-z オプションでそのまま渡すと、中身のエントリをすべて処理します。--glob-pattern,-G オプションを指定することで対象のエントリをフィルタできます。

中身がこういう ZIP アーカイブがあるとして、ignore.txt が不要だとします。

$ unzip -Z clf.log.zip
Archive:  clf.log.zip
Zip file size: 670 bytes, number of entries: 2
-rw-r--r--  3.0 unx      676 tx defN 23-Dec-05 16:54 clf.log
-rw-r--r--  3.0 unx        0 bx stor 23-Dec-05 17:47 ignore.txt
2 files, 676 bytes uncompressed, 358 bytes compressed:  47.0%

以下のようにオプションを設定します。

$ alpen clf -z clf.log.zip -G "*.log"
{"remote_host":"192.168.1.1","remote_logname":"-","remote_user":"frank","datetime":"[10/Dec/2023:13:55:36 +0200]","method":"GET","request_uri":"/apache_pb.gif","protocol":"HTTP/1.1","status":"200","size":"2326","referer":"http://www.example.com/start.html","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"}
{"remote_host":"203.0.113.45","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:57:18 +0200]","method":"POST","request_uri":"/login.php","protocol":"HTTP/1.1","status":"302","size":"4523","referer":"http://www.example.com/login","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
{"remote_host":"198.51.100.34","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:59:01 +0200]","method":"GET","request_uri":"/products","protocol":"HTTP/1.1","status":"404","size":"1749","referer":"http://www.example.com/store","user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"}
{"remote_host":"192.0.2.62","remote_logname":"-","remote_user":"jill","datetime":"[10/Dec/2023:14:02:23 +0200]","method":"GET","request_uri":"/images/logo.png","protocol":"HTTP/1.1","status":"200","size":"2048"}
{"remote_host":"172.16.254.1","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:14:04:56 +0200]","method":"GET","request_uri":"/news/article.html","protocol":"HTTP/1.1","status":"200","size":"980"}

オプション

行番号を有効にしたい場合は --line-number,-l オプションを使います (フィールド名が index なのは直すかもしれない)

$ alpen clf -f clf.log -l
{"index":"1","remote_host":"192.168.1.1","remote_logname":"-","remote_user":"frank","datetime":"[10/Dec/2023:13:55:36 +0200]","method":"GET","request_uri":"/apache_pb.gif","protocol":"HTTP/1.1","status":"200","size":"2326","referer":"http://www.example.com/start.html","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"}
{"index":"2","remote_host":"203.0.113.45","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:57:18 +0200]","method":"POST","request_uri":"/login.php","protocol":"HTTP/1.1","status":"302","size":"4523","referer":"http://www.example.com/login","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
{"index":"3","remote_host":"198.51.100.34","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:13:59:01 +0200]","method":"GET","request_uri":"/products","protocol":"HTTP/1.1","status":"404","size":"1749","referer":"http://www.example.com/store","user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"}
{"index":"4","remote_host":"192.0.2.62","remote_logname":"-","remote_user":"jill","datetime":"[10/Dec/2023:14:02:23 +0200]","method":"GET","request_uri":"/images/logo.png","protocol":"HTTP/1.1","status":"200","size":"2048"}
{"index":"5","remote_host":"172.16.254.1","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:14:04:56 +0200]","method":"GET","request_uri":"/news/article.html","protocol":"HTTP/1.1","status":"200","size":"980"}

メタデータも出力したい場合は --metadata,-m オプションを使います。末尾に以下のような集計情報が追加されます。

$ alpen clf -f clf.log -m
{"total":5,"matched":5,"unmatched":0,"skipped":0,"source":"clf.log","errors":null}

特定の行をスキップしたい場合は --skip,-s に行番号を指定してください。ヘッダーの除外等で有効です。

$ alpen clf -f clf.log -l -m -s 2,3
{"index":"1","remote_host":"192.168.1.1","remote_logname":"-","remote_user":"frank","datetime":"[10/Dec/2023:13:55:36 +0200]","method":"GET","request_uri":"/apache_pb.gif","protocol":"HTTP/1.1","status":"200","size":"2326","referer":"http://www.example.com/start.html","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"}
{"index":"4","remote_host":"192.0.2.62","remote_logname":"-","remote_user":"jill","datetime":"[10/Dec/2023:14:02:23 +0200]","method":"GET","request_uri":"/images/logo.png","protocol":"HTTP/1.1","status":"200","size":"2048"}
{"index":"5","remote_host":"172.16.254.1","remote_logname":"-","remote_user":"-","datetime":"[10/Dec/2023:14:04:56 +0200]","method":"GET","request_uri":"/news/article.html","protocol":"HTTP/1.1","status":"200","size":"980"}
{"total":5,"matched":3,"unmatched":0,"skipped":2,"source":"clf.log","errors":null}

出力フォーマット

デフォルトは NDJSON 形式で、--output json または -o json とした場合と同じです。見やすくインデントしたい場合は pretty-json を指定します。

$ alpen clf -f clf.log -l -m -o pretty-json
{
  "index": "1",
  "remote_host": "192.168.1.1",
  "remote_logname": "-",
  "remote_user": "frank",
  "datetime": "[10/Dec/2023:13:55:36 +0200]",
  "method": "GET",
  "request_uri": "/apache_pb.gif",
  "protocol": "HTTP/1.1",
  "status": "200",
  "size": "2326",
  "referer": "http://www.example.com/start.html",
  "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
}
{
  "index": "2",
  "remote_host": "203.0.113.45",
  "remote_logname": "-",
  "remote_user": "-",
  "datetime": "[10/Dec/2023:13:57:18 +0200]",
  "method": "POST",
  "request_uri": "/login.php",
  "protocol": "HTTP/1.1",
  "status": "302",
  "size": "4523",
  "referer": "http://www.example.com/login",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
{
  "index": "3",
  "remote_host": "198.51.100.34",
  "remote_logname": "-",
  "remote_user": "-",
  "datetime": "[10/Dec/2023:13:59:01 +0200]",
  "method": "GET",
  "request_uri": "/products",
  "protocol": "HTTP/1.1",
  "status": "404",
  "size": "1749",
  "referer": "http://www.example.com/store",
  "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"
}
{
  "index": "4",
  "remote_host": "192.0.2.62",
  "remote_logname": "-",
  "remote_user": "jill",
  "datetime": "[10/Dec/2023:14:02:23 +0200]",
  "method": "GET",
  "request_uri": "/images/logo.png",
  "protocol": "HTTP/1.1",
  "status": "200",
  "size": "2048"
}
{
  "index": "5",
  "remote_host": "172.16.254.1",
  "remote_logname": "-",
  "remote_user": "-",
  "datetime": "[10/Dec/2023:14:04:56 +0200]",
  "method": "GET",
  "request_uri": "/news/article.html",
  "protocol": "HTTP/1.1",
  "status": "200",
  "size": "980"
}
{
  "total": 5,
  "matched": 5,
  "unmatched": 0,
  "skipped": 0,
  "source": "clf.log",
  "errors": null
}

TSV で出力すると、そのままスプシに貼り付けられるので便利です。例えば Mac の場合は alpen clf -f clf.log -o tsv | pbcopy のように、パイプでクリップボードに流し込んでください。TSV の場合のみヘッダーを有効化するオプション --header,-H が使えます。このヘッダーは 1 行目の解析結果に基づいています。

$ alpen clf -f clf.log -l -m -o tsv -H
index   remote_host     remote_logname  remote_user     datetime        method  request_uri     protocol        status  size    referer user_agent
1       192.168.1.1     -       frank   [10/Dec/2023:13:55:36 +0200]    GET     /apache_pb.gif  HTTP/1.1        200     2326    http://www.example.com/start.html       Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
2       203.0.113.45    -       -       [10/Dec/2023:13:57:18 +0200]    POST    /login.php      HTTP/1.1        302     4523    http://www.example.com/login    Mozilla/5.0 (Windows NT 10.0; Win64; x64)
3       198.51.100.34   -       flank zappa     [10/Dec/2023:13:59:01 +0200]    GET     /products       HTTP/1.1        404     1749    http://www.example.com/store    Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)
4       192.0.2.62      -       jill    [10/Dec/2023:14:02:23 +0200]    GET     /images/logo.png        HTTP/1.1        200     2048
5       172.16.254.1    -       -       [10/Dec/2023:14:04:56 +0200]    GET     /news/article.html      HTTP/1.1        200     980
5       5       0       0       clf.log null

LTSV の入力を受け付けますが、出力も LTSV にすることができます。ltsv を指定します。

$ alpen clf -f clf.log -l -m -o ltsv
index:1 remote_host:192.168.1.1 remote_logname:-    remote_user:frank   datetime:[10/Dec/2023:13:55:36 +0200]   method:GET  request_uri:/apache_pb.gif  protocol:HTTP/1.1   status:200  size:2326   referer:http://www.example.com/start.html   user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
index:2 remote_host:203.0.113.45    remote_logname:-    remote_user:-   datetime:[10/Dec/2023:13:57:18 +0200]   method:POST request_uri:/login.php  protocol:HTTP/1.1   status:302  size:4523   referer:http://www.example.com/login    user_agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64)
index:3 remote_host:198.51.100.34   remote_logname:-    remote_user:-   datetime:[10/Dec/2023:13:59:01 +0200]   method:GET  request_uri:/products   protocol:HTTP/1.1   status:404  size:1749   referer:http://www.example.com/store    user_agent:Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)
index:4 remote_host:192.0.2.62  remote_logname:-    remote_user:jill    datetime:[10/Dec/2023:14:02:23 +0200]   method:GET  request_uri:/images/logo.png    protocol:HTTP/1.1   status:200  size:2048
index:5 remote_host:172.16.254.1    remote_logname:-    remote_user:-   datetime:[10/Dec/2023:14:04:56 +0200]   method:GET  request_uri:/news/article.html  protocol:HTTP/1.1   status:200  size:980
total:5 matched:5   unmatched:0 skipped:0   source:clf.log  errors:null

需要があるかどうか不明ですが、key=value ペアで出力したい場合は text を指定します。NDJSON とは別のオーソドックスなログ形式という感じですね。

$ alpen clf -f clf.log -l -m -o text
index="1" remote_host="192.168.1.1" remote_logname="-" remote_user="frank" datetime="[10/Dec/2023:13:55:36 +0200]" method="GET" request_uri="/apache_pb.gif" protocol="HTTP/1.1" status="200" size="2326" referer="http://www.example.com/start.html" user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
index="2" remote_host="203.0.113.45" remote_logname="-" remote_user="-" datetime="[10/Dec/2023:13:57:18 +0200]" method="POST" request_uri="/login.php" protocol="HTTP/1.1" status="302" size="4523" referer="http://www.example.com/login" user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
index="3" remote_host="198.51.100.34" remote_logname="-" remote_user="-" datetime="[10/Dec/2023:13:59:01 +0200]" method="GET" request_uri="/products" protocol="HTTP/1.1" status="404" size="1749" referer="http://www.example.com/store" user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X)"
index="4" remote_host="192.0.2.62" remote_logname="-" remote_user="jill" datetime="[10/Dec/2023:14:02:23 +0200]" method="GET" request_uri="/images/logo.png" protocol="HTTP/1.1" status="200" size="2048"
index="5" remote_host="172.16.254.1" remote_logname="-" remote_user="-" datetime="[10/Dec/2023:14:04:56 +0200]" method="GET" request_uri="/news/article.html" protocol="HTTP/1.1" status="200" size="980"
total=5 matched=5 unmatched=0 skipped=0 source="clf.log" errors=null

こんな感じで、Apache CLF, S3, CloudFront, ALB, NLB, CLB のアクセスログを同様に変換できます。なお Apache CLF with virtual host の場合は先頭に virtual host がある必要があります。

シェル補完

こちらの記事とほぼ同じ内容です。

注意点

  • それなりに正確だと思っていますが、100% マッチさせることができるわけではありません。
  • request は通常 "GET /index.html HTTP/1.1" のような形式ですが、このツールでは method: GET,request_uri: /index.html,protocol: HTTP/1.1 のように分解しています。
  • ただし、LTSV を入力として指定した場合はラベルをそのままキーとして使うため、分解しません (この挙動ややこしいので分解するのやめるかもしれない)

今後の展望

より汎用化する方向性を考えています。

  • 設定ファイルで独自のログ形式を読めるようにする
  • 設定ファイルで独自の出力テンプレートを指定できるようにする (text/template でできる範囲)

おわりに

正直、利用シーンは限定されると思います。ただ、本ツールは Go 言語で実装しており、コマンドラインパーサー以外のコア機能はモジュールとして再利用可能になっています。これを直接 Lambda などに組み込むことで柔軟な変換が可能になるはずです。