🦓

GCloudのエラーログをSlackに通知する

に公開

はじめに

Google Cloud Platform(GCP)を利用していると、システムの異常やエラーを迅速に検知することが重要です。
標準のアラート通知では、エラーが発生したことは通知されますが、詳細なログ内容が表示されず、リンクをクリックして別画面でログを確認する必要があります。

緊急時の問題把握が難しいと感じたため、ログの内容をSlackチャンネルに直接送信する方法を調査&まとめてみました。

tl:dr;

以下のアーキテクチャを構築することによって、上記の問題を解決するができました。

Log Router → Pub/Sub → Cloud Function → Slack Webhook

実装手順

1. Pub/Subトピックの作成

まず、ログデータを一時的に保存するためのPub/Subトピックを作成します。

  1. GCPコンソールで「Pub/Sub」に移動
  2. 「トピックを作成」をクリック
  3. トピック名を入力(例:logs-to-slack
  4. デフォルト設定のまま「作成」をクリック

Pub/Subについて

2. ログシンクの作成

次に、特定の条件に一致するログをPub/Subに転送するためのシンクを作成します。

  1. GCPコンソールで「ロギング」→「ログルーター」に移動
  2. 「シンクを作成」をクリック
  3. シンク名を設定(例:error-logs-to-slack
  4. シンクに含めるログを選択するクエリを入力:
severity>=ERROR

このクエリは重要度が「ERROR」以上のログを対象としています。

  1. 宛先として「Cloud Pub/Sub」を選択
  2. 作成したトピック(logs-to-slack)を選択
  3. 「作成」をクリック

ログのクエリについて
サンプルクエリ

3. 権限の設定

シンク作成後、GCPロギングサービスがPub/Subトピックに書き込めるよう権限を設定します。

  1. シンク作成後に表示されるサービスアカウント(servic-123456789012@gcp-sa-logging.iam.gserviceaccount.comのような形式)をメモ
  2. 「IAMと管理」→「IAM」に移動
  3. 「アクセスを許可」をクリック
  4. 「プリンシパル」にサービスアカウントのメールアドレスを入力
  5. ロールとして「Pub/Subパブリッシャー」を選択
  6. 「保存」をクリック

4. Slackウェブフックの設定

次に、Slackでログを受け取るためのウェブフックを設定します。

  1. Slack APIにアクセス
  2. 「Create New App」→「From scratch」を選択
  3. アプリ名とワークスペースを設定
  4. 「Incoming Webhooks」を選択し、有効化
  5. 「Add New Webhook to Workspace」をクリック
  6. ログを送信するチャンネルを選択
  7. 生成されたウェブフックURLをコピー

5. Cloud Functionの作成

最後に、Pub/Subからメッセージを受け取り、Slackに送信するCloud Functionを作成します。

  1. GCPコンソールで「Cloud Functions」に移動
  2. 「関数を作成」をクリック
  3. 関数名を入力(例:logs-to-slack
  4. トリガーを「Cloud Pub/Sub」に設定
  5. 作成したトピックを選択
  6. 「次へ」をクリック
  7. ランタイムに「Node.js 20」(または最新バージョン)を選択
  8. エントリポイントに関数名(processLogEntry)を設定

package.json

package.json
{
  "name": "sample-pubsub",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/pubsub": "^0.18.0",
    "@slack/webhook": "^6.1.0"
  }
}

index.js

index.js
const {IncomingWebhook} = require('@slack/webhook');

// SlackウェブフックURLを設定
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL';
const webhook = new IncomingWebhook(SLACK_WEBHOOK_URL);

exports.processLogEntry = async (message, context) => {
  try {
    // Pub/Subメッセージをデコード
    const logEntry = JSON.parse(Buffer.from(message.data, 'base64').toString());
    console.log('ログエントリを処理中:', JSON.stringify(logEntry));

    // Slack用にログエントリをフォーマット
    const slackMessage = formatLogForSlack(logEntry);

    // Slackに送信
    await webhook.send(slackMessage);
    console.log('メッセージが正常にSlackに送信されました');
  } catch (error) {
    console.error('ログエントリの処理中にエラーが発生しました:', error);
  }
};

function formatLogForSlack(logEntry) {
  // ログから必要な情報を抽出
  const severity = logEntry.severity || 'INFO';
  const timestamp = logEntry.timestamp || new Date().toISOString();

  let message = '';
  if (logEntry.textPayload) {
    message = logEntry.textPayload;
  } else if (logEntry.jsonPayload) {
    message = JSON.stringify(logEntry.jsonPayload, null, 2);
  } else {
    message = 'No message';
  }

  const httpRequest = logEntry.httpRequest || {};
  const resource = logEntry.resource || {type: 'unknown'};

  // ログへのリンクを作成
  const projectId = logEntry.resource?.labels?.project_id || 'unknown-project';
  const logName = encodeURIComponent(logEntry.logName || '');
  const logLink = `https://console.cloud.google.com/logs/query;query=${encodeURIComponent(`logName="${logEntry.logName}"`)}?project=${projectId}`;

  // フォーマットされたSlackメッセージを返す
  return {
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: `ログエントリ [${severity}]`
        }
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*メッセージ:*\n\`\`\`${message}\`\`\``
        }
      },
      {
        type: "section",
        fields: [
          {
            type: "mrkdwn",
            text: `*タイムスタンプ:* ${timestamp}`
          },
          {
            type: "mrkdwn",
            text: `*リソース:* ${resource.type || 'unknown'}`
          }
        ]
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `<${logLink}|ログの詳細を表示>`
        }
      }
    ].filter(Boolean)
  };
}
  1. SLACK_WEBHOOK_URLを先ほどコピーしたURLに置き換え
  2. 「デプロイ」をクリック

動作確認

設定が完了したら、実際に動作確認を行います。

  1. エラーを発生させるようなリクエストをGCPのサービスに送信
  2. Slackチャンネルを確認し、詳細なログ情報が表示されることを確認

カスタマイズ例

メッセージフォーマットのカスタマイズ

Slackに送信するメッセージの形式は、formatLogForSlack関数で自由にカスタマイズできます。例えば、特定のフィールドのみを表示したり、色を変更したりすることが可能です。

フィルタの調整

ログシンクのクエリを変更することで、特定の条件のログのみを取得できます。例えば、特定のサービスやリソースタイプに絞り込むこともできます。

セキュリティに関する注意点

Slackウェブフックの認証情報はCloud Functionのコード内に直接記述せず、Secret Managerなどを使って安全に管理するこができます。

まとめ

この記事では、GCPのログを完全な形でSlackに送信する方法をまとめてみしました。
この仕組みにより、エラーやアラートが発生した際に、即座に詳細を確認できるようになり、対応時間の短縮につながります。

また、このアーキテクチャは他のシステムへの連携にも応用できます。例えば、Slackの代わりにDiscordやMicrosoft Teamsに送信することも可能です。

誰かの参考になれば嬉しいです。

https://cloud.google.com/blog/ja/products/devops-sre/use-slack-and-webhooks-for-notifications?hl=ja
https://cloud.google.com/integration-connectors/docs/connectors/slack/configure

Discussion