えいのうにっき

主に Web 系技術ネタ。背景画像 is powered by grass-graph.shitemil.works

いまさらだけど、Nginx が出力したアクセスログ(ltsv形式)を fluentd 経由で BigQuery に送ってみたよ

前回の いまさらだけど fluentd に入門した - えいのうにっき に引き続き、「いまさら fluentd」シリーズ(?)。

blog.a-know.me

事例としてはめちゃくちゃありそうなこの組み合わせ、「自分的にかゆいところ」に手が届いてるかんじの記事がないような気がしたので、それを少しでも増やすことができれば、という思いで、書いてみる。

nginx のアクセスログの形式を ltsv 形式にする

まずはこれから。

そのために nginx の conf に手を加える必要がある。たとえばこんな↓かんじかな。

log_format  ltsv  "local_time:$time_local"
                  "\thost:$remote_addr"
                  "\tforwardedfor:$http_x_forwarded_for"
                  "\treq:$request"
                  "\tstatus:$status"
                  "\tsize:$body_bytes_sent"
                  "\treferer:$http_referer"
                  "\tua:$http_user_agent"
                  "\treqtime:$request_time"
                  "\tcache:$upstream_http_x_cache"
                  "\truntime:$upstream_http_x_runtime"
                  "\tvhost:$host"
                  "\tmethod:$request_method"
                  "\turi:$request_uri";

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

ltsv、 Labeled Tab-serapated Values の略で、上記のような設定だと、下記のようなレコードがログファイルに出力されることになる。

local_time:02/Feb/2016:17:52:29 +0900 host:59.106.108.116 forwardedfor:- req:GET / HTTP/1.1 status:200 size:15488 referer:- ua:mackerel-http-checker/0.0.1 reqtime:0.035 cache:- runtime:- vhost:home.a-know.me method:GET uri:/

このアクセスログファイルを、fluentd の in_tail プラグインを使って随時読み込み、BigQuery に送りたいわけなんだけど。

2016-02-03 追記

LTSV において使用するラベルには、推奨されるものが http://ltsv.org/ で定められている、とのこと、コメントでご指摘頂きました。…し、しらなかった…! 皆様はご注意あれ!

in_tail プラグインの設定を行う

こんな↓かんじ。

<source>
  type tail
  format ltsv
  time_format %Y-%m-%d %H:%M:%S %z
  path /var/log/nginx/home.a-know.me.access.log
  pos_file /var/log/td-agent/home.a-know.me.access_log.pos
  tag log.nginx
</source>

いまさらだけど fluentd に入門した - えいのうにっき でも勉強したように、入力プラグインからのひとつのイベント・メッセージは、 [ time, tag, record ] という配列で構成され、これが fluentd のルーティングエンジンに送信されることになる。

なので、今回の場合は↓のようなイベントが送信されることになる。

2016-02-02T09:12:26+09:00<TAB>log.nginx<TAB>{"local_time":"02/Feb/2016:19:04:31 +0900","host":"59.106.108.116","forwardedfor":"-","req":"GET / HTTP/1.1","status":"200","size":"71808","referer":"-","ua":"mackerel-http-checker/0.0.1","reqtime":"0.036","cache":"-","runtime":"-","vhost":"home.a-know.me","method":"GET","uri":"/"}

投入先のテーブル名に年月をつける、そのための準備

BigQuery は、その性格的に、あまり気にせずクエリを投げるのがいいんだけど、かといってその探索対象が大きくなると、その料金が気になってくる。 そのため、どうしても肥大しがちなログなんかは投入先のテーブルをある単位で分割しておくのが良い。(その気になれば、複数テーブルを対象に SELECT できる)

その目的として、今回は下記のようなディレクティブをひとつ挟んだ。

<match log.nginx>
  type record_reformer
  enable_ruby true
  tag ${tag}.${time.strftime('%Y%m')}
</match>

要、 fluent-plugin-record-reformer 。tag の書き換えを目的として使う。書き換え後の tag は、例えば log.nginx.201602 のようになる。このタグの末尾の数字を、テーブル名に使う魂胆。

後述する fluent-plugin-bigquery は、それ単体で、書き出し先テーブル名に年月を加える、くらいのことはできるので、fluent-plugin-bigquery の README を見てみても良いと思う。

BigQuery 側の準備

ここらで BigQuery サイドでも、以下の様な準備を予めしておく必要がある。

  • GCP project の作成
  • サービスアカウントの作成
  • dataset の作成

これらについては、FluentdでGoogle BigQueryにログを挿入してクエリを実行する - Qiita がとても明るいので、ぜひ参考にすると良いと思う。 ただテーブルについては、今回は年月ごとに動的に作成することになるので、手動では作成しないこと。

qiita.com

fluent-plugin-bigquery の設定

今回、下記のような設定を行った。(ゆくゆく、同内容を別の場所にも投入したくて fluent-plugin-forest , copy プラグインも合わせて使っている。)

<match log.nginx.**>
  type forest
  subtype copy
  <template>
    <store>
      type bigquery
      method insert

      auth_method json_key
      json_key /etc/td-agent/.keys/nginx-log-to-bigquery-jsonkey.json

      project a-know-home
      dataset centrage_nginx_log

      flush_interval 1
      buffer_chunk_records_limit 1000
      buffer_queue_limit 1024
      num_threads 16

      auto_create_table true
      table nginx_access_log_${tag_parts[-1]}

      time_format %s
      time_field time
      schema_path /etc/td-agent/settings/nginx_access_log_schema.json
    </store>
  </template>
</match>

要、fluent-plugin-bigquery 。以下、ひとつずつに関してメモ。

json_key

BigQuery 側の準備 でサービスアカウントを作成したが、その認証情報(json形式)。それを、サーバ内の任意の場所に置いておき、そのパスを指定する。

project

GCP には project idproject number があって非常にややこしいんだけど、前者の方で ok.

dataset

BigQuery 側の準備 で BigQuery 上に作成した、dataset 名。

auto_create_table

必要に応じて、動的にテーブルを作成したい場合は、これに true を指定する。

table

投入先のテーブル名を指定する。 nginx_access_log_201602 のようなテーブルを作りたいので、上記のような指定。 tag_parts プレースホルダは、 fluent-plugin-forest によって使えるようになったもの。

schema_path

投入先のテーブルのスキーマjson 形式のファイルとして用意しておくんだけど、そのパスを指定する。 今回だと、以下の様なファイルを作成しておいた。

[
  { "name": "time",    "type": "timestamp" },
  { "name": "local_time",    "type": "string" },
  { "name": "host",  "type": "string"  },
  { "name": "forwardedfor",  "type": "string"  },
  { "name": "req",  "type": "string"  },
  { "name": "status",    "type": "integer"  },
  { "name": "size",  "type": "integer"  },
  { "name": "referer",  "type": "string"  },
  { "name": "ua",    "type": "string" },
  { "name": "reqtime",   "type": "float" },
  { "name": "cache",    "type": "string" },
  { "name": "runtime",   "type": "float" },
  { "name": "vhost", "type": "string" },
  { "name": "method",    "type": "string" },
  { "name": "uri",    "type": "string"  }
]

name に指定する文字列は、nginx の ltsv ログファイル内の各ラベルに一致している必要がある。順番は無関係(なはず)。 よくみると、ログファイルには指定していない time があるんだけど、これについては↓で。

time_format time_field

これが少しややこしかった。個人的には以下の様な理解なんだけど、違っていれば教えて欲しい m(_ _)m

たとえば、テーブル定義に abc があって、そこに [time, tag, record] の time を入れたいときは、 time_field abc と書く。そのときのフォーマットの指定を、 time_format でできる。というかんじ。

FluentdでGoogle BigQueryにログを挿入してクエリを実行する - Qiita では BigQueryにはTIMESTAMP型は存在しないため、unix epochを指定する。unix epoch形式で挿入しておけばクエリで後から変換が可能だ とあるんだけど、今の BigQuery には TIMESTAMP型は上述の json ファイルの通り、存在するので、今回は time_format %s で取得したものを time_field time とすることで TIMESTAMP 型な time カラムに投入している。それにより、クエリの結果もいいかんじに表示される。

なので、nginx の ltsv アクセスログファイルの時点で time というラベルを付けて出力してやるのなら、 time_field の指定はいらない、はず。一応 fluent-plugin-bigquery の当該コードを読んで、その指定が無い場合の考慮もされているように見えたので、そのはず。

2016-02-04 追記

time 周りに関して、上記のような理解のまま、今度は Rails のアプリケーションログを BigQuery に送ろうと、 Rails のアプリケーションログも BigQuery に送る by a-know · Pull Request #58 · a-know/a-know-home-server · GitHub で作業を進めていた。

github.com

が、自分の理解したようにはならず。ただ、そのときの挙動(警告メッセージ)から、in_tail プラグインの time_format って、format ltsv のときは ltsv の time を time_format のフォーマットに従って変換して、それを [time, tag, record] の time として扱うのかな と思った。まだ in_tail プラグインのソースを見てないから、想像になっちゃうけど。

ただ、そう考えた上での修正を実施したところ、意図通りの挙動をしてくれたので、そうなのかな、と思っているイマココ。あとでまた確認したいところだけど。

2016-02-10 追記

format ltsv などとしたときの parser は、デフォルトでは record 内の time フィールドを除去してしまうそう。そのため、これを残したい場合は keep_time_key を指定すれば良かった。

Parser removes time field from event record by default. If you want to keep time field in record, set true to keep_time_key. Default is false. - keep_time_key / tail Input Plugin | Fluentd

参考までに、この機能が入れられたときの PR 。

github.com

2017-03-15 追記

さらに追記。以下のようなメモを見つけた。

fluentd で time というキーを in_tail -> out_xxx で出力したい場合の注意点 · GitHub

include_time_key true かつ time_key を time 以外にする 。なるほど。ここらへん、奥が深い。。。

結果

こんな↓かんじで、nginx のアクセスログをクエリで検索できるようになった。

f:id:a-know:20160202200833p:plain

まとめ

…とまぁ、そんなかんじのことを、nginx のアクセスログを fluentd 経由で BigQuery に投げてみる by a-know · Pull Request #54 · a-know/a-know-home-server · GitHub の PullRequest(ぼっちプロジェクト・chef 使用)でも試行錯誤の過程も含め、残しているので、この記事と合わせて、参考にしてもらえればと。

github.com

参考リンク

関連エントリ

blog.a-know.me

blog.a-know.me

blog.a-know.me



follow us in feedly