クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Postmaster Toolsの迷惑メール率をDatadogで監視する

OGP

こんにちは。エンジニアの砂川です。

2023年10月頃にGoogleYahoo!から新しいメール送信者ガイドラインが出されました。 該当するメール送信者は、ガイドラインに沿っているか確認をし、沿っていない場合は対応する必要があります。

blog.google

support.google.com

対応完了しきった皆様、お疲れ様でした。 絶賛対応中の方々、頑張っていきましょう。

ところで皆さんGmailのPostmaster Toolsの迷惑メール率の監視はいかがでしょうか?

クラウドワークスではGmailガイドラインで定めているPostmaster Toolsの迷惑メール率を監視し、閾値を超えるとアラートを通知するような仕組みを作り、運用をしています。

この記事では、その仕組みをご紹介します。

なぜ作ったのか

  • 定期的にPostmaster Toolsにログインして監視するのが面倒
  • (サブ)ドメイン毎の迷惑メール率を確認する時、切り替えが面倒

これらの面倒さを解決するために、各ドメインのメトリクスを一覧できるようDatadogのダッシュボードとSlack通知をするような仕組みを作りました。

全体像

仕組みの全体像
仕組みの全体像

全体像としては上記のように定期的にPostmaster Tools APIで迷惑メール率等を取得し、Datadogに送信、監視をしてSlack通知をしています。

定期的に実行する処理部分は、Go言語で実装しました。

Postmaster Tools APIを利用する準備

  1. Google Cloudでプロジェクト作成
  2. サービスアカウントを払い出し(メールアドレスが発行されます)
  3. Postmaster Toolsのユーザー管理にて、監視したいドメインサブドメイン毎に発行されたメールアドレスを登録
  4. 払い出したサービスアカウントのキーを発行(JSON

払い出したキーはJSONファイルとなりますが、本番環境では環境変数で扱いたいので、ローカル開発環境では以下のように環境変数を設定して開発します。

export GOOGLE_APPLICATION_CREDENTIALS_JSON=$(cat ./credentials.json | jq -c '.')

Postmaster Tools APIを利用して迷惑メール率を取得

Postmaster Tools APIを利用して迷惑メール率を取得するコードは以下のようになります。

※一部実装を端折っています

package postmastertools

import (
    "context"
    "fmt"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/gmailpostmastertools/v1"
    "google.golang.org/api/googleapi"
    "google.golang.org/api/option"
    "os"
    "time"
)

// OkTrafficStat はPostmasterTools APIで取得に成功し、利用する値だけを抽出したドメインごとの値
type OkTrafficStat struct {
    // ドメイン
    // 例: domains/crowdworks.jp
    DomainName string
    // 迷惑メール率
    // 例: 0.001 -> 0.1%
    UserReportedSpamRatio float64
}

// NgTrafficStat はPostmasterTools APIで取得に失敗したドメインごとの理由
type NgTrafficStat struct {
    // ドメイン
    // 例: domains/crowdworks.jp
    DomainName string
    // 失敗した理由
    // 例: "notFound", "badRequest"
    Reason string
}

// FetchResult はPostmaster Tools APIで取得したデータの集合
type FetchResult struct {
    OkTrafficStats []OkTrafficStat
    NgTrafficStats []NgTrafficStat
}

// FetchPostmasterTrafficStats はPostmaster ToolsのAPIを叩いた結果を返す
// 1. 環境変数 GOOGLE_APPLICATION_CREDENTIALS_JSON にある client_email が登録されているドメインのリストを取得
// 2. ドメイン毎にAPIを叩いた結果をスライスに詰め込み、全体の結果を返す
// TrafficStatの取得に失敗(2**系以外)でも、早期リターンせずに失敗としてスライスに詰め込んでいる理由
// - そのドメインで、対象日にメールが1通も送信されていなかったりしてデータが無いと404が返ってくる(これで早期リターンをしてしまうと、他ドメインのTrafficStatが取得できない)
// - 全ドメイン分のTrafficStatを一旦取得するため
func FetchPostmasterTrafficStats(targetDate time.Time) (*FetchResult, error) {
    ctx := context.Background()
    creds, err := google.CredentialsFromJSON(ctx, []byte(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")), "https://www.googleapis.com/auth/postmaster.readonly")
    if err != nil {
        return nil, fmt.Errorf("環境変数 GOOGLE_APPLICATION_CREDENTIALS_JSON からクレデンシャル作成に失敗: %v", err)
    }
    gmailpostmastertoolsService, err := gmailpostmastertools.NewService(ctx, option.WithCredentials(creds))
    if err != nil {
        return nil, fmt.Errorf("NewService作成に失敗: %v", err)
    }

    domains, err := gmailpostmastertoolsService.Domains.List().Do()
    if err != nil {
        return nil, fmt.Errorf("DomainのList取得に失敗: %v", err)
    }

    targetDateStr := targetDate.Format("20060102")
    var okTrafficStats []OkTrafficStat
    var ngTrafficStats []NgTrafficStat
    for _, d := range domains.Domains {
        parent := d.Name + "/trafficStats/" + targetDateStr
        trafficStat, err := gmailpostmastertoolsService.Domains.TrafficStats.Get(parent).Do()
        if err != nil {
            ngTrafficStats = append(ngTrafficStats, NgTrafficStat{
                DomainName: d.Name,
                Reason:     err.(*googleapi.Error).Errors[0].Reason,
            })
        } else {
            okTrafficStats = append(okTrafficStats, OkTrafficStat{
                DomainName:            d.Name,
                UserReportedSpamRatio: trafficStat.UserReportedSpamRatio,
            })
        }
    }
    return &FetchResult{
        OkTrafficStats: okTrafficStats,
        NgTrafficStats: ngTrafficStats,
    }, nil
}

Postmaster Tools API の詳細は公式のドキュメントを御覧ください。

developers.google.com

Datadogへカスタムメトリクスとして送信

Datadogへカスタムメトリクスを送信するコードは以下のようになっています。

※一部コメントや実装を端折っています

package datadog

import (
    "context"
    "fmt"
    "github.com/DataDog/datadog-api-client-go/v2/api/datadog"
    "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
    "time"
)

// Metric はグラフ化しやすいよう加工されたDatadog用のメトリクス
type Metric struct {
    // タイムスタンプ
    timestamp time.Time
    // ドメイン
    // 例: crowdworks.jp
    DomainName string
    // 迷惑メール率
    // 例: 0.001 -> 0.1%
    UserReportedSpamRatio float64
}

// ToDatadogSeries はDatadogに送信するためのデータを作成
func (m Metric) ToDatadogSeries(currentTime time.Time, datadogEnv string) []datadogV2.MetricSeries {
    tags := []string{
        "env:" + datadogEnv,
        "domain:" + m.DomainName,
    }

    return []datadogV2.MetricSeries{
        {
            Metric: "crowdworks.postmastertools.user_reported_spam_ratio",
            Type:   datadogV2.METRICINTAKETYPE_GAUGE.Ptr(),
            Points: []datadogV2.MetricPoint{
                {
                    Timestamp: datadog.PtrInt64(currentTime.Unix()),
                    Value:     datadog.PtrFloat64(m.UserReportedSpamRatio),
                },
            },
            Tags: tags,
        },
    }
}

// SubmitDatadog はMetricで与えられたドメインごとの値をDatadogに送信
func SubmitDatadog(metrics []Metric, currentTime time.Time, datadogEnv string) error {
    var series []datadogV2.MetricSeries
    for _, e := range metrics {
        series = append(series, e.ToDatadogSeries(currentTime, datadogEnv)...)
    }

    body := datadogV2.MetricPayload{Series: series}
    ctx := datadog.NewDefaultContext(context.Background())
    configuration := datadog.NewConfiguration()
    apiClient := datadog.NewAPIClient(configuration)
    api := datadogV2.NewMetricsApi(apiClient)
    _, r, err := api.SubmitMetrics(ctx, body, *datadogV2.NewSubmitMetricsOptionalParameters())

    if err != nil {
        return fmt.Errorf("MetricsApi.SubmitMetricsに失敗: %v", r)
    }
    return nil
}

迷惑メール率をDatadogのカスタムメトリクスとして送信しています。

DatadogのAPIの詳細は以下をご覧ください。

docs.datadoghq.com

Postmaster ToolsとDatadog API仕様による気をつけるべきポイント

Postmaster Toolsで確認できる最新の迷惑メール率は大体 3日前 の迷惑メール率です。 しかし、Datadogのカスタムメトリクス送信時に指定できるタイムスタンプには、未来は10分、過去は1時間という仕様があります。

よって、実際には 3日前 の迷惑メール率を、プログラム実行時の時刻(現在時刻)をタイムスタンプとして、Datadogに送信しています。 このタイムスタンプのズレは、アラート内容に 3日前 である旨を記述してカバーしています。

Datadogのカスタムメトリクスの詳細については、ドキュメントを御覧ください。

docs.datadoghq.com

Datadogのダッシュボード

Datadogのダッシュボードを用意し、各ドメインの迷惑メール率の一覧を出して、見やすくなりました。

各ドメインの迷惑メール率
ドメインの迷惑メール率

監視していて迷惑メール率が上がった場合は、なんらか対策を検討しましょう。

まとめ

迷惑メール率を1ドメインずつ目視チェック運用を継続するのは面倒です。 そのため、Postmaster Toolsの迷惑メール率をドメイン毎に一覧でき、悪くなった時もSlack通知ですぐに気付けるようにしました。

We're Hiring !

クラウドワークスでは一緒に課題を解決してくださるエンジニアを募集しています。

crowdworks.co.jp

© 2016 CrowdWorks, Inc., All rights reserved.