NginxのアクセスログをFluentdでMySQLに格納して可視化

# 背景
個人管理のサーバでホームページを公開していて、各ページへのアクセス情報を見てみたくなった。ログ収集の試行もかねてアクセスログを可視化してみた。

# 環境構築の方針
- Docker, Docker-Composeを使って構築する。
- 個人的な経験値から以下のミドルウェアを利用する。
    - nginx：Webサーバ
    - fluentd：nginxのアクセスログを収集
    - mysql：収集したログ情報の格納
    - metabase：DBに格納したログ情報の可視化

# 構成
nginxが出力したログファイルを、fluentdで収集してmysqlに格納する。
構成図
![構成図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404547/029027d6-f3d4-e3e4-61a2-02b5638da29b.png "構成図")

# 前提環境
- OS : Ubunt 20.04.4 LTS
- Docker : 20.10.17, build 100c701
- Docker compose : version 1.29.1, build c34c88b2

# 構築
以下のステップで構築する。
1\. ディレクトリ構成の作成
2\. docker-composeファイルの作成
3\. mysqlコンテナの構築
4\. fluentdコンテナの構築
5\. Metabaseコンテナの構築

### 1\. ディレクトリの作成
以下のディレクトリ構成を作成した。
個別コンテナの設定ファイルなどについては後述する。
```
/home/com/homepage 
 ├─ nginx
 │  ├─ conf
 │  │  └─ nginx.conf
 │  └─ log
 │     └─ access.log　・・・nginxのアクセスログファイル
 ├─ contents　・・・webページのコンテンツ
 │  └─ index.html
 ├─ fluented
 │  ├─ data
 │  │  └─ nginx_access.log.pos ・・・最終読み込み位置の保持ファイル
 │  ├─ Dockerfile   
 │  └─ fluent.conf ・・・Fluentdの設定ファイル
 ├─ mysql
 │  ├─ scripts
 │  │  └─ create_tabel.sql　・・・テーブル作成のSQLファイル
 │  └─ dbvolume
 ├─ metabase
 └─ docker-compose.yaml
```
### 2\. docker-composeファイルの作成
以下の通り、docker-composeファイルを作成した。
[ 特記事項 ]
    - fluentdのコンテナからnginxのログファイルを参照する。

```:docker-compose.yaml
version: '3'
services:
  nginx:
    image: nginx:1.23
    ports:
      - 80:80
    volumes:
      - ./contents:/usr/share/nginx/html
      - ./nginx/log:/var/log/nginx
      - ./nginx/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf
    environment:
      TZ: Asia/Tokyo
    restart: always

  metabase:
    image: metabase/metabase:v0.46.2
    restart: always
    volumes:
      - ./metabase:/metabase-data
    ports:
      - 3000:3000
    environment:
      MB_DB_FILE: /metabase-data/metabase.db

  fluentd:
    build:
      context: .
      dockerfile: ./fluentd/Dockerfile
    ports:
      - 24224:24224
    volumes:
      - ./nginx/log:/var/log/nginx
      - ./fluentd/fluent.conf:/fluentd/etc/fluent.conf
      - ./fluentd/data:/fluentd/log

  db:
    image: mysql:8.0
    restart: always
    volumes:
     - ./mysql/dbvolume:/var/lib/mysql
     - ./mysql/scripts:/docker-entrypoint-initdb.d
    ports:
     - 3306:3306
    environment:
      - MYSQL_DATABASE=log
      - MYSQL_USER=log
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=passwordpassword
      - TZ=Asia/Tokyo
```

### 3\. mysqlコンテナの構築
以下のステップを実施した。
1\. テーブル定義ファイルの作成
2\. コンテナ起動時のテーブル作成自動化

#### 3-1\. テーブル定義ファイルの作成
　Fluendの公式ページを参照して、nginxのアクセスログを解析した際の出力要素を確認した。
　それらの項目をカラムとして持つテーブルを作成するためのSQL文をファイルに出力した。
```sql:create_tabel.sql
CREATE TABLE `nginx_access_log` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `time_jst` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
  `remote_ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `host_ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `user_id` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `method` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `access_path` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `size` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `referer` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `agent` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `http_code` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL,
  `xforwarded` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;

```
参考
[Fluentd nginx parser plugin](https://docs.fluentd.org/parser/nginx)

#### 3-2\. コンテナ起動時のテーブル作成自動化
Fluentdのコンテナ起動時にテーブルが無いと、Fluentdでエラーになる。そのため、テーブルが存在しない場合には、自動的にテーブルを作成するようにした。
docker-entrypoint-initdb.dにスクリプトファイルを格納しておくと、コンテナ起動時に自動的に実行されるので、3-1で作成したスクリプトをコンテナにマウントする。
```:docker-compose.yaml
  db:
    image: mysql:8.0
    restart: always
    volumes:
     - ./mysql/dbvolume:/var/lib/mysql
     - ./mysql/scripts:/docker-entrypoint-initdb.d←これを追加
 ```

参考
[【Docker】【MySQL】コンテナ初回起動時にスクリプトを実行させる](https://zenn.dev/re24_1986/articles/978801ae092498)


### 4\. fluentdコンテナの構築
以下のステップを実施した。
1\. MySQLプラグインのインストール
2\. ログ収集の設定

#### 4-1. MySQLプラグインのインストール
fluentdで読み込んだnginxのログをmysqlにbulk_insert[^1]するために、td-agentでmysqlのプラグインを追加しておく必要がある。
fluentdのデフォルトのコンテナイメージには、mysqlのプラグインが含まれていない。コンテナ立ち上げ時にインストールしておくようにDockerfileを用意してbuildする。

[ 特記事項 ]
    - インストールするプラグインは、fluent-plugin-mysql
    - ファイルサイズが大きくならないように--no-documentを指定
    - ユーザflunetには、インストール権限が無いため、rootに切り替える
    - コンテナのタイムゾーンをJSTに変更
```Dockerfile:Dockerfile
FROM fluent/fluentd:v1.16-1

USER root

RUN apk add --no-cache --virtual .build-deps \
    build-base \
    ruby-dev \
    tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo "Asia/Tokyo" > /etc/timezone && \
    apk add --no-cache \
    mariadb-dev && \
    gem install fluent-plugin-mysql --no-document && \
    apk del .build-deps

USER fluent
```

参考
[fluent/fluentd - Docker Image](https://hub.docker.com/r/fluent/fluentd/)
[【Docker環境】FluentdとMySQLのbulk insertを使用したログ収集【前編】](https://www.aska-ltd.jp/jp/blog/60)

#### 4-2. ログ収集の設定
nginxのログを読み込んで、mysqlサーバに登録するために設定ファイルを作成する。

```:fluent.conf
<source>
  @type tail
  format nginx
  path /var/log/nginx/access.log
  pos_file /fluentd/data/nginx_access.log.pos
  tag nginx.access
</source>

<match nginx.access>
  @type mysql_bulk
  host 192.168.2.10
  port 3307
  database log
  username log
  password password
  
  column_names id,time_jst,remote_ip,host_ip,user_id,method,access_path,http_code,size,referer,agent,xforwarded
  key_names id,host,user,${time},method,path,version,code,size,referer,agent,xforwarded
  table nginx_access_log
  
  flush_interval 10s
</match>
```
参考
[fluentdのnginxアクセスログ取得をdockerで試す](https://medium.com/nyle-engineering-blog/fluentd%E3%81%AEnginx%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%AD%E3%82%B0%E5%8F%96%E5%BE%97%E3%82%92docker%E3%81%A7%E8%A9%A6%E3%81%99-bf600b1f3389)



### 5\. metabaseによる可視化
ダッシュボードの作成
「分析」機能-「要約」で、集約方法：カウント、集約キー：time_jst(週)を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404547/2809f566-aa19-7efc-8ee6-eb39cfe30a33.png)

ひとまず、以上で可視化までのパスを通すことができました。
ダッシュボードの拡張は今後のタスクにして、今回はここまでにします。


## 躓いたところ
### 1\. ログファイルのパーミッション

fluentdのコンテナを立ち上げたとき、以下のエラーが発生して、コンテナが起動しなかった。

> unexpected error error_class=Errno::EACCES error="Permission denied @ rb_file_s_stat - `/var/log/nginx/access.log"

fluentdがnginxのアクセスログ(access.log)を読み込む権限が無くてエラーになっていた。
fluentdが参照できるようにaccess.logファイルのパーミッションを変更した。
> chmod 775 /home/com/homepage/web/log/access.log

fluentdのコンテナを再起動したら、上記のエラーは解消したが、次のエラーが発生した。
> unexpected error error_class=Errno::EACCES error="Permission denied @ rb_sysopen - /fluentd/log/nginx_access.log.pos"

fluentdがnginxのアクセスログ(access.log)を読み込む際に、
どこまで読み込んだかという情報を保持するために、access.log.posというファイルを作成するが、そのファイルを作れなくてエラーになっていた。
ファイルを作成できるようにディレクトリのパーミッションを変更した。

> chmod 777 ./fluentd/data

これで、エラー無くコンテナが起動するようになった。

参照
[Permission denied @ rb_file_s_stat #2425](https://github.com/fluent/fluentd/issues/2425)

### 2\. fluentdのログ確認
fluentdのログを確認するために、fluentdのコンテナに接続しようと以下のコマンドを実行したが、fluentdのコンテナにはbashがなかったので接続できなかった。
> docker exec -it homepage_nginx_1 /bin/bash

bashではなく、shはインストールされていたので、下記のコマンドで接続することで、起動できた。
> docker exec -it homepage_nginx_1 /bin/sh

接続後、コンソールにて、コマンドを実行してログを確認できた。
> fluentd -vv -c /fluentd/etc/fluent.conf


参照
[Fluentd の Docker イメージを動かした際のメモ](https://gigazine.net/news/20150612-oculus-touch/)
[Fluentd Logging](https://docs.fluentd.org/deployment/logging)

### 3\. fluentdコンテナのタイムゾーン
初期の状態ではUTCになっていた。docker-composeにて、環境変数でタイムゾーンを指定しても反映されなかった。
下記のサイトを参照して、Dockerfileを修正し、コンテナイメージを変更した。
[Dockerコンテナ（Fluentd）のTimeZoneをUTCからJSTに変更する](https://www.aska-ltd.jp/jp/blog/104)

## その他のTips
#### 1\. 既存ログの読み込み
既に運用を開始しているwebサーバのアクセスログを可視化するため、ログファイルの先頭から読み込みたい。
fluentdの初期設定では、末尾の情報しか読み込まないので、設定の変更が必要になる。
read_from_headをTrueにする。

fluent.conf
```
<source>
  @type tail
  format nginx
  path /var/log/nginx/access.log
  pos_file /fluentd/data/nginx_access.log.pos
  tag nginx.access
  read_from_head true　← 追加！！
</source>
```
参考
[fluentdのin_tailプラグインの動作について理解する](https://debug-life.net/entry/996#toc12)

#### 2\. 監視間隔の短縮
デフォルトの設定では、ログファイルの更新を確認する監視間隔は60秒になっている。動作確認をする際には、時間間隔が短いほうが都合がいいので、設定を変更した。
fluent.conf
```
<source>
  @type tail
  format nginx
  path /var/log/nginx/access.log
  pos_file /fluentd/data/nginx_access.log.pos
  tag nginx.access
  read_from_head true
  refresh_interval 10　← 追加！！
</source>
```

参考
[fluentdのin_tailプラグインの動作について理解する](https://debug-life.net/entry/996#toc12)

#### 3\. テーブルのオートインクリメントの初期化
DBへの格納を試行錯誤していると、オートインクリメントによってIDの値がどんどん大きくなっていってしまう。
運用開始時点では、1から始まるようにしたいので、オートインクリメントの値を初期化した。
以下のSQLを実行することで初期化できる。
> ALTER TABLE <テーブル名> AUTO_INCREMENT = 1;

今回のケースでは、以下のSQLになる。
> ALTER TABLE nginx_access_log AUTO_INCREMENT = 1;

下記のようにtruncate table文で、すべてのレコードを削除した場合もインクリメントは初期化される。
> TRUNCATE TABLE <tbl_name>

参考
[MySQL で AUTO_INCREMENT の値をリセットする方法](https://mysql.sql55.com/query/mysql-reset-auto-increment.php)
[全てのデータを削除する(TRUNCATE TABLE文)](https://www.javadrive.jp/mysql/insert/index12.html#section2)


[^1]: bulk_insert：リレーショナルデータベース（RDB）のテーブルに行を追加する際、複数の行を一回のSQL文の実行で追加すること　[e-words](https://e-words.jp/w/%E3%83%90%E3%83%AB%E3%82%AF%E3%82%A4%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%88.html#:~:text=%E3%83%90%E3%83%AB%E3%82%AF%E3%82%A4%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%88%20%E3%80%90bulk%20insert%E3%80%91,%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%88%97%E6%8C%99%E3%81%99%E3%82%8B%E3%80%82 "bulk_insertとは")