@kyanny's blog

My thoughts, my life. Views/opinions are my own.

logfmt 形式のログを見やすく整形する by grep -o

logfmt いじりまだやってるのかよという感じだが、まだやっている。

grep -o はマッチした部分文字列をそれぞれ一行ずつ出力することを利用して、

❯ cat logfmt.log
at=info method=GET path=/ host=mutelight.org fwd="124.133.52.161" dyno=web.2 connect=4ms service=8ms status=200 bytes=1653
level=info tag=stopping_fetchers id=ConsumerFetcherManager-1382721708341 module=kafka.consumer.ConsumerFetcherManager
level=info msg="Stopping all fetchers" tag=stopping_fetchers id=ConsumerFetcherManager-1382721708341 module=kafka.consumer.ConsumerFetcherManager
❯ egrep -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log
at=info
method=GET
path=/
host=mutelight.org
fwd="124.133.52.161"
dyno=web.2
connect=4ms
service=8ms
status=200
bytes=1653
level=info
tag=stopping_fetchers
id=ConsumerFetcherManager-1382721708341
module=kafka.consumer.ConsumerFetcherManager
level=info
msg="Stopping all fetchers"
tag=stopping_fetchers
id=ConsumerFetcherManager-1382721708341
module=kafka.consumer.ConsumerFetcherManager

これが最低限だが、どこまでが同一行だったか分かりづらい。egrep -n で行番号をつけると多少マシになる。

❯ egrep -n -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log
1:at=info
1:method=GET
1:path=/
1:host=mutelight.org
1:fwd="124.133.52.161"
1:dyno=web.2
1:connect=4ms
1:service=8ms
1:status=200
1:bytes=1653
2:level=info
2:tag=stopping_fetchers
2:id=ConsumerFetcherManager-1382721708341
2:module=kafka.consumer.ConsumerFetcherManager
3:level=info
3:msg="Stopping all fetchers"
3:tag=stopping_fetchers
3:id=ConsumerFetcherManager-1382721708341
3:module=kafka.consumer.ConsumerFetcherManager

空行で区切られているとグループ分けを視覚的に把握しやすそう。これは awk で各行の行頭にある行番号を直前行と比較して違ってたら空行を挟む、というのでごちゃごちゃやることもできるが、

❯ egrep -n -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log | awk '{cur=$0; if(substr(prev,1,1)!=substr(cur,1,1)){print ""}; print; prev=cur }'

1:at=info
1:method=GET
1:path=/
1:host=mutelight.org
1:fwd="124.133.52.161"
1:dyno=web.2
1:connect=4ms
1:service=8ms
1:status=200
1:bytes=1653

2:level=info
2:tag=stopping_fetchers
2:id=ConsumerFetcherManager-1382721708341
2:module=kafka.consumer.ConsumerFetcherManager

3:level=info
3:msg="Stopping all fetchers"
3:tag=stopping_fetchers
3:id=ConsumerFetcherManager-1382721708341
3:module=kafka.consumer.ConsumerFetcherManager

(グループ分け後は行番号は不要なので消したい、という場合は egrep -n -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log | awk '{cur=$0; if(substr(prev,1,1)!=substr(cur,1,1)){print ""}; print substr($0, 3); prev=cur }' と substr したものを print するか、パイプで | cut -c 3- と繋ぐ)

linux - split a file by a line prefix - Super User にスマートな回答があった。

❯ egrep -n -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log | awk -F: '{print>$1}'

一時ファイルをたくさん作ることになるが、スクリプトはとても簡潔で書きやすい。そしてシェルっぽいやり方だとも感じる。

❯ cat 1
1:at=info
1:method=GET
1:path=/
1:host=mutelight.org
1:fwd="124.133.52.161"
1:dyno=web.2
1:connect=4ms
1:service=8ms
1:status=200
1:bytes=1653

❯ cat 2
2:level=info
2:tag=stopping_fetchers
2:id=ConsumerFetcherManager-1382721708341
2:module=kafka.consumer.ConsumerFetcherManager

❯ cat 3
3:level=info
3:msg="Stopping all fetchers"
3:tag=stopping_fetchers
3:id=ConsumerFetcherManager-1382721708341
3:module=kafka.consumer.ConsumerFetcherManager

後始末しやすいように一時的な作業ディレクトリを作って移動しておくか、スクリプトをちょっと変えて出力ファイル名に prefix をつけてもいい。

❯ egrep -n -o '[^ ]*=([^ ]*|"[^"]*")' logfmt.log | awk -F: '{fname="hoge_"$1; print>fname}'

❯ ls hoge_*
hoge_1  hoge_2  hoge_3

中身を見るのにいちいちファイルを開かないといけないのは手間に思えるが、こういうテキスト処理のコマンドの結果は作業中割と頻繁に見返したくなるもので、その都度重たいコマンドを再実行するのは結果的に時間の無駄に繋がったりしがち。なので最初から一時ファイルに書いておくのが正解なのかもしれない。

grep/egreprg で置き換え可能。grep 自体や正規表現の方言を避けるためにも rg が使えるなら使うのがベター。