API リクエストを BigQuery でいい感じに集計してみた

本記事では弊チームで新たに導入したモニタリング構成について紹介します。
**BigQuery を使って API リクエストを集計する構成**で可視化した際の知見を記載してみました。

## 想定読者

* GCP 環境で**複数の集計軸でモニタリングをしたい**と考えている方
    * サービス全体の可用性だけでなく **API x 特定の HTTP Header ごとの可用性**を可視化したい、など
    * 今回は例として **API x UserAgent** で集計しましたが、その他にも **API x IP Address** で集計することも可能です[^1]

## 背景・モチベーション

弊チームでは社内外のプロダクトから利用される共通プラットフォームを開発しています。
もともと API のリクエストをもとにプラットフォーム全体の Availability や Latency をモニタリングしていたのですが、全体の結果だけでは下記のような気になる動きを見せた場合に別途調査が必要。という状況でした。

* 瞬間的にリクエスト数が増えたが、実際**どの API のリクエスト数**が増えたの？
* 可用性が低下した場合に、**社内のどのプロダクトのチームに影響**があるの？

これを改善するために、追加で監視を導入することになりました。

## 課題

もともと使用していたこともあり、当初は Grafana の機能を使用して集計しようとしました。
ですが、

* **API 単位で連携されているデータ**を **API x 特定の HTTP Header の形式**で集計するにはどうすれば..？
* **API の PATH に含まれるプレースホルダの値**を無視して集計するにはどうすれば..？

と Grafana の機能だけで実現するには少し難易度の高い面がありました。
今回は上記のような課題を BigQuery にロジックを集約する形で解決しています。

## サービス構成

下図の構成で実現しました。 BigQuery との連携の容易さから BI には Data Portal を使用しています。
簡単な処理の流れとしては下記です。

1. Load Balancing のログを Log Router で BigQuery に転送
1. BigQuery の View を使用し加工・集計
1. Data Portal に連携し可視化

<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378910/b1112403-ec51-e528-cae3-eba6d95bbbe8.png">

## View(BigQuery)

BigQuery の View はデータ加工と集計の二段構えで実装しました。
例として下記の内容を想定したクエリを記載します。

* ゴール
    * 直近1日の **API x UserAgent[^2] ごと**に **5xx レスポンスを返却した割合**を算出する
* クエリ詳細
    * API は下記の条件で**同一リクエスト**とみなし集計する
        * HTTP Method が異なる場合、**別の API リクエスト**とみなす
        * PATH に含まれる ID 等の変数は無視し、変数が異なる場合も**同一の API リクエスト**とみなす
            * 今回は ID に UUID を使用している想定で集計します
    * **5xx レスポンスを返却した割合**は分単位で集計する
        * 荒く見たい場合(日単位、等)は Data Portal 側で**分単位** -> **日単位**に変更することが可能
* 補足
    * クエリ中の変数にはそれぞれ適した値を使用ください
        * `<PROJECT_ID>`: GCP のプロジェクト ID
        * `<BIGQUERY_DATASET_NAME>`: Log Router が連携している BigQuery のデータセット名
        * `<BIGQUERY_TABLE_NAME>`: Log Router が連携している BigQuery のテーブル名

１. 加工用の View(`base_view`)

```sql
SELECT
  httpRequest.status AS status_code,
  httpRequest.userAgent AS user_agent,
  TIMESTAMP_TRUNC(timestamp, MINUTE) AS minute_trunc_timestamp,
  FORMAT("%s %s",
    httpRequest.requestMethod,
    REGEXP_REPLACE(
      httpRequest.requestUrl,
      r'\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b',
      'UUID'
    )
  ) AS api
FROM `<PROJECT_ID>.<BIGQUERY_DATASET_NAME>.<BIGQUERY_TABLE_NAME>`
WHERE TIMESTAMP_SUB(CURRENT_TIMESTAMP() , INTERVAL 24 HOUR) < timestamp;
```

２. 分単位で集計する View(`aggregate_view`)

```sql
SELECT
  minute_trunc_timestamp,
  api,
  user_agent,
  SUM(IF(status_code >= 400 AND status_code < 500, 1, 0)) AS client_error_count,
  SUM(IF(status_code >= 500, 1, 0)) AS server_error_count,
  SUM(IF(status_code < 400, 1, 0)) AS success_count,
  SUM(IF(status_code is not NULL, 1, 0)) AS total_request_count,
FROM `<PROJECT_ID>.<BIGQUERY_DATASET_NAME>.base_view`
GROUP BY
  minute_trunc_timestamp, api, user_agent
;
```

あとは Data Portal に`aggregate_view`を連携し `server_error_count / total_request_count` の値を可視化すれば完成です:tada:[^3]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378910/ebe196a0-5eee-dc2c-2798-f592cf24218a.png)

## 注意点

* 料金を不要に増やさないよう**パーティションの設定**を推奨します
    * Log Router 作成時にパーティション分割テーブルを設定してください
    * BigQuery で View の作成時にパーティションをなるべく跨がないよう `WHERE` 条件を記載してください
        * 前述のクエリ例では日ごとにパーティションを設定している想定で下記 `WHERE` 句を記載しています
            * `WHERE TIMESTAMP_SUB(CURRENT_TIMESTAMP() , INTERVAL 24 HOUR) < timestamp;`
* **BigQuery の料金監視**を推奨します
    * パーティションを設定していても、その他の要因(リクエスト数の増加等)により BigQuery の料金が増える可能性があります

## その他、本構成で良かった点

本構成は下記の観点から比較的お手軽に実現できたと考えています。
**どういった情報を得られるかわからないけど、ひとまず可視化してみたい。**というユースケースには相性がいいと思います。

* Google のサービスで完結するためデータ連携が容易
    * Datadog や Grafana といったサービスとログのデータを連携させる場合、Cloud Pub/Sub や専用の Plugin を使用する必要がでてくると思いますが、Data Portal では標準機能で GCP と連携することができます
* 無料で使用できる範囲が多く費用を抑えられる
    * Data Portal の使用にお金がかからないため、金額的に気にすべきは BigQuery だけになります
    * BigQuery も無料枠[^4]があり、保存するデータサイズや前述のパーティションに気をつければそこまで増大することはなさそうです

## 今後の展開

今後の展開として下記などの理由から Data Portal ではなく別のサービスに移行することも考えています。

* データ同期のリアルタイム性を向上させたい
* API リクエスト量が増え UI でグラフの拡大・縮小したい

ですが、 BI を変更することになっても、現状 BigQuery に集計ロジックを集約しているため比較的楽に移行できると考えています。

## Special thanks

今回の導入は @i_am_miko さんら弊社 CRE チームのメンバーにも協力してもらいました:tada:

## 参考文献

* [データポータルを使用して BigQuery データを可視化する](https://cloud.google.com/bigquery/docs/visualize-data-studio?hl=ja)

[^1]: [Load Balancing のログ](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) をもとに集計しています。その他のフィールドでも集計することが可能です。
[^2]: 弊社ではサービス識別子を UserAgent に指定する運用となっています。現状社内サービスからの連携が主であり、社内サービスごとに集計するために使用しています。
[^3]: グラフの値は記事用に適当な値を表示しています。
[^4]: [BigQuery の料金](https://cloud.google.com/bigquery/pricing)