SendGrid の送信ログを無料で7日間以上見られるように、BigQuery にリアルタイムで保存する

公開:2020/09/29
更新:2020/09/30
5 min読了の目安(約5000字TECH技術記事

これはなに?

イマチケというAmazon Payでサクッと買える有料ネットライブのチケット販売サービスを開発・運営しています。

そこでは、SendGridを使って購入確認メールを送っているのですが、携帯キャリア系メールアドレスの迷惑メールブロックやら RFC違反のメールアドレスでやらで購入確認メールが送れないことがままあり、お客様からの問い合わせが来たときに調査が必要でした。

調査のために SendGrid のメールアクティビティを確認しますが、とりあえず無料のプランではじめているため、管理UI上で確認できるメール送信ログが7日間しか残りません。しかし、お客様からの問い合わせはチケットを買ってから、2週間後のライブ配信直前などに来ることがあり、調べるころには履歴が残っておらず、メールが届かない原因がわからないということがありました。

SendGrid には メール送信イベントごとの Webhook があるので、手が空いたらこれを Cloud Functions for Firebase で受けて、Firestore を経由して BigQuery に残すようにしました。その手順とコードを残しておきます。

1. Webhook を受ける Cloud Function を用意する

SendGrid から Webhook で POST が来るので、それを Cloud Functions で受けて Firestore に1送信イベントを1ドキュメントとして追加します。

Cloud Functions 受け口のURLは以下のようになります。
https://asia-northeast1-<projectid>.cloudfunctions.net/postMailLog

実際に SendGrid の Webhook URLに設定するURLはBASIC 認証設定を入れた以下のようなURLになります
https://<ユーザ名>:<パスワード>@asia-northeast1-<projectid>.cloudfunctions.net/postMailLog

以下そのソースコードです。typescript で書きました。

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

admin.initializeApp(functions.config().firebase)
const firestore = admin.firestore()

const BASIC_USERNAME = '***'
const BASIC_PASSWORD = '***'

// BASIC 認証
function basicAuth(authorization: string): boolean {
  const parts = Buffer.from(authorization.replace('Basic ', ''), 'base64')
    .toString()
    .split(':')
  if (parts[0] !== BASIC_USERNAME || parts[1] !== BASIC_PASSWORD) {
    return false
  } else {
    return true
  }
}

// Webhook を受けるエントリポイント
export const postMailLog = functions
  .region('asia-northeast1') // 東京リージョン
  .https.onRequest(async (request, response) => {
    try {
      functions.logger.info('postMailLog', request.body)

      if (!basicAuth(request.get('authorization') || '')) {
        throw new Error('basicAuth failed')
      }

      const logRef = firestore.collection('maillogs')
      const events = request.body || []
      for (const event of events) {
        // イベントごとに firestore にドキュメントとして保存
        event.createdAt = admin.firestore.FieldValue.serverTimestamp()
        await logRef.add(event)
        functions.logger.info('maillog added', { event })
      }

      functions.logger.info('postMailLog completed')
      response.status(200).end()
    } catch (err) {
      functions.logger.info('error', err)
      response.status(500).send(err)
    }
  })

2. SendGrid の Webhook を設定する

SendGrid の管理UIにログインして、左側のメニューから Settings -> Mail Settings を選び、出てくる画面の一番上にある "Event Webhook" の鉛筆アイコンをクリックして、Webhook 設定画面を開きます。

SendGrid / Mail Settings

出てくる設定画面に、受け口のWebhook URLを設定し、記録したいイベントにチェックをつけて、保存します。(黒塗りにしてるところは BASIC認証のユーザ名、パスワードです。)

これで一旦 Firestore にほぼリアルタイムで保存されるようになりました。
次は、これを BigQuery に保存します。

3. Firebase Extensionを設定し、BigQuery に連動で自動保存。

Firebase の拡張機能、Export Collections to BigQuery を設定し Firestore に保存されたタイミングで BigQuery に保存します。

拡張機能のカタログ画面から、Export Collections to BigQuery を探して「Install]ボタンをクリックします。

Export Collections to BigQuery

4ステップの設定画面がでてきます。入力項目がでてくる最後のステップまで、そのまま「次へ」で進みます。最後の設定画面では出力する先の BigQuery 上の設定を入力します。

拡張機能を構成

設定項目は以下のとおりに設定しました。

項目 設定 説明
Cloud Functions location Tokyo (asia-northeast1) この拡張機能が動作するリージョンを設定します。あとで変更できません。
Colleciton path maillogs 同期元の Firestore 上にあるコレクションのパスを設定
Dataset ID firestore_export 同期先 BigQuery 上の Dataset を設定
Table ID maillogs 同期先 BigQuery 上のテーブル名を設定

これで、「拡張機能をインストール」をクリックするとインストールが始まります。4分程度待つと完了です。この拡張機能は Firestore のイベントトリガで起動して、BigQuery にドキュメントを保存してくれるものです。

以上で完了です。最終的に SendGrid でメールを送信してから、数秒程度で送信ログが BigQuery に反映される形になります。

4. Datastudio で表示させるために、BigQuery でビューを作成する

あとは、これまた無料のGoogle Dataportalでデータソースにこの BigQuery テーブルを指定して、画面に出してやります。ただ、BigQuery には JSON形式で保存されてしまって取り扱いづらいです。そこで、BigQuery 上にビューを作ってそれをデータソースにすることにしました。ついでにタイムスタンプも日本に設定して見やすくします。

CREATE VIEW imaticket.maillogs AS
SELECT 
  FORMAT_TIMESTAMP('%Y-%m-%d %H:%M:%S', timestamp, 'Asia/Tokyo') as timestamp,
  json_extract_scalar(data, "$.event") as event,
  json_extract_scalar(data, "$.email") as email,
  ifnull(json_extract_scalar(data, "$.response"),json_extract_scalar(data, "$.reason")) as response
FROM imaticket.maillogs_raw_changelog
WHERE
  json_extract_scalar(data, "$.event") is not null

Firestore の拡張機能で保存されるテーブル名には suffix として _raw_changelog というのがついています。それを元に maillogs という VIEW を作りました。

datastudio からはこのビューをデータソースにして、ダッシュボードを作ればokです。

無料になる範囲

  • Cloud Functions for Firebase: 月200万回まで無料です。
  • Firestore: 1日2回まで書き込み無料です。
  • BigQuery: ストレージ毎月10GBまで無料、クエリは毎月1TBまで無料

1回のメール送信で、だいたい平均して2回ちょいの書き込みが行われます (processed, deliveredの2イベント)。上記から、SendGrid の無料枠1万2000通/月なので、その範囲内ならすべて無料でつかえますね。