はじめに
この記事の冒頭で書いたように、私のグループではセクション内の運用工数削減を目的としたツールを開発しています。現状使える状態のツールは 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 などに組み込むことで柔軟な変換が可能になるはずです。