New RelicとSlackBotを使った障害監視の仕組みを作った話


お久しぶりです。AiRecoのふっきーです。

時の流れは本当に早いもので、今年の4月で入社して4年目を迎えました。
右も左もわからなかった新人時代から、少しは成長してきたのではないかと信じています。
今回は、そんな成長した私がたしなみ時間を利用して、チームに役立つ障害監視の仕組みを作成しました。
その内容をご紹介しようと思います。
同じような取り組みを検討している方々にとって、少しでも参考になれば嬉しいです。

背景

特定の障害に対して、迅速かつ確実に通知を受け取れる仕組みが求められていました。
特に重要なアラートについては、チーム全体で共有し、適切な対応を取る必要があります。
そこで、以下の2つの方法を使って障害監視を行っていました。

  1. New Relic→Slackへのアラート連携
  2. 監視サービスの法人契約
    • 特定の障害が発生してから1時間継続した場合、チームメンバーに電話をかけて通知してくれるサービス
    • 従量課金制ではなく固定費で、月額約2万円


Slackにアラートを連携しているのであれば、「チャンネルの通知をONにすればいいのでは?」と思われる方もいるかもしれません。
しかし、アラートの内容によっては頻繁に通知が届くものや、注意レベル程度のものも含まれるため、すべてのアラートが通知されるのは煩わしいという課題がありました。
そこで、特定のアラートに対してのみ通知が届く仕組みを作りたいという思いがあり、チーム内でも以前から「アラートBotが欲しい」という話が出ていました。
さらに、特定のアラートのためだけに契約していた電話通知型監視サービスは月額2万円の固定費が発生していましたが、現代の技術を活用すれば、より効率的かつ低コストで運用できる可能性があると考え、代替案を検討することにしました。

目的

アラートBotを作成する目的は以下の通りです。

  1. 特定のアラートに対してチームに連絡を行う機能を実現すること
  2. 特定のアラートにおいては電話をかける仕組みを導入すること

ただし、すべての機能を紹介すると非常に長い記事になってしまうため、本記事では「チームに連絡を行う機能」を備えたアラートBotの作成について解説します。
電話をかける機能の実装については、いずれ別の記事で詳しく紹介する予定です。

作成の流れ

では、実際にアラートBotをどのように作成していったのかをご紹介します。

  1. アラートの把握
    まずは、Slackに連携しているアラートの全体像を把握するところから始めました。
    これらのアラートは私がチームに参加する前から設定されていたため、正直なところ、それまで詳しい内容を把握していませんでした…。
    そこで、以下の項目を整理することにしました。

    • 現在どんなアラートがSlackに連携されているのか
    • 各アラートはどんな条件で「Active化」、「Close化」するのか
    • 各アラートに対してどのようなアクションを取るべきなのか

    これらをすべてまとめた上で、チームメンバーに「各アラートに対してどんな反応が欲しいか」をヒアリングしました。
    その結果、アラートは以下の4つのパターンに分類されました。

    1. 何も対応する必要がないアラート(Botからのメッセージは不要)
    2. 現状のアラート設定を見直し修正するだけで済むアラート(Botからのメッセージは不要)
    3. メッセージの返信を促したいアラート
    4. メッセージに加えて、New Relicの特定のスクリーンショットを返信させたいアラート

  2. Slackアプリの作成
    以下の記事を参考にしながらSlackアプリを作成し、アラートを連携しているチャンネルに追加しました。
    tools.slack.dev
    Scopes設定では、Botがチャンネルに対してどのような権限を持つかを指定できます。私の場合、以下のように設定しました。
    ローカル環境での実験用チャンネルがprivateかどうか、またはDM(ダイレクトメッセージ)を許可するかどうかなど、Botの利用目的や運用環境に応じて設定内容を検討すると良いと思います。
    BotのScopes設定例


  3. コード作成
    下準備が整ったので、いよいよBotを動かすコードを作成します。今回はNode.jsを利用しました。
    アラートBotの仕組みは、以下のようなフローで動作します。

    1. アラートを受信したら、メッセージから何のアラートが発生したかを読み取る
    2. 対応するメッセージを返信する
      • 特定のアラートの場合は、New Relicダッシュボードのスクリーンショットを添えて返信する
    3. 返信はアラートのスレッドに対して行い、チームメンションを付けて送信する
      • ただし、アラートの種類によってはメンションを付けない場合もある
    4. 特定のアラートの場合、発生時から1時間経過したタイミングで追加メッセージを送信する

    超ざっくりですが、フロー図も作成してみました。
    ざっくりフロー図

    コード全体を載せると非常に長くなってしまうため、この記事ではポイントを絞って一部のコードを紹介します。

    • グループメンションの設定方法
      通常、個人メンションをつけてメッセージを送る際には、個人に割り当てられている「メンバーID」を使用します。
      メンバーIDは、プロフィール画面の三点リーダーから「メンバーIDをコピー」することで取得できます。



      以下は個人メンションをつけたメッセージ送信のコード例です:
      const CHANNEL_NAME = "#test_channel"; // チャンネル名を指定
      const USER_ID = "XXXXXXXXXXX"; // メンションするユーザーIDを指定
      const MESSAGE = `<@${USER_ID}> \nこれはテストです。`; // メンション付きのメッセージ
      
      await client.chat.postMessage({
          channel: CHANNEL_NAME,
          text: MESSAGE 
      });
      

      では、グループメンションも同様にIDが必要になりますが、そのIDはどこから確認できるのでしょうか?
      調べてみたところ、管理者であればSlackの管理画面から、またはSlackのusergroups.listAPIを利用して取得する方法が出てくるのですが、
      個人的に一番手っ取り早く感じたのはブラウザの開発者ツールを使う方法です。

      Slackをブラウザで開き、さらに開発者ツールを開きます(通常は右クリック →「検証」またはF12キー)。
      グループメンションをクリックし、該当の要素を確認してみてください。
      すると、次のようなaタグがハイライトされているかと思います。
      <a target="_blank" class="c-link c-mrkdwn__user_group c-mrkdwn__user_group--link c-mrkdwn__user_group--mention" data-user-group-id="XXXXXXXXXXX" data-qa-user-group-mrkdwn-is-linked="true" data-qa-user-group-mrkdwn-is-mention="true" data-stringify-type="mention" data-stringify-id="XXXXXXXXXXX" data-stringify-label="@team-testproject" href="https://test.slack.com/admin/user_groups" rel="noopener noreferrer">@team-testproject</a>
      
      この中の属性data-user-group-idがグループIDです。

      取得したグループIDをコードに埋め込むことで、グループメンション付きのメッセージを送信できます。ただし、個人メンションとは記述方法が異なるため注意が必要です。
      以下はグループメンションをつけたメッセージ送信のコード例です:
      const CHANNEL_NAME = "#test_channel"; // チャンネル名を指定
      const GROUP_ID = "XXXXXXXXXXX"; // メンションするグループIDを指定
      const MESSAGE = `<!subteam^${GROUP_ID}> \nこれはテストです。`; // メンション付きのメッセージ
      
      await client.chat.postMessage({
          channel: CHANNEL_NAME,
          text: MESSAGE 
      });
      
      ぜひ試してみてください!

    • New Relicのスクリーンショット取得方法
      New Relicでは、NerdGraph APIのdashboardCreateSnapshotUrlメソッドを使用することで、特定のページを指定してスクリーンショットを取得することができます。
      const postData = JSON.stringify({
          query: `mutation { dashboardCreateSnapshotUrl(guid: "${guid}") }`,
      });
      const options = {
          hostname: "api.newrelic.com",
          port: 443,
          path: "/graphql",
          method: "POST",
          headers: {
              "Content-Type": "application/json",
              "API-Key": apikey,
          },
      };
      

      各ページのGUIDの取得方法です。
      まずダッシュボード名の横にある三点リーダーをクリックして、ダッシュボードのGUIDを取得します。


      次に、NerdGraph API Explorerを使用して以下のクエリを実行します。YOUR_DASHBOARD_GUIDには、ダッシュボードのGUIDを入力してください(両方に同じ値を入れます)。
      {
        actor {
          entitySearch(query: "parentId ='YOUR_DASHBOARD_GUID' or id ='YOUR_DASHBOARD_GUID'") {
            results {
              entities {
                guid
                name
                ... on DashboardEntityOutline {
                  guid
                  name
                  dashboardParentGuid
                }
              }
            }
          }
        }
      }
      
      このクエリを実行すると、各ページのGUIDが取得できます。
      これをdashboardCreateSnapshotUrlメソッドのguidに渡すことで、スクリーンショットのPDFリンクを取得することができます。
      取得したリンクをSlackのメッセージで送信することで、スクリーンショットを共有できます。
      ちなみに、ページ内の特定のウィジェットを指定してスクリーンショットを取得することはできません。そのため、ページが大きい場合はスクリーンショットが見にくくなる可能性があります。
      ページをコンパクトに作成することをおすすめします。
      また、NerdGraph APIのクエリでは変数を使うことができない点にも注意してください。

    • 一定時間経過後の追加アクション
      特定のアラートにおいて、アラート発生から1時間が経過した際に追加のメッセージを送信したいケースがありました。
      現時点では、1時間後にNew Relicのスクリーンショットを送信するだけの動作を実装していますが、将来的にはここに「電話をかける仕組み」を追加する予定です!
      以下はそのコード例です:
      if ({何かしらの条件}) {
          const timerId = setTimeout(async () => {
              try {
                  // New Relicからスクリーンショットを取得
                  // スクリーンショットと共に、1時間経過したことをSlackに通知
              } catch (error) {
                  console.error(error);
              } finally {
                  messageTimers.delete(message.ts);
              }
          }, 60 * 60 * 1000); // ここで1時間を指定
      
          messageTimers.set(message.ts, timerId);
      }
      

      1. 条件の確認:最初に条件を確認し、特定のアラートに対してのみタイマーを設定します。
      2. タイマーの設定:setTimeoutを使用して、1時間後にアクションを実行するタイマーを設定します。
        1時間はミリ秒単位で指定しており、60 * 60 * 1000(60分×60秒×1000ミリ秒)となっています。
      3. スクリーンショットの取得と通知:タイマーが発動した際に、New Relicからスクリーンショットを取得し、Slackに通知を送信します。
      4. クリーンアップ:処理が完了したら、messageTimers.delete(message.ts)でタイマーIDを削除し、メモリを解放します。

  4. コンテナアプリへの移行
    コードが完成したら、Botを実行する環境を整える必要があります!
    開発時はローカル環境でコードを実行しながら進めていましたが、Botを常時起動させるためにコンテナアプリに移行しました。
    本Botはgit管理しており、コードに修正が入るとパイプラインを通じて自動的にデプロイされる仕組みを実現しています。
    また、Botで使用するシークレット(APIキーや認証情報など)はコンテナアプリで管理するようにしました。
    これにより、セキュリティが向上するとともに、自分以外のチームメンバーも管理できる環境が整いました。

完成したもの

では、実際に完成したBotの動作を見てみましょう!

  • メンションを付けずにメッセージのみ送信


  • メンションを付けずにメッセージ+New Relicのスクリーンショットを送信


  • メンションを付け、メッセージ+New Relicのスクリーンショットを送信。その後、1時間後に再度メッセージ+スクリーンショット送信

まとめ

いかがでしたでしょうか?
今回は、Slack上でアラートを効率的に通知するBotを作成し、その仕組みから運用環境までを一通りご紹介しました。
このBotを導入することで、チーム内でのアラート対応がスムーズになり、New Relicのスクリーンショットを活用した視覚的な情報共有も可能になりました。
また、コンテナアプリへの移行により、Botの安定稼働とチームでの管理が簡単になった点も大きな進展です。

次回は、さらに機能を拡張し、Twilioを使った電話架電の仕組みについてご紹介する予定です。
これにより、緊急性の高いアラートに対して、より迅速で確実な対応が可能になります。
是非お待ちいただければと思います。

最後に、今回の記事を通じて何か参考になる部分がありましたら嬉しいです。

お知らせ

ecbeingでは新進気鋭なエンジニアを募集しております!
careers.ecbeing.tech