Nginxのアクセスログをfluentd経由でMySQLに保存する

「nginx実践入門」を呼んでやってみたかったことの一つ、fluentdに入門してみようと思います。

環境構築は https://itoka.hatenadiary.com/entry/2022/03/04/004453 がベースです。

環境

  • Apple M1 Pro
  • macOS Monterey 12.1
  • Docker Engine 20.10.12
  • Docker Compose v2.2.3
  • Ruby 3.1.1
  • Ruby on Rails 7.0.2
  • Nginx 1.21.6
  • fluentd 1.14

fluentdのDockerfile

hub.docker.com

Docker Hub > fluentd > How to build your own image > 3.1 For current images を参考に、mysqlプラグインが入ったDockerfileを作成していきます。

ディレクトリはapp/docker/fluentd/にしました。

ちなみに、&& apk add --no-cache mariadb-dev \ がないと、ビルド時にERROR: Error installing fluent-plugin-mysql:で失敗しました。

FROM fluent/fluentd:v1.14-1

# Use root account to use apk
USER root

# below RUN includes plugin as examples elasticsearch is not required
# you may customize including plugins as you wish
RUN apk add --no-cache --update --virtual .build-deps \
  sudo build-base ruby-dev \
    && apk add --no-cache mariadb-dev \ # これが無いとビルド時エラー
  && sudo gem install fluent-plugin-mysql --no-document \ # mysqlプラグインの指定
  && sudo gem sources --clear-all \
  && apk del .build-deps \
  && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

docker-compose.yml

version: '3'

services:
  web:
    build:
      context: .
      dockerfile: ./docker/web/Dockerfile
    volumes:
      - .:/app
      - public:/app/public
      - tmp:/app/tmp
    depends_on:
      - db
    stdin_open: true
    tty: true
    environment:
      DB_ROOT_USERNAME: root
      DB_USERNAME: development
      DB_PASSWORD: password
      TZ: Asia/Tokyo
      DB_HOST: db

  db:
    platform: linux/x86_64
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=development
      - MYSQL_USER=development
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=passwordpassword
      - TZ=Asia/Tokyo
    ports:
      - '3306:3306'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./volumes/mysql/data/db/:/var/lib/mysql
      - ./volumes/mysql/conf/db/my.cnf:/etc/my.cnf.d/my.cnf

  nginx:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./docker/nginx/conf.d:/etc/nginx/conf.d
      - public:/app/public
      - tmp:/app/tmp
      - http-log:/var/log/nginx # 追加
    ports:
      - "80:80"
    depends_on:
      - web

  fluentd: # 追加
    build:
      context: .
      dockerfile: ./docker/fluentd/Dockerfile
    ports:
      - 24224:24224
    volumes:
      - ./docker/fluentd/fluent.conf:/fluentd/etc/fluent.conf
      - http-log:/var/log/nginx

  log_db: # 追加
    platform: linux/x86_64
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=log
      - MYSQL_USER=log
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=passwordpassword
      - TZ=Asia/Tokyo
    ports:
      - '3307:3306'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./volumes/mysql/data/log_db/:/var/lib/mysql
      - ./volumes/mysql/conf/log_db/my.cnf:/etc/my.cnf.d/my.cnf
      - ./docker/fluentd/initdb.d:/docker-entrypoint-initdb.d

volumes:
  public:
  tmp:
  http-log: # 追加 
  • nginxとfluentdでログを共有するため、http-logボリュームを追加しました
  • nginxについて、個別設定ファイルを作成するため、./docker/nginx/conf.d:/etc/nginx/conf.dをvolumeに指定しました
  • ログ保管用にlog_dbというMySQLコンテナを立てます
  • log_db/docker-entrypoint-initdb.dにログ保管用のCREATE TABLE文を記載したSQLを指定し、コンテナ起動時にテーブルが作成されるようにします

nginxのログフォーマットを変更する

使いやすいLTSVにします。

conf.dに格納し、nginx.confでincludeするようにします。

docker/nginx/conf.d/log_format.conf

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

log_format  ltsv  'time:$time_iso8601\t'
                  'remote_addr:$remote_addr\t'
                  'status:$status\t'
                  'request_method:$request_method\t'
                  'request_uri:$request_uri\t'
                  'request_length:$request_length\t'
                  'https:$https\t'
                  'uri:$uri\t'
                  'query_string:$query_string\t'
                  'bytes_sent:$bytes_sent\t'
                  'body_bytes_sent:$body_bytes_sent\t'
                  'http_referer:$http_referer\t'
                  'useragent:$http_user_agent\t'
                  'forwardedfor:$http_x_forwarded_for\t'
                  'request_time:$request_time\t'
                  'upstream_addr:$upstream_addr\t'
                  'upstream_response_time:$upstream_response_time\t'
                  'upstream_cache_status:$upstream_cache_status';

docker/nginx/nginx.conf

http {
  upstream puma {
    server  unix:///app/tmp/sockets/puma.sock;
  }

  include /etc/nginx/conf.d/*.conf;

    access_log  /var/log/nginx/access.log ltsv;

# ...

また、/var/log/nginx/access.log/var/log/nginx/error.logですが、nginxのdocker imageだとこれらのファイルは標準出力へのシンボリックリンクとなっており、ファイルに出力してくれないようでした。

https://imanengineer.net/docker-compose-nginx-log/

ログのファイル名を別のものに変えると、ファイルに出力してくれます。

docker/nginx/nginx.conf

# ...

error_log /var/log/nginx/error_dev.log; # error_dev.log

http {
  upstream puma {
    server  unix:///app/tmp/sockets/puma.sock;
  }

  include /etc/nginx/conf.d/*.conf;

    access_log  /var/log/nginx/access_dev.log ltsv; # access_dev.log

# ...

ログ保管用MySQLテーブル

カラム定義は適当です。LTSVのフォーマットに合わせて作成します。

docker/fluentd/initdb.d/Log.sql

DROP SCHEMA IF EXISTS log;
CREATE SCHEMA log;
USE log;

DROP TABLE IF EXISTS nginx_access_log;

CREATE TABLE nginx_access_log
(
  id                      INT AUTO_INCREMENT PRIMARY KEY,
  time                    DATETIME,
  remote_addr             VARCHAR(200),
  status                  INT,
  request_method          VARCHAR(10),
  request_uri             TEXT(3000),
  request_length          INT,
  https                   VARCHAR(10),
  uri                     TEXT(3000),
  query_string            TEXT(3000),
  bytes_sent              INT,
  body_bytes_sent         INT,
  http_referer            TEXT(3000),
  useragent               TEXT(500),
  forwardedfor            TEXT(500),
  request_time            VARCHAR(20),
  upstream_addr           VARCHAR(200),
  upstream_response_time  VARCHAR(20),
  upstream_cache_status   VARCHAR(20)
);

fluent.conf

fluentdの設定ファイルを用意します。

docker/fluentd/fluent.conf

<source>
  @type tail
  format ltsv
  path /var/log/nginx/access_dev.log
  pos_file /var/log/nginx/access_dev.log.pos
  tag nginx.access
  time_key time
  time_format %Y-%m-%dT%H:%M:%S%z
</source>

<match nginx.access>
  @type mysql_bulk
  host log_db
  port 3306
  database log
  username log
  password password
  table nginx_access_log
 
  column_names time,remote_addr,status,request_method,request_uri,request_length,https,uri,query_string,bytes_sent,body_bytes_sent,http_referer,useragent,forwardedfor,request_time,upstream_addr,upstream_response_time,upstream_cache_status
  key_names ${time},remote_addr,status,request_method,request_uri,request_length,https,uri,query_string,bytes_sent,body_bytes_sent,http_referer,useragent,forwardedfor,request_time,upstream_addr,upstream_response_time,upstream_cache_status

    flush_interval 10s
</match>

source

  • @typeには tailを指定します
  • formatにはLTSVを指定します
  • time_formatは、後ほどMySQLのDATETIMEカラムに格納しやすくするために指定しています(ログフォーマットでせっかくtime:$time_iso8601を指定したのに意味がない・・?)

match nginx.access

  • sourcenginx.accessのタグをつけ、match nginx.accessMySQLに飛ばしていきます
  • プラグインMySQLに送りつける場合、@typemysql_bulkのようです
  • host~passwordはログデータを格納するDB接続情報を指定します(ここではlog_dbに指定した接続情報
  • column_namesにはログを保管するMySQLのカラムを指定します
  • key_namecolumn_namesとLTSVのラベルをマッピングさせます

動作確認

ここまで対応したらdocker-compose upをしたうえでrailsアプリケーションにアクセス(http://localhost)し、アクセスログMySQLに送られていることを確認できると思います。

# MySQLにログイン
$ docker-compose exec log_db bash -c 'mysql -u ${MYSQL_USER} -p${MYSQL_PASSWORD}'

mysql> use log
mysql> select * from nginx_access_log\G

*************************** 1. row ***************************
                    id: 1
                  time: 2022-03-16 14:04:59
           remote_addr: 172.18.0.1
                status: 304
        request_method: GET
           request_uri: /
        request_length: 1534
                 https: 
                   uri: /
          query_string: -
            bytes_sent: 788
       body_bytes_sent: 0
          http_referer: -
             useragent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
          forwardedfor: -
          request_time: 0.506
         upstream_addr: unix:///app/tmp/sockets/puma.sock
upstream_response_time: 0.506
 upstream_cache_status: -
26 rows in set (0.01 sec)

おまけ

本当はMySQLに取り込んだレコードをMetabaseに読み込んでSQL発行までしたかったのですが、コンテナの起動がうまくいったりいかなかったりしたので、おまけ程度にdocker-compose.ymlを載せておきます。。

# ...

log_db:
    platform: linux/x86_64
    image: mysql:8.0
    environment:
      - MYSQL_DATABASE=log
      - MYSQL_USER=log
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=passwordpassword
      - TZ=Asia/Tokyo
    ports:
      - '3307:3306'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./volumes/mysql/data/log_db/:/var/lib/mysql
      - ./volumes/mysql/conf/log_db/my.cnf:/etc/my.cnf.d/my.cnf
      - ./docker/fluentd/initdb.d:/docker-entrypoint-initdb.d

  metabase: # 追加
    image: metabase/metabase
    volumes:
      - "./docker/metabase/data:/metabase-data"
    environment:
      MB_DB_FILE: /metabase-data/metabase.db
      MB_DB_TYPE: mysql
      MB_DB_DBNAME: log
      MB_DB_PORT: 3306
      MB_DB_USER: log
      MB_DB_PASS: password
      MB_DB_HOST: log_db
    ports:
      - "3000:3000"
    depends_on:
      - log_db

参考記事

tech-lab.sios.jp

www.aska-ltd.jp