Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

SendGrid Event Webhookをcloudwatch-logs-aggregatorでメトリック化する(前編)

arthur-1 Mackerel Advent Calendar 2023ラソン16日目の記事です。(12/16に投稿予定でしたが都合がつかず本日12/18の投稿となります。)

cloudwatch-logs-aggregator

Mackerelはcloudwatch-logs-aggregatorというモニタリングのための便利ツールを公開しています。Amazon Cloudwatch Logsのログを集計してMackerelにサービスメトリックとして投稿できるツールです。

github.com

Terraformのmoduleとして公開されているので、どんなログをどのように集計してメトリック化するか決めてしまえば、あとはTerraform moduleに具体的な設定をvariableとして渡してあげるだけで必要なリソースや設定を作成して動かすことが可能です。

今回はこのツールの具体的な利用例として、SendGrid Event Webhookの情報をサービスメトリック化してMackerelに投稿する事例をご紹介します。

作ったもの

SendGridのEvent Webhookとは、メールの配信やユーザーの行動に関するイベントを指定したURLに送信し続けることができる機能です。

sendgrid.kke.co.jp

このWebhookの情報から、以下のように配信に関するイベントの発生件数をメトリック化するものを作りました。

Mackerelにメトリックとして投稿することで、メール送信が一定数滞留しているときにアラートを出すといったことが可能になります。また、SendGridのダッシュボードではこういった統計情報が1時間ごとの粒度でしか見れないのですが、この仕組みでは1分粒度のメトリックとして閲覧することが可能になります。よりリアルタイムにモニタリングできるというわけです。

ソースコードはこちらになります:

github.com

構成

今回のアプリケーションは主に2つのmoduleで構成されています:

sendgrid-webhook-receiverはSendGridのWebhookを受け取るHTTPサーバで、受け取ったイベントごとの総数をCloudwatch Logsに構造化ログとして書き込む役割を担っています。今回はAPI Gateway (HTTP API)とLambdaを利用して作りました。

sendgrid-webhook-logs-aggregatorは、書き込まれたログを一定間隔で集計してMackerelにサービスメトリックとして投稿する役割を担っています。この部分は特に新しくアプリケーションを書き起こしたわけでなく、mackerelio-labs/mackerel-monitoring-modulesで配布されているcloudwatch-logs-aggregatorのmoduleに設定を加えてapplyしただけのものになっています。

もし監視したい先のアプリケーションがすでにあって、同じようにcloudwatch-logs-aggregatorを使いたいだけなら、差分としてはリポジトリの以下のファイルだけがあれば同じような仕組みを作れるはずです。

  • terraform/main.tf
  • terraform/sendgrid-webhook-logs-aggregator/main.tf
  • terraform/sendgrid-webhook-logs-aggregator/variables.tf

使い方

本アプリケーションを動かしてみたい方は以下のようにすると良いでしょう。Go 1.21.4及びTerraform 1系が必要です。(実験的な実装であり本番環境での利用はおすすめしません。)

リポジトリのclone

git clone https://github.com/Arthur1/sendgrid-events-to-mackerel.git
cd sendgrid-events-to-mackerel

Lambdaファイルのbuild

make build/sendgrid-webhook-receiver-lambda/lambda.zip

Terraformの適用

cd terraform
vim main.tf

「# 以下の値は利用者の環境に合わせて設定すること」の部分を書き換えます。

module "sendgrid_webhook_logs_aggregator" {
  source                = "./sendgrid-webhook-logs-aggregator"
  region                = local.region
  target_log_group_name = module.sendgrid_webhook_receiver.lambda_log_group_name
  # 以下の値は利用者の環境に合わせて設定すること
  # メトリックとして投稿する先のMackerelのサービス名
  mackerel_service_name = "Hoge"
  # MackerelのAPIキーが格納されているAWS Systems Managerパラメータストアのパラメータ名
  mackerel_api_key_name = "/mackerel.io/hogehoge/apikey"
}

その後、terraform applyでリソースが作成されます。

terraform init
terraform apply

AWSのコンソールなどから、API GatewayのURLを取得しておきましょう。

SendGrid Event Webhookの送り先に登録

SendGridのコンソールの左メニューから「Mail Settings」→Webhook Settingsブロックの中の「 Event Webhooks」と遷移します。

Create new webhooksボタンを押して、以下のように設定します。URLを入力するところでは、先ほど取得したAPI GatewayのURLに /sengrid/events とパスをつけて入力してください。

ここまでできたら、配信イベントが起きるたびに定期的にWebhookが送信され、最終的にイベントをメトリックにしてMackerel上で閲覧できるようになるはずです。

実装の簡単な解説

sendgrid-webhook-receiverのサーバーの中で、受け取ったWebhookのデータの中身を見て、イベント種別ごとのイベント数を数えて構造化ログとして出力しています。Go言語のバージョン1.21以降ではgolang.org/x/exp/slogパッケージを利用することで構造化ログの出力ができます。

internal/server/handler.go#L45-L52

logger.Info(
  "sendgrid delivery events count",
  "processedCount", eventsCountByType.DeliveryProcessed,
  "droppedCount", eventsCountByType.DeliveryDropped,
  "deliveredCount", eventsCountByType.DeliveryDelivered,
  "deferredCount", eventsCountByType.DeliveryDeferred,
  "bounceCount", eventsCountByType.DeliveryBounce,
)
{
  "time":"2023-12-18T00:00:00.0000+09:00",
  "level":"INFO",
  "msg":"sendgrid delivery events count",
  "processedCount":3,
  "droppedCount":0,
  "deliveredCount":3,
  "deferredCount":0,
  "bounceCount":0
}

(読みやすいように改行やインデントを入れています。)

これを集計するCloudwatch Logs Insightsのクエリは以下の通りです。

terraform/sendgrid-webhook-logs-aggregator/main.tf#L25-L28

filter level = "INFO" and msg = "sendgrid delivery events count"
| stats sum(processedCount) as `~processed`, sum(droppedCount) as `~dropped`, sum(deliveredCount) as `~delivered`, sum(deferredCount) as `~deferred`, sum(bounceCount) as `~bounce`

たとえば、先ほど挙げた例と同じログが2件あったら

{
  "~processed": 6,
  "~dropped": 0,
  "~delivered": 6,
  "~deferred": 0,
  "~bounce": 0
}

という結果が得られるでしょう。このような集計を1分ごと行ってMackerelにメトリックとして投稿しているというわけです。

また、Webhookからイベントが全く送信されずログが存在しない場合、メトリックが途切れたのではなくイベントの数が0件だとみなしたいはずです。そうしたい場合には以下のようにメトリックのデフォルト値を指定して0埋めすることができます。

terraform/sendgrid-webhook-logs-aggregator/main.tf#L30-L36

  default_metrics = {
    "${local.metric_name_prefix}.delivery_events.processed" = 0
    "${local.metric_name_prefix}.delivery_events.dropped"   = 0
    "${local.metric_name_prefix}.delivery_events.delivered" = 0
    "${local.metric_name_prefix}.delivery_events.deferred"  = 0
    "${local.metric_name_prefix}.delivery_events.bounce"    = 0
  }

次回予告

次回の後編では、なぜcloudwatch-logs-aggregatorを使うのか、cloudwatch-logs-aggregatorに求める改良案などを書いていきます。