notebook

都内でWEB系エンジニアやってます。

awkでカジュアルなログ集計

awkワンライナー( ログ集計 )

ちょっとしたログを集計したりする場合に、いちいちスクリプト書いて実行するのはめんどうですよね、そこでawkの出番です。

シェルコマンドと組み合わせて割と簡単に集計ができます。

いつも書き方忘れていちいち調べてるのでよく使うものを残しておきます。

基本構文

awkではセパレータで区切った文字列を$1,$2...のように持っています

デフォルトのセパレータはスペースです。

サンプルではcatしてパイプで渡してますが、tailとかで垂れ流しとかもできます

  • すべてのフィールドを出力する
cat sample.txt | awk '{print $0}'
  • 特定のフィールドを出力する
$ cat sample.txt
hoge fuga piyo
aaaa bbbb cccc
dddd eeee ffff
$ cat sample.txt | awk '{print $1,$2}'
hoge fuga
aaaa bbbb
dddd eeee
  • 行数を出力する
$ cat sample.txt | awk '{print FNR,$1,$2}'
1 hoge fuga
2 aaaa bbbb
3 dddd eeee

組み込み変数「FNR」を使うことで行数を出力することができます

  • フィールド数を出力する
$ cat sample.txt
hoge fuga piyo
aaaa bbbb cccc
dddd eeee ffff
$ cat sample.txt | awk '{print $(NF-1)}'
fuga
bbbb
eeee

組み込み変数「NF」を使うことでフィールド数を出力することができます

また、組み込み変数から計算した値のフィールドの値を出力することもできます

分岐

$ cat sample.log
Dec 8 19:18:44 hostname 182.250.253.9 domain "GET /hoge HTTP/1.1" 200 - 17370
Dec 8 19:18:44 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:45 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:46 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:46 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:46 hostname 122.29.23.11 domain "GET /images/moge.png HTTP/1.1" 200 21595 669
Dec 8 19:18:47 hostname 106.158.127.211 domain "GET /hoge HTTP/1.1" 200 - 14123
Dec 8 19:18:47 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:48 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173
$ cat sample.log | awk '{if($10!=200)print $0}'
Dec 8 19:18:44 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:46 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
  • レスポンスのバイト数が○○以上のアクセスを絞りたいとき
$ cat sample.log | awk '{if($11>1000)print $0}'
Dec 8 19:18:44 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:46 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:46 hostname 122.29.23.11 domain "GET /images/moge.png HTTP/1.1" 200 21595 669
  • リクエストパスに○○が含まれているものを絞りたいとき
$ cat sample.log | awk '{if($8~/yo/)print $0}'
Dec 8 19:18:45 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:46 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:47 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:48 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173

こちらは正規表現が使えます

$ cat sample.log| awk '{if($3~/19:18:46/ && $10!=200 )print $0}'
Dec 8 19:18:46 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864

論理和論理積も使うことができます

セパレータ

  • 明示的にセパレータを指定
$ cat sample.csv
aaa,bbb,ccc,ddd
eee,fff,ggg,hhh
iii,jjj,kkk,lll
$ cat sample.csv | awk -F"," '{print $1,$2}'
aaa bbb
eee fff
iii jjj

-Fオプションでセパレータを指定できます。 デフォルトはスペースですが、別のものを指定したい場合に使用します。これでCSVとかもいけますね

  • セパレータを複数指定する
$ cat sample.log
Dec 8 19:18:44 hostname 182.250.253.9 domain "GET /hoge HTTP/1.1" 200 - 17370
Dec 8 19:18:44 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:45 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:46 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:46 hostname 153.120.34.51 domain "GET /fuga HTTP/1.0" 404 1379 57864
Dec 8 19:18:46 hostname 122.29.23.11 domain "GET /images/moge.png HTTP/1.1" 200 21595 669
Dec 8 19:18:47 hostname 106.158.127.211 domain "GET /hoge HTTP/1.1" 200 - 14123
Dec 8 19:18:47 hostname 49.98.50.118 domain "GET /images/poyo.png HTTP/1.1" 200 243 173
Dec 8 19:18:48 hostname 49.98.50.118 domain "GET /images/piyo.png HTTP/1.1" 200 243 173
$ cat sample.log | awk -F"[: ]" '{print $4,$5,$10}'
18 44 /hoge
18 44 /fuga
18 45 /images/poyo.png
18 46 /images/piyo.png
18 46 /fuga
18 46 /images/moge.png
18 47 /hoge
18 47 /images/poyo.png
18 48 /images/piyo.png

ログはスペースで区切って.....時間はコロンで区切って......なんて時にセパレータを複数指定することができます

セパレータにしたい文字列を[]で複数指定します

  • 出力時の区切り文字を指定する
$ cat sample.log | awk -v OFS=":" '{print $1,$2}'
/hoge:200
/fuga:404
/images/poyo.png:200
/images/piyo.png:200
/fuga:404
/images/moge.png:200
/hoge:200
/images/poyo.png:200
/images/piyo.png:200

$1と$2の間にコロンが出力されるようになります

集計

  • 平均
$ cat sample.log | awk -v OFS="\n" '{sum+=$12}END{print "sum: "sum,"avg: "sum/NR}'
sum: 148582
avg: 16509.1

よくある平均出したいみたいな時。

ENDブロックはBEGINブロックとセットで書くことが多いですが、とくに必要なければ省略も可能です。

また、変数の初期化もとくに値をセットする必要がなければ必要ありません。

上記コマンドは下記コマンドと等価

$ cat sample.log | awk -v OFS="\n" 'BEGIN{sum=0}{sum+=$12}END{print "sum: "sum,"avg: "sum/NR}' * 流れ * BEGIN処理 * 行ごとに処理 * END処理

  • グループごとの行数
$ cat sample.log | awk -v OFS=":" -F"[: ]" '{print $3,$4,$5}' | sort | uniq -c
      2 19:18:44
      1 19:18:45
      3 19:18:46
      2 19:18:47
      1 19:18:48
# awkだけでやった場合はこんな感じ
$ cat sample.txt | awk -F"[: ]" '{name[$3":"$4":"$5]++;}END{for(t in name){print name[t],t}}'
2 19:18:44
1 19:18:45
3 19:18:46
2 19:18:47
1 19:18:48

時間ごとのアクセス数を出してみました

連想配列が使えるので普段スクリプト書いてやっているようなこともワンライナーでできたりします

まぁ、シェルコマンドと合わせても実現できますね

実践

# レスポンスタイム毎の件数の算出
$ grep --- access_log | awk '{ c[int($18/1000000)]++;} END{for(t in c){print t"s",c[t]}}'  | sort -n
0s 55113
1s 237
2s 101
3s 138
4s 146
5s 209
6s 297
7s 309
8s 291
9s 269
10s 235
# 分間、IP毎のエラーログの集計
$ grep -P '10 17:(0|1|2)' error_log.2014-12-10 | awk -F"[: ]" '{c[$3":"$4,$21]++} END{for( i in c ){ split(i,is,SUBSEP); print is[1],is[2],c[is[1],is[2]]}}' | sort
17:16 (10.0.32.202) 36
17:17 (10.0.32.201) 39
17:17 (10.0.32.205) 33
17:18 (10.0.32.202) 57
17:18 (10.0.32.204) 15

awkでは多次元配列も扱うことができます、これを用いることで若干細かな集計も行うことができます。

まぁこれくらいやるならスクリプト書いたほうがいい気もしますけどねw

おまけ

  • 色付け
cat sample.log | awk '{print "\033[35m",$1,"\033[37m"}'

シェルスクリプトとかと変わらないですね

とりあえずこのくらい覚えておけばぱっとワンライナーでログが集計できるんじゃないかと思います。