nginxのログから表示したくないものをフィルタする

こんにちは、GMOインターネット株式会社の斉藤です。
今回のnginxの小ネタのお話です。

はじめに

nginxは非常に多く使われているWebサーバーの1つで、弊社のレンタルサーバやーVPSでも多く使われています。

私が関わっている案件で、「nginxのアクセスログから特定のクエリだけを除去したい」という要求がありました。クエリというのは、ここではURLに含まれるクエリ文字列のことです。

たとえば、このdeveloper.gmo.jpのサイトの右上にある検索フォームから「gmo」というワードで検索を行うと、URLは下記のようになります。

https://developers.gmo.jp/?s=gmo&action=Search

このs=golangの部分だけをアクセスログから除去したいと言う要求です。

平たく言うとユーザーが入力した検索文字列などをログに残したくないという話でした、実際には以下のような要件になります。

  • nginxのアクセスログのURLクエリ文字列から特定のキーを除去したい
  • HTTPリファラのURLも同様にしたい
  • 一時的であれストレージに記録はしたくない(なので一度記録されたログファイルに置換をかけるのは避けたい)
  • nginxだけで完結したい
  • s=hogehogeのキーの部分は残し値の部分だけ除去したい

2番目を少し補足すると、アクセスログではHTTPリファラーにもURLが入ることもあります。典型的なのは下記のようなWebサイトのアセットへのアクセスログです。

XXX.XXX.XXX.XXX - - [04/May/2021:18:00:00 +0900] "GET /js/index.js HTTP/1.1" 200 37194 "https://developers.gmo.jp/?s=gmo&action=Search" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"

さて、通常こういった要件ではPOSTリクエストを使うのが一般的です。ただ、今回の案件は既存のシステムへの改修でしたので、リクエスト方法変更という大きな変更ができなかったのです。

前提

nginxのログ出力形式はngx_http_log_moduleのlog_formatディレクティブで設定できます。
出力自体はaccess_logディレクティブで行います。

log_formatのデフォルトは以下のようになっています。

log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';

access_log /spool/logs/nginx-access.log compression buffer=32k;

変数が使えるのでこれで実現できそうです。$request と $http_referer ですね。これらの変数はnginxによってプリセットされるので、これを置換してURLクエリの該当部分を変更すれば良さそうです。そして、このlog_formatディレクティブはhttpコンテクストの中にしか書けません。access_logはhttp, server, locationの3つのコンテクストの中に書くことができます。

これを踏まえて、以下方針を決めました。

  • $requestのURLクエリ文字列から特定のキー除外した変数$filtered_requestを用意
  • $http_refererののURLクエリ文字列から特定のキー除外した変数$filtered_refererを用意
  • 独自のlog_formatを定義
  • access_logに上記log_formatを指定してログを出力する

設定方法

変数の用意

まず変数の用意です。
オリジナルの変数に対して正規表現置換を行って、URLクエリ文字列から特定キーを除去します。ただ、今回は完全に空文字にするのでは無く[FILTERED]という文字列に置き換えることにしました。この方が意図的に除去されたことがわかるので良いでしょう。

これはserverコンテクストの中で設定します。以下は例です。

server {
    listen 443 ssl default_server;

(snip...)

    set $filtered_request $request;
    if ($filtered_request ~ (.*)q=[^&]*(.*)) {
    set $filtered_request $1q=[FILTERED]$2;
    }
    set $filtered_referer $http_referer;
    if ($filtered_referer ~ (.*)q=[^&]*(.*)) {
      set $filtered_referer $1q=[FILTERED]$2;
    }

これで$filtered_requestと$filtered_refererと言う2つの変数がセットされます。

log_formatの定義

次に、httpコンテクストの中でlog_formatを定義します。

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;


    include /etc/nginx/mime.types;
    default_type application/octet-stream;


(snip...)

  log_format myformat '$http_cf_connecting_ip - $remote_addr - $remote_user [$time_local] '
        '"$filtered_request" $status $body_bytes_sent '
        '"$filtered_referer" "$http_user_agent"';

ここで$requestを$filtered_request、$http_refererを$filtered_refererにそれぞれ変更します。log_foramtの最初の引数は、このフォーマットの名前です。

access_logディレクティブの定義

最後にserverコンテクストに戻ってaccess_logディレクティブを定義します。access_logディレクティブは第二引数にログフォーマットの名前を渡すことができるので、ここに先ほど作成したフォーマットの名前を渡します。

access_log /dev/stdout myformat;
error_log /dev/stderr;

上で設定は完了です。nginxをリロードして設定を反映させましょう。すると、ログには以下のように記録されているはずです。

XXX.XXX.XXX.XXX - - [04/May/2021:18:00:00 +0900] "GET /?s=[FILTERED]&action=Search" 200 1335 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"

おわりに

今回は特定のURLパラメータをログに出力しない設定をしましたが、同様のやり方でログへの出力を柔軟に変更できるでしょう。何かのお役に立ったならうれしく思います。

それではまた次回お目にかかりましょう。

ブログの著者欄

斉藤 弘信

GMOインターネット株式会社

2000年に同社に入社。ユーザーサポートやデータセンターでのオペレーション業務等を担当し,その後社長室などのゼネラル部門を経て,2014年9月よりクラウド/ホスティング事業のテクニカルエバンジェリストを担当。現在はWebプロモーション研究室のソフトウェアエンジニアとして勤務。得意分野はWebアプリケーションの設計/開発,Linuxサーバー構築/運用。

採用情報

関連記事