nginxログをFluentdとNew Relicで見える化する

記事タイトルとURLをコピーする

こんにちは、マネージドサービス部 大城です。

リバースプロキシ(今回の場合nginx)のログをNew Relic Logsに連携して見える化してみます。

New Relic APMを入れたらデフォルトでログが自動転送されるので便利です。APMが対応している言語であればAPMを入れるのが手っ取り早いです。

ただ、APMが対応していない言語やアプライアンス製品などAPMを入れるのが難しい環境は、リバースプロキシ(今回の場合nginx)があればログをNew Relic Logsに連携することである程度見える化することができます。制限がある環境で見える化したい場合の参考にしてください。

環境準備

今回もisucon13の再現環境を利用させていただきました。前回のブログの内容でAWS環境にオールインワンを構築します。

sshした後に「isucon」ユーザーにスイッチします。ターミナル上は基本的に「isucon」ユーザーで操作することになります。言語はpythonを使うので切り替えておきます。

sudo -i -u isucon
sudo systemctl disable --now isupipe-go.service
sudo systemctl enable --now isupipe-python.service

やってみた

Fluentdのインストール

Before Installation を参照してカーネルパラメータなどを設定します。

今回の環境はある程度設定されていたので、追加設定は /etc/sysctl.conf のみでした。環境によって違いますのでよく読んでから設定してください。

sudo vi /etc/sysctl.conf

/etc/sysctl.conf

net.core.somaxconn = 1024
net.core.netdev_max_backlog = 5000
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_wmem = 4096 12582912 16777216
net.ipv4.tcp_rmem = 4096 12582912 16777216
net.ipv4.tcp_max_syn_backlog = 8096
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
# If forward uses port 24224, reserve that port number for use as an ephemeral port.
# If another port, e.g., monitor_agent uses port 24220, add a comma-separated list of port numbers.
# net.ipv4.ip_local_reserved_ports = 24220,24224
net.ipv4.ip_local_reserved_ports = 24224

sysctlコマンドで反映させます。

sudo sysctl -p

Install by DEB Package (Debian/Ubuntu) を参照しFluentdをインストールします

curl -fsSL https://toolbelt.treasuredata.com/sh/install-ubuntu-jammy-fluent-package5-lts.sh | sh

Fluentd plugin for log forwarding を参照しNew Relicのプラグインをインストールします

sudo fluent-gem install fluent-plugin-newrelic

Fluentdの設定ファイルを作成します

sudo vi /etc/fluent/fluentd.conf

/etc/fluent/fluentd.conf

#Tail one or more log files
<source>
  @type tail
  <parse>
    @type none
  </parse>
  path /var/log/nginx/access.log
  tag example.service
</source>

#Add hostname and service_name to all events with "example.service" tag
<filter example.service>
  @type record_transformer
  <record>
    service_name ${tag}
    hostname "#{Socket.gethostname}"
  </record>
</filter>

#Forward all events to New Relic
<match **>
  @type newrelic
  license_key xxxxxxxxxxxxxxxxxxNRAL
</match>

Linux上のfluentdユーザーにnginxのログの読み取り権限を与えます

sudo chmod o+r /var/log/nginx/access.log

Fluentdを再起動します

sudo systemctl restart fluentd.service

この状態で負荷ツールを実行してログを流してみます。

cd
./bench run --enable-ssl

New Relic Logsにログが連携されました。

ログを構造化する

一旦は連携されるようになりましたが、もう少し工夫してみます。現在のNew Relic Logsの値は下の通りになっています。(不要な部分は削除しています)

New Relic Logsのデータ

{
  "hostname": "ip-10-200-1-171",
  "message": "127.0.0.1 - - [30/Apr/2024:09:42:30 +0000] \"GET /api/livestream/search?limit=50 HTTP/1.1\" 499 0 \"-\" \"isucandar\"",
  "service_name": "example.service",
  "timestamp": 1714470150618
}

この状態だと message に解析したい情報が集約されており、扱いにくいので分解して解析しやすいようにします。

nginxのログ形式を ltsv に変更します。

sudo vi /etc/nginx/nginx.conf

/etc/nginx/nginx.conf

# (不要な箇所は削除しています)
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
}

http {
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;
    log_format ltsv 'time:$time_iso8601\t'
                    'remote_addr:$remote_addr\t'
                    'request_method:$request_method\t'
                    'request_length:$request_length\t'
                    'request_uri:$request_uri\t'
                    'https:$https\t'
                    'uri:$uri\t'
                    'query_string:$query_string\t'
                    'status:$status\t'
                    'bytes_sent:$bytes_sent\t'
                    'body_bytes_sent:$body_bytes_sent\t'
                    'referer:$http_referer\t'
                    'useragent:$http_user_agent\t'
                    'forwardedfor:$http_x_forwarded_for\t'
                    'request_time:$request_time\t'
                    'upstream_response_time:$upstream_response_time';
    access_log /var/log/nginx/access.log ltsv;
    error_log /var/log/nginx/error.log;
    gzip on;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

nginxを再起動します

sudo systemctl restart nginx.service

/etc/fluent/fluentd.conf の @type を none -> ltsv に修正します。この設定で key:value の形でパースして連携してくれます。

sudo vi /etc/fluent/fluentd.conf

/etc/fluent/fluentd.conf

#Tail one or more log files
<source>
  @type tail
  <parse>
    @type ltsv
  </parse>
  path /var/log/nginx/access.log
  tag example.service
</source>

#Add hostname and service_name to all events with "example.service" tag
<filter example.service>
  @type record_transformer
  <record>
    service_name ${tag}
    hostname "#{Socket.gethostname}"
  </record>
</filter>

#Forward all events to New Relic
<match **>
  @type newrelic
  license_key xxxxxxxxxxxxxxxxxxNRAL
</match>

sudo systemctl restart fluentd.service

この状態で再度ログ転送してみます

cd
./bench run --enable-ssl

New Relic Logs上のデータが key:value の形で構造化され解析しやすくなりました。

New Relic Logs上のデータ

{
  "body_bytes_sent": "57049",
  "bytes_sent": "57214",
  "forwardedfor": "-",
  "hostname": "ip-10-200-1-171",
  "https": "on",
  "query_string": "-",
  "referer": "-",
  "remote_addr": "127.0.0.1",
  "request_length": "720",
  "request_method": "GET",
  "request_time": "0.097",
  "request_uri": "/api/user/qsato1/icon",
  "service_name": "example.service",
  "status": "200",
  "timestamp": 1714470881000,
  "upstream_response_time": "0.100",
  "uri": "/api/user/qsato1/icon",
  "useragent": "isucandar"
}

この状態だと下のようなNRQLで集計することができます

StatusCode別の集計
FROM Log select count(*) facet status SINCE 10 minutes ago

リクエスト多いURI TOP10
FROM Log select count(*) facet request_uri
SINCE 10 minutes ago LIMIT 10

user_idをログに埋め込む

もう少し解析しやすいように、ログに user_id 情報を埋め込んでみます。

フレームワークによって方法が違いますが、今回Python、Flaskで試してみます。同じようなことは大体のフレームワークでもできると思います。

レスポンスヘッダーに userid 情報を埋め込むようにコード追加します。

参考

How to configure a Flask app to log user id and session id in nginx logs | by Surya Sankar | Medium

Flaskでヘッダー情報をafter_requestで設定する | kosh.dev

vi /home/isucon/webapp/python/app.py
# 下記を追加
@app.after_request
def after_request(response):
    response.headers['X-Userid'] = session.get(Settings.DEFAULT_USER_ID_KEY)
    return response

アプリを再起動します

sudo systemctl restart isupipe-python.service

nginxのログに upstream_http_x_userid を出力するよう設定します

sudo vi /etc/nginx/nginx.conf

/etc/nginx/nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
}

http {
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;
    log_format ltsv 'time:$time_iso8601\t'
                    'remote_addr:$remote_addr\t'
                    'request_method:$request_method\t'
                    'request_length:$request_length\t'
                    'request_uri:$request_uri\t'
                    'https:$https\t'
                    'uri:$uri\t'
                    'query_string:$query_string\t'
                    'status:$status\t'
                    'bytes_sent:$bytes_sent\t'
                    'body_bytes_sent:$body_bytes_sent\t'
                    'referer:$http_referer\t'
                    'useragent:$http_user_agent\t'
                    'forwardedfor:$http_x_forwarded_for\t'
                    'request_time:$request_time\t'
                    'upstream_response_time:$upstream_response_time\t'
                    'x_userid:$upstream_http_x_userid';
    access_log /var/log/nginx/access.log ltsv;
    error_log /var/log/nginx/error.log;
    gzip on;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

nginxを再起動します

sudo systemctl restart nginx.service

この状態で再度ログ転送してみます

cd
./bench run --enable-ssl

New Relic Logs上のデータに x_userid が追加されました。

New Relic Logsのデータ

{
  "body_bytes_sent": "52857",
  "bytes_sent": "53124",
  "forwardedfor": "-",
  "hostname": "ip-10-200-1-171",
  "https": "on",
  "query_string": "-",
  "referer": "-",
  "remote_addr": "127.0.0.1",
  "request_length": "509",
  "request_method": "GET",
  "request_time": "0.032",
  "request_uri": "/api/user/chiyo890/icon",
  "service_name": "example.service",
  "status": "200",
  "timestamp": 1714472779000,
  "upstream_response_time": "0.032",
  "uri": "/api/user/chiyo890/icon",
  "useragent": "isucandar",
  "x_userid": "1017"
}

この状態だと下のようなNRQLで集計することができます

利用が多いuseridの集計
SELECT count(*) FROM Log
FACET x_userid LIMIT 10 SINCE 10 minutes ago

特定の userid のリクエスト履歴とレスポンスタイム
SELECT x_userid,request_uri,upstream_response_time
FROM Log WHERE x_userid = 1011 LIMIT 50 SINCE 1 day ago 

lookupテーブルの活用

もう少し頑張って lookup テーブルを試してみます。今回は users テーブルの id,name カラムでcsvを作って lookup テーブルに追加してみます。

注意点

New Relicさんのブログにも記載がありますが、アップロードしたテーブル情報は難読化されないため、機密性の高い情報はアップロードしないほうが無難です。今回はテスト用データなのでユーザー名をアップロードしていますが、実際の環境で利用する場合はデータの機密性を充分に検討してから活用してください。

参考

Lookupテーブルでダッシュボードをリッチに+α - Speaker Deck

CSVを取り込んだデータをNRQLで組み合わせる「LookupTable」 #初心者 - Qiita

MySQLで--secure-file-privというエラーが出たときのメモ - willwealth’s diary

MySQLでSELECTしたデータをCSVファイルに出力してみよう!【MAC環境】 | ポテパンスタイル

csv出力

sudo mysql isupipe

mysql> select id,name from users order by id limit 10;
+----+------------------+
| id | name             |
+----+------------------+
|  1 | test001          |
|  2 | ayamazaki0       |
|  3 | yoshidamiki0     |
|  4 | hidekimurakami0  |
|  5 | akemikobayashi0  |
|  6 | eishikawa0       |
|  7 | tomoya100        |
|  8 | kobayashiminoru0 |
|  9 | yamadakaori0     |
| 10 | tanakahanako0    |
+----+------------------+
10 rows in set (0.00 sec)

mysql> select @@secure_file_priv;
+-----------------------+
| @@secure_file_priv    |
+-----------------------+
| /var/lib/mysql-files/ |
+-----------------------+
1 row in set (0.00 sec)

mysql>  select id,name from users into outfile '/var/lib/mysql-files/users.csv' fields terminated by ',' enclosed by '"' escaped by '"' lines terminated by '\r\n';
Query OK, 1095 rows affected (0.00 sec)

/var/lib/mysql-files/users.csv の1行目に id,name のヘッダーをつけて lookup テーブルにアップロードします

この状態で再度ログ転送してみます

cd
./bench run --enable-ssl

アップロードしたlookupテーブルを結合して見える化することができます。

利用が多いユーザー名の集計
FROM Log JOIN (FROM lookup(users) select id,name)
ON numeric(x_userid) = id
SELECT count(*)
FACET name
since 20 minutes ago limit 10

ユーザー名で解析することができるようになりました。lookupテーブル便利ですね。

さいごに

ここまで読んでいただき、ありがとうございます。この記事が誰かの役に立ったら幸いです。

数年前 Fluentd + OpenSearch + Kibana が好きで見える化にハマっていた時期がありました。同じことがNew Relicでも容易にできます。試してみてください。

大城 慶明 (記事一覧)

マネージドサービス部

2022年10月サーバーワークスに入社、沖縄からリモート勤務。AWSを勉強中。沖縄では大城が多いので「よっさん」と呼ばれています。AWS14冠。NRUG沖縄支部運営。X @yo_ohshiro