NginxのアクセスログとエラーログをJSONで整形しよう

# はじめに
### Nginxのログって見づらくない？

デフォルト設定のnginxログ出力はこんな感じになっています。
```
192.0.2.0 - - [07/Jun/2023:12:00:00 +0900] "GET /hoge/files/hogehoge/getstats?dir=HOGE HTTP/2.0" 200 16426 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "-"
```

## 目的
エラー状況の解析、把握のために通常ログ・4XXエラー・5XXエラーと3つのファイルに分割して保存し、リクエスト先URLを表示することで攻撃への対処に活用できるようにします。

## 設定

### エラー処理
mapでそれぞれのレスポンスステータスに合わせてフラグをセットします。
```
......
http {

    map $status $res_status_4xx {
        ~^[4] 1;

        default 0;
    }

    map $status $res_status_5xx {
        ~^[5] 1;

        default 0;
    }

......
```

### JSON形式で保存
JSON形式でログを保存することでパースしやすくし、リクエスト先URLを記録します。
また、デフォルトのtime表示からISO8601表記に変更して可読性を高めます。
```
......
    log_format json escape=json '{'
                                '"time":"$time_iso8601",'
                                '"forwarded_for":"$http_x_forwarded_for",'
                                '"client":"$remote_addr",'
                                '"status":"$status",'
                                '"request_method":"$request_method",'
                                '"url":"$scheme://$http_host$request_uri",'
                                '"referer":"$http_referer",'
                                '"request_body":"$request_body",'
                                '"body_bytes_sent":$body_bytes_sent,'
                                '"user_agent":"$http_user_agent",'
                                '"authorization":"$http_authorization"'
                                '}';

    access_log /var/log/nginx/access.log json;
    access_log /var/log/nginx/error4xx.log json if=$res_status_4xx;
    access_log /var/log/nginx/error5xx.log json if=$res_status_5xx;
......
```
設定が完了したら保存してnginxを再起動してください。

### 確認
curlか何かでURLを叩き、ログを確認します。

出力は以下。

```
{
	"time": "2023-06-07T12:30:00+09:00",
	"forwarded_for": "",
	"client": "192.0.2.0",
	"status": "200",
	"request_method": "GET",
	"url": "https://example.com/hogehoge",
	"referer": "",
	"request_body": "",
	"body_bytes_sent": 81,
	"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
	"authorization": ""
}
```


古いログフォーマットファイルに追記されるとあまりよろしくないので、固めてバックアップしてから古いログは消してしまいましょう。



## 追記
せっかくJSONにしたんだし何か活用しようということで、SQLに突っ込めるPythonスクリプトを書きました。
あんまり意味はないしたぶんfluentdとか使った方が早いです。


*watchdogsでtail -Fが実装できればfluentdの代替になるかもしれません。
```python
import json
import mysql.connector

# JSONファイルのパス
json_file_path = ["/var/log/nginx/error4xx.log", "/var/log/nginx/error5xx.log"]

# MySQL接続情報
mysql_host = ""
mysql_user = ""
mysql_password = ""
mysql_database = ""
mysql_port = ""

# MySQLに接続
db = mysql.connector.connect(
    host=mysql_host,
    user=mysql_user,
    password=mysql_password,
    database=mysql_database,
    port=mysql_port
)
cursor = db.cursor()

# JSONファイルを読み込む
for path in json_file_path:
    with open(path, 'r') as f:
        stream = f.readlines()
        for raw_data in stream:
            data = json.loads(raw_data)
            
            time = data["time"].replace("T", " ").strip("+09:00")
            client_ip = data["client"]
            url = data["url"]
            status_code = data["status"]
            request_method = data["request_method"]
            referer = data["referer"]
            request_body = data["request_body"]
            body_bytes_sent = data["body_bytes_sent"]

            # MySQLに挿入するクエリを作成
            query = "INSERT INTO errors (id, time, client_ip, url, status_code, request_method, referer, request_body, body_bytes_sent) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)"
            values = (0, time, client_ip, url, status_code, request_method, referer, request_body, body_bytes_sent)
            cursor.execute(query, values)
# 変更をコミットして接続を閉じる
db.commit()
cursor.close()
db.close()
```

テーブルとカラムが存在し、ID(PK)がオートインクリメントであること前提です。