Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

GCP の Error Reporting に飛ぶエラーを Slack に通知したい

DSOC サービス開発部のエンジニアの小山です。

私のチームでは GCP を使っており、GCP の Error Reporting というエラーを収集してくれるサービスでアプリケーションで発生したエラーを確認しています。

Error Reporting

ですが、この Error Reporting は現時点では Slack に直接通知する方法はなく、メールでの通知か mobile アプリへの通知しかありません。(Notifications, Cloud Console Mobile App)

メールを Slack に通知する方法もありますが、それだと Slack に通知されるまで数分のラグがあり、エラー通知としては微妙です。 メール通知や mobile アプリへの通知自体もそれはそれで便利なのですが、 業務中はやはり Slack に通知が飛んでくるのが一番気づきやすいですし、そのままチームメンバーとのやり取りもできるので便利です。

そこで、いくつかの案を検討し Slack へのエラー通知を実装しました。

検討した案

  • ①. Cloud Logging -> Cloud Logging の Log Router -> Cloud Pub/Sub -> Cloud Functions -> Slack
  • ②. Cloud Logging -> Cloud Logging の Log Router -> Cloud Monitoring -> Slack
  • ③. Cloud Logging -> DataDog -> Slack
  • ④. Sentry などのエラートラッキング用のツール/サービスを導入する

私のチームはまだ小さくエラー自体も少ないので、今回は ① を採用しました。 アプリケーションが大きくなってきたりしたら ④ への移行ができたらと思っていますが、一旦は素朴に解決しました。 (①の実装をやってみたかった、というのも大きいです > < )

②も具体的に検討しましたが、エラーの発生を Slack に通知することはできてもエラーの詳細は通知できなさそうだったので諦めました。

③、④ は次のような考慮ポイントがいくつかあり、今回はさくっと解決しておきたかったので見送りました。

  • サービスの導入で費用が発生する
  • 個人情報などのセンシティブな情報がエラーにでてしまう可能性が否定できないので、それらが該当サービスに入って問題ないのか

今回は ① を採用しましたが、① にもマイナスな面はあります。

  • エラー通知したいためだけにこれは少し仰々しい
    • 個別に見ていけばやっていることはシンプルですが、全体を見ると少し仰々しい。
  • エラーが発生したら常に Slack に通知されるので、ノイズになりやすい
    • Logging にエラーがでたら、それをそのまま Pub/Sub, Functions を経由して Slack に通知しているだけなので、エラーが大量に発生すると全部通知されてノイズ。

実装の概要

通知されるまでの流れとしては以下です。

  • Cloud Logging にエラーログをはく
  • Cloud Logging の Log Router でエラーログをフィルターして、 Cloud Pub/Sub に流すよう設定する
  • Cloud Pub/Sub はそのまま Cloud Functions にエラーログの情報をながす
  • Cloud Functions でエラーログの中身を読んで加工し、 Slack へ通知する

実装の作業でのメインは、

  • Pub/Sub Trigger な Functions を作成
  • Functions 作成時に自動できる Pub/Sub の topic に対してログを流すように Log Router を設定する

この2点です。

Pub/Sub Trigger な Functions を作成

https://cloud.google.com/functions/docs/calling/pubsub 上記を参考に、 Pub/Sub trigger な Functions を作成します。

関数の中身は以下です。

import fetch from "node-fetch";

// ログのフォーマットの参考
// https://cloud.google.com/functions/docs/calling/pubsub#event_structure
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
// https://cloud.google.com/logging/docs/export/using_exported_logs#pubsub-organization

type ReceivedMessage = {
  data: string; // base64 された Data
  attributes: {
    [key: string]: string;
  };
};

type Data = {
  log: string;
  insertId: string;
  textPayload?: string;
  jsonPayload?: {
    message: string;
    [key: string]: string;
  };
  timestamp: string;
  labels: { [key: string]: string };
  severity: string;
  resource: {
    labels: {
      module_id: string; // ex.) "foo-service"
      project_id: string; // ex.) "your-project-id"
      version_id: string; // ex.) "20200101t111111"
    };
  };
};

// https://cloud.google.com/functions/docs/writing/background#function_parameters
type Context = {
  eventId: string;
  timestamp: string;
  eventType: string;
  resource: string;
};

const notify2Slack = async (body: object) => {
  const options = {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env["SLACK_OAUTH_ACCESS_TOKEN"]}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ...body, channel: process.env["SLACK_CHANNEL"] }),
  };
  const response = await fetch("https://slack.com/api/chat.postMessage", options);

  const res = await response.json();
  if (res.ok) {
    console.log("Posted a message to slack.");
  } else {
    console.error(`Failed to post a message to slack. ${JSON.stringify({ res, body })}`);
  }
};

const buildBody = (data: Data) => {
  const errorText =
    data.textPayload ?? data.jsonPayload?.message ?? "No error message found in textPayload, jsonPayload";

  return {
    username: "エラー通知",
    icon_emoji: ":fire:",
    text: `${data.resource.labels.project_id} でエラーが発生しました`,
    attachments: [
      {
        text: errorText,
        color: "#ff0000",
        footer: `${data.resource.labels.project_id}, ${data.resource.labels.module_id}, ${data.resource.labels.version_id}`,
      },
    ],
  };
};

// Cloud Functions の本体
export const notifyErrorLog = async (pubSubEvent: ReceivedMessage, _context: Context) => {
  const dataString = Buffer.from(pubSubEvent.data, "base64").toString();
  const data = JSON.parse(dataString) as Data;

  await notify2Slack(buildBody(data));
};

実際のものよりは少しだけ簡略化してあります。 また、上記のコードはTypeScript で書いてありますので、Cloud Functionsに設定するときには 先に transpile を行います。

必要な Package, 環境変数は以下です。

"dependencies": {
    "node-fetch": "2.6.0"
},
  • SLACK_OAUTH_ACCESS_TOKEN
  • SLACK_CHANNEL

(Slack 通知部分は別のライブラリを導入するともう少しシンプルになるとは思っています)

Pub/Sub の topic に対してログを流すように Log Router を設定する

https://cloud.google.com/logging/docs/routing/overview https://cloud.google.com/logging/docs/export/configure_export_v2

上記を参考に Log Router の synk を設定します。 ドキュメントは少し複雑に見えるかもしれないですが、実際画面を見ながら操作すれば簡単にできると思います。

synk 作成時のクエリの例を以下に記載します。

resource.type="gae_app"
severity>=ERROR

まとめ

今回、私のチームではこの方法で、エラーを Slack に通知するようにしました。 この方法がベストだとは全然思っていないのですが、チームサイズが小さく・エラー量が少ないという私のチームの現状では、ほぼ問題なく運用できています。 たまに、エラーが大量に発生して Slack の通知がうるさいときもあるのですが、それはそれで異常に気づきやすいので一旦はアリかなと感じています。

なによりも、エラーがすぐに Slack に通知されるようになったのはとても価値があります。

この解決方法がマッチするチームはあまり多くはないかもしれないですが、 選択肢の一つとして参考になれば幸いです。


buildersbox.corp-sansan.com

buildersbox.corp-sansan.com

© Sansan, Inc.