システム障害のエラーをAI分析してエンジニアに電話をかけるシステムを作った(kintone、Vonage、make、Dify、GAS、Mackerel)

この記事は[ProtoPedia Advent Calendar 2024](https://qiita.com/advent-calendar/2024/protopedia) の 22日目の記事です。
 
# 作ろうと思ったきっかけ
自分はよくハッカソンに参加する。
イベントの種類によっては業務扱いで参加することもある。

社内発表会などで得た知見をフィードバックするが
「それ業務で使える？」「作ったらいくらで売れる？」
など、当然だが現実的な反応が返ってくる。

このままでは業務扱いが認められなくなる。
理解ある管理者を増やすため、業務で使えそうなシステムを作ることにした。

# 作ったもの
https://protopedia.net/prototype/6174

# システム概要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/72f5fe3b-444d-8278-3c6d-4ed43dddcbf5.png)

システムのコア
- kintoneによるDB管理
- DifyによるAI分析
- makeによる各種処理の結合

付加価値
- Vonageによる電話通知
- Mackerelによる監視連携

## 動作例
緊急度が高い場合(電話あり)
```
エラーログ(Apacheの異常終了)
[Thu Apr 25 11:52:03.207133 2024] [core:error] [pid 31274] AH00046: child process 32436 still did not exit, sending a SIGKILL

緊急度
high

要約
このエラーログは、Apache HTTPサーバーのプロセスが正常に終了しないことを示しています。
子プロセスが指定された時間内に終了せず、強制終了信号(SIGKILL)が送信されているため、サーバーの安定性やパフォーマンスに悪影響を及ぼす可能性があります。 

対処方法
この問題を解決するためには、まずApacheの設定を見直し、子プロセスの最大数やタイムアウト設定を調整します。
また、エラーログやアクセスログを確認して、特定のリクエストや処理が原因であるかを調査し、必要に応じてアプリケーションのコードを修正することが重要です。
最終的には、Apacheを再起動して変更を適用し、問題が解消されたか確認します。
```

緊急度が低い場合(電話なし)
```
エラーログ(Apacheの正常起動)
[Sun Aug 11 03:11:02.255000 2024] [mpm_prefork:notice] [pid 951] AH00163: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/8.2.9 configured -- resuming normal operations

緊急度
low 

要約
このエラーログは、Apacheサーバーが正常に動作を再開したことを示しています。特に、mpm_preforkモジュールの通知メッセージであり、エラーではなく通常の運用状態に戻ったことを示しています。 

対処方法
特に復旧作業は必要ありませんが、サーバーの運用状況を監視し、異常が発生した際には適宜対応してください。
```
# 実装詳細
## kintoneレコード登録されたエラーログをAI分析

まずはデータを格納する箱を作るため、kintoneで適当なアプリを作成する
![スクリーンショット 2024-12-21 13.25.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/4c47a445-6365-bc38-278c-830ed12003bc.png)

makeで新しいシナリオを作成してwebhookの受け口を用意
![スクリーンショット 2024-12-21 13.27.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/656be0c5-f58d-af58-ca37-2d79e1115746.png)

kintoneのレコード作成時の投げ先を登録する
![スクリーンショット 2024-12-21 13.28.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/36b53a39-a937-747a-7fc1-c3a2c7193a92.png)

Difyでログ分析用のワークフローとプロンプトを用意
json形式での応答を指示することで実装を時短
![スクリーンショット 2024-12-21 13.32.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/1f971d98-daa4-113a-81ae-0f265ea62898.png)

makeでつなぐ
エラーログにはダブルクォートや改行も含まれる
これはDifyへのリクエストjson作成に都合が悪いので変換・除去する
![スクリーンショット 2024-12-21 13.36.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/822f30d3-e86e-f612-8051-9ce94527f68a.png)
![スクリーンショット 2024-12-21 13.38.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/72f7a79b-0184-eb33-e47a-07bb8ed5ece2.png)
![スクリーンショット 2024-12-21 13.34.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/e5656dc5-6638-86eb-1112-c871d80a63d9.png)

Difyの応答をパースしてkintoneに保存する
![スクリーンショット 2024-12-21 14.25.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/77e6c82a-c184-34d0-0a94-e67b15c1ed3d.png)

これでコア機能が完成

## 緊急度が高ければVonageで電話をかける
緊急度で処理の継続をフィルタ
![スクリーンショット 2024-12-21 14.28.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/785dd034-88ea-5fb5-b2a9-fd98e0bb47d2.png)

JWT作成用のGASを準備
```js
// JWTを生成して返すGoogle Apps Script関数
function doGet(e) {
  // アルゴリズム、アプリケーションID、秘密鍵を指定
  const privateKey = `-----BEGIN PRIVATE KEY-----
your private key
-----END PRIVATE KEY-----`;

  const header = {
    alg: "RS256",
    typ: "JWT"
  };

  const jti = generateUUID();

  // JWTのペイロード
  const payload = {
    iat: Math.floor(Date.now() / 1000), // 現在の発行時間
    jti: jti, // 一意識別子（UUIDなどを生成可能）
    sub: "alice", // サブジェクト（通常はユーザーIDなど）
    exp: Math.floor(Date.now() / 1000) + (60 * 60), // 有効期限（ここでは1時間後に設定）
    acl: {
      paths: {
        "/*/rtc/**": {},
        "/*/users/**": {},
        "/*/conversations/**": {},
        "/*/sessions/**": {},
        "/*/devices/**": {},
        "/*/image/**": {},
        "/*/media/**": {},
        "/*/knocking/**": {},
        "/*/legs/**": {},
        "/v1/calls/**": {} //これが重要
      }
    },
    application_id: "vonage app id(uuid)" // VonageアプリケーションID
  };

  // Base64URLエンコード
  const base64UrlEncode = (obj) => {
    return Utilities.base64EncodeWebSafe(JSON.stringify(obj)).replace(/=+$/, '');
  };

  const headerEncoded = base64UrlEncode(header);
  const payloadEncoded = base64UrlEncode(payload);
  const signatureInput = `${headerEncoded}.${payloadEncoded}`;

  // RS256で署名を生成
  const signature = Utilities.computeRsaSha256Signature(signatureInput, privateKey);
  const signatureEncoded = Utilities.base64EncodeWebSafe(signature).replace(/=+$/, '');

  // 完成したJWT
  const jwt = `${headerEncoded}.${payloadEncoded}.${signatureEncoded}`;

  // JWTをレスポンスとして返す
  return ContentService.createTextOutput(jwt).setMimeType(ContentService.MimeType.TEXT);
}

function generateUUID() { 
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}
```

JWTを使ってVonageAPIをコール
エラー要約の読み上げとアクション要求

ここでは簡易化のため発信先をハードコードしている
実運用ではkintoneアプリに連絡先を登録しmakeで取得すればよい
(make内の日付変数などを活用してローテーション可能)
![スクリーンショット 2024-12-21 14.33.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/80c94e41-2007-bf32-de8e-3a9da9a9ca9a.png)

```js
{
"to":[
{"type":"phone","number":"81XXXXXXXXXX"}
],
"from":{
  "type":"phone","number":"Vonageで購入した電話番号"
},
"ncco": [
  {
    "action":"talk",
    "text":"{{18.explanation}}",
    "language":"ja-JP",
    "bargeIn": false
  },
  {
    "action": "talk",
    "text": "1を押すともう一度メッセージを再生します。2を押すと通話を終了し、対応開始を登録します。3を押すとマネージャーに電話をかけます。4を押す、または通話終了で次のエンジニアに電話をかけます。",
    "language": "ja-JP",
    "bargeIn": true
  },
  {
    "action": "input",
    "submitOnHash": true,
    "eventUrl": ["振り分け用webhook
?record={{6.record.`レコード番号`.value}}"],
    "dtfm": {
        "timeOut": 10,
        "maxDigits": 1
    }
  }
]}
```

電話口での番号入力に応じて処理を分岐するmakeシナリオを用意
上記のeventUrlに指定
(この辺りJWT含めてmakeでは処理しづらい)
![スクリーンショット 2024-12-21 14.39.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/48741c4c-d9a1-3875-4084-b521375ff01c.png)


## Mackerel監視との連携
GASでwebhookの受け口を用意
kintoneにレコード登録
(makeでも良いが、無料は2シナリオなのでGASで代用)
```js
function doGet(e) {
  // 正常なレスポンスとして200 OKを返す
  return ContentService.createTextOutput('OK').setMimeType(ContentService.MimeType.TEXT);
}

// Kintoneへのレコード登録
function postToKintone(mackerel) {

  if (mackerel.alert.status == "ok") {
    return;
  }

  // Kintoneの設定
  var kintoneDomain = 'your kintone domain'; // Kintoneのサブドメイン
  var appId = 'your app id'; // KintoneアプリのID
  var apiToken = 'your api token'; // KintoneのAPIトークン

  // APIエンドポイントのURL
  var url = 'https://' + kintoneDomain + '.cybozu.com/k/v1/record.json';

  // レコードデータを作成（フィールドコードを指定）
  var payload = {
    "app": appId,
    "record": {
      "error_log": {
        "value": mackerel.alert.status + " " + mackerel.message
      },
      "hostname": {
        "value": mackerel.host.name
      },
      "source": {
        "value": "mackerel"
      }
    }
  };

  // リクエストヘッダー
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      'X-Cybozu-API-Token': apiToken
    },
    'payload': JSON.stringify(payload)
  };

  // リクエスト送信
  var response = UrlFetchApp.fetch(url, options);
  Logger.log(response.getContentText());
}

// Mackerel Webhook受信処理
function doPost(e) {
  try {
    // Mackerelからのデータをパース
    var json = JSON.parse(e.postData.contents);

    // 受け取ったデータをKintoneに送信
    postToKintone(json);

    // 正常終了のレスポンス
    return ContentService.createTextOutput('Success').setMimeType(ContentService.MimeType.TEXT);
  } catch (error) {
    Logger.log('Error: ' + error);
    return ContentService.createTextOutput('Error: ' + error).setMimeType(ContentService.MimeType.TEXT);
  }
}
```

Mackerelの監視エラー発生時の通知を登録
![スクリーンショット 2024-12-21 14.44.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/4820bede-0dc1-39b9-8677-4d1b0f2086c2.png)

適当なサーバにエージェントをインストール
監視ルールを設定
(動作確認ため閾値を低く設定)
![スクリーンショット 2024-12-21 14.53.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/8f3a06ee-7baa-c239-fa3c-ea5be3b9a5b2.png)

kintoneにレコード登録されると同様に処理される
(監視エラーメッセージが簡素なので、AI分析は一般論になりがち)
![スクリーンショット 2024-12-21 14.54.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535537/9ddc87bf-aae4-70f4-f0c5-1aa724e4d5a3.png)


# 作ってみた感想
社内報告会をしたところ
「いいじゃん」
「こういうのを待ってた」
とかなり好評だった

気を良くして実運用に向けた検討を始めた
ハードルになるのは以下の３点
- AI分析のトークン使用量の抑制
- 分析精度向上ため構成情報を用いた高度な推論(RAG)
- 機密性の高い情報の取り扱い
回答精度の高いローカルLLMが理想だ

しかしオンプレGPUサーバは高額。
無料商用利用可能なモデルも限られる。
そして現状のローカルLLMではGPT4o並の回答は期待できない。

また、エンジニアが本当に求めているのは自動復旧。
そのためには簡易な切り分けや復旧コマンドはAIに実施してほしい。
自律型AIエージェントの機能が必要だ。
誤った判断で被害を拡大させないため、コマンド権限の制限や監視が必要になる。

そんな展望を語っているといつの間にか「生成AIを用いた業務改善の推進者」のレッテルがつき、仕事が増えた。

のびのびハッカソンライフを満喫するために作ったプロダクトのせいで、ハッカソンに参加する時間が減りそうだ。