NginxとLuaでリクエストHTTPヘッダを全てログに残す

NginxでリクエストHTTPヘッダを全てログに残すにはLuaスクリプトを使うといい。NginxでLuaを動かすにはNginxのモジュールが組み込まれている必要がある。

CentOS7でyumから入れたNginxにはlua-nginx-moduleが組み込まれていない。

$ cat /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=0

$ sudo yum install nginx --enablerepo=nginx

$ nginx -V 2>&1 | grep -o ngx_lua | head -1

OpenResty

install

Luaモジュールを使うために、NginxをソースからinstallするかLua周りを整えてくれるNginxのOpenRestyを使う。

ソースからのインストールはCentOS7にNginx + Lua のインストールを参考にしようとしたが結構大変そう。

対してOpenRestyが便利そうなのでOpenRestyを使う。

OpenRestyはnginxのほかにngx_luaをはじめとするCで書かれた各種サードパーティモジュールとngx_luaのAPIを利用したrestyモジュール、そしてLua/LuaJITで構成されています。

OpenRestyに含まれているnginx自体は本家のnginxと基本同じなので、別にOpenRestyを利用しなくても自分でngx_luaを組み込んだり、サーバ上にrestyモジュールを配布することで似たような環境を構築することは可能ですが、OpenRestyであれば主要なモジュールやライブラリが./configure、make、make installの一連の流れですべてゴソッとインストールされますし、OpenRestyのconfigureスクリプトはnginxのconfigureスクリプトを継承したものなのでnginxのconfigureオプションをほぼそのまま利用することもできます。

なので、ngx_luaを利用したいようなケースでは特に理由がなければOpenRestyを利用する方が手っ取り早くそして運用も楽になるのでオススメです。

引用: https://tech.mercari.com/entry/2016/05/25/170108

公式のインストール手順を見てinstallする。yumで簡単にinstallできる。

$ sudo yum install yum-utils
$ sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
$ sudo yum install openresty

$ openresty -V 2>&1 | grep -o ngx_lua | head -1
ngx_lua

nginx.conf

リクエストHTTPヘッダを全てログに残すためのLuaスクリプトをnginx.confに書く。

$ cd /usr/local/openresty/nginx/
$ sudo vim conf/nginx.conf

serverディレクティブやlocationディレクティブに書けるようだ。https://stackoverflow.com/questions/24380123/how-to-log-all-headers-in-nginxを参考にして書いた。

server {
    listen       80;
略
    header_filter_by_lua_block {
        local h = ngx.req.get_headers()
        local h_str = "##"
        for k, v in pairs(h) do
          h_str = h_str..k..":"..v.."\t"
        end
        h_str = h_str.."##"
        ngx.log(ngx.ERR, "Got header "..h_str)
    }
}

各ヘッダはタブでつなげて一つの文字列とし、後でこの部分だけを取り出しやすいように##でくくった。

$ sudo systemctl start openresty
$ systemctl status openresty

$ curl http://localhost/
$ curl http://localhost/test
$ tail logs/error.log
2018/07/27 20:53:02 [error] 8434#8434: *30 [lua] header_filter_by_lua:8: Got header ##host:localhost    accept:*/*      user-agent:curl/7.29.0  ## while sending response to client, client: 127.0.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost"
2018/07/27 20:53:05 [error] 8434#8434: *31 open() "/usr/local/openresty/nginx/html/test" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: "GET /test HTTP/1.1", host: "localhost"
2018/07/27 20:53:05 [error] 8434#8434: *31 [lua] header_filter_by_lua:8: Got header ##host:localhost    accept:*/*      user-agent:curl/7.29.0  ##, client: 127.0.0.1, server: localhost, request: "GET /test HTTP/1.1", host: "localhost"

##host:localhost accept:*/* user-agent:curl/7.29.0 ##というようにヘッダが取得できている。

ヘッダログの利用例

残したヘッダログを用いて、ブラウザやアプリから送られてきたHTTPリクエストをヘッダ部分までログから正確に再現する。それを使ってCDNの暖気などに使う。

ワンライナーで書くと次のようになる。

$ cat error.log | grep 'Got header' | sed 's/^.*Got header ##//' | sed 's/##.*"GET//' | sed 's#HTTP/1.1".*##' | sort -u | ruby -e 'STDIN.readlines.each{|line| puts "curl " + line.chomp.split("\t").map{|i| "-H \"#{i}\""}.join(" ").gsub(/-H \" (\/.*) \"$/, "http://localhost\\1") }' | sh

さすがに長いので改行する。

$ cat error.log |
    grep 'Got header' |
    sed 's/^.*Got header ##//' |
    sed 's/##.*"GET//' |
    sed 's#HTTP/1.1".*##' |
    sort -u

ここまでで行っていることは、##ヘッダ文字列##GET /URL/URLを取り出し一意にしている。このような出力となる。

host:localhost  accept:*/*      user-agent:curl/7.29.0   /
host:localhost  accept:*/*      user-agent:curl/7.29.0   /test

これをcurlでアクセスするように行単位で文字列整形する。

## 続き
ruby -e 'STDIN.readlines
              .each{|line|
                puts "curl " +
                     line.chomp
                         .split("\t")
                         .map{|i| "-H \"#{i}\""}
                         .join(" ")
                         .gsub(/-H \" (\/.*) \"$/, "http://localhost\\1") }
'

出力はこうなる。

curl -H "host:localhost" -H "accept:*/*" -H "user-agent:curl/7.29.0" http://localhost/
curl -H "host:localhost" -H "accept:*/*" -H "user-agent:curl/7.29.0" http://localhost/test

あとは出力をパイプでshにつないで、実際にcurlを発行している。