運用中のAPIでエラーが出たときどうやって検知してSlackに通知しよう 〜実装編〜

# はじめに
これは [KWC Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kwc) の記事です。

はじめまして、KWCに新卒で入社して、バックエンドエンジニアをしているユウスケと申します。
テックブログは初めてです！

**\*紹介\***
昨日の記事で[運用中のAPIでエラーが出たときどうやって検知してSlackに通知しよう 〜概要編〜](https://qiita.com/Suzurikawa/items/cad72315337bb01bb414) の続きとなりますので、先に概要編を読むことをおすすめします！

<!-- ↓ 硯川さんの記事のリンクに変える -->
https://qiita.com/Suzurikawa/items/cad72315337bb01bb414

# 目次
1. [概要編をサクッとおさらい](#概要編をサクッとおさらい)
1. [システムの構成](#システムの構成)
1. [いざ実装へ](#いざ実装へ)
1. [まとめ](#まとめ)

# 概要編をサクッとおさらい
今回やりたいことは、**エラー出た際にSlackへログを通知する**ことです。

Slackへのメッセージ送信の機能の実装場所はプログラム内部に組み込むか、外部に実装する方法の２つあります。詳しくは[概要編](https://qiita.com/Suzurikawa/items/cad72315337bb01bb414)で！
今回は、外部に実装する方法を話していきます！

# システムの構成
<!-- 最終的にはSREの協力を得ながら次のような構成になりました。 -->
システム構成図は以下のようになります。
1. APIのログはCloudWatch Logsに溜まっていきます
1. CloudWatch　Logsでは、`Subscription filter`を使い特定の文字が含まれるログを検知します
    1. 今回は`_SLACK_NOTIFY`をトリガーにしています
1. 特定の文字があった場合は、そのログがeventとしてLambdaへ送られるようにしています
1. Lambdaの中でeventの中身を見て、必要な処理をしてからSlackにメッセージを送信します

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674343/1e8f55f1-5964-62de-3886-996b92dc7e90.png">

# いざ実装へ
## サブスクリプションフィルターで検知するログの作成

サブスクリプションフィルターで検知する`_SLACK_NOTIFY`を含んだログを出力します。

今回は下のようなログを出力します。
```sh:ログの例
2022/12/12 11:12:44 ERROR_SLACK_NOTIFY: エラーメッセージ
```

## サブスクリプションフィルターの設定
Cloud Watchのコンソール画面から、設定したいロググループへ移動して、サブスクリプションフィルターの設定を行います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2802860/8b5ceacb-781a-c7f3-ec64-5f94c82c9d08.png">

`Lambda サブスクリプションフィルターを作成`では以下の情報を入力します。

### 送信先を選択
- 送信したいLambdaを選択します
（作成済みのLambdaが表示されます）

### ログ形式とフィルターを設定
| 項目 | 内容 |
|:-|:-|
| ログの形式  |  その他 |
| フィルターパターン  |  _SLACK_NOTIFY |
|  フィルター名 | 任意の名前  |

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2802860/06cf508b-d4ac-4069-a385-ca63d9dc56ab.png" width=70%>

これでCloud Watch Logsへ`_SLACK_NOTIFY`のログが出力された時、サブスクリプションフィルターが起動して、Lambdaが起動します。

## Slackへメッセージ送信

### メッセージ取り出し

サブスクリプションフィルターから次のような`event`がLambdaへ渡されます。
```json
{
    "owner": "<aws account>",
    "logGroup": "XXXXXXXXXXXXXXXX",
    "logStream": "XXXXXXXXXXXXXXX",
    "subscriptionFilters": [
        "設定したフィルター名"
    ],
    "messageType": "DATA_MESSAGE",
    "logEvents": [
        {
            "id": "3720074778603986400000000000000",
            "timestamp": 1668139223818,
            "message": "ERROR_SLACK_NOTIFY: エラーメッセージ"
        }
    ]
}
```

次のコードは`event`からログを取り出している例です。
```go
func HandleRequest(ctx context.Context, event events.CloudwatchLogsEvent) (string, error) {
	e, _ := event.AWSLogs.Parse() // サブスクリプションフィルターから渡されたeventをパース
	message := e.LogEvents[0].Message
    fmt.Println(message)
    
	return "", nil
}
```

<details>
<summary>全体のコード（APIのログを取り出す例）</summary>

```go
package main
​
import (
	"context"
	"encoding/json"
	"fmt"
​
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
​
func HandleRequest(ctx context.Context, event events.CloudwatchLogsEvent) (string, error) {
	e, _ := event.AWSLogs.Parse()
	message := e.LogEvents[0].Message
    fmt.Println(message)
    
	return "", nil
}
​
func main() {
	lambda.Start(HandleRequest)
}
```
</details>

**Slackへメッセージを送る関数作成**

SlackのSDKを使い、Slackへメッセージを送信します。
```go
func SendMessage(message string) {
	c := slack.New(os.Getenv("SLACK_TOKEN"))

	_, _, err := c.PostMessage(os.Getenv("POST_CHANNEL"), slack.MsgOptionBlocks(
		&slack.SectionBlock{
			Type: slack.MBTSection,
			Text: &slack.TextBlockObject{
				Type: "mrkdwn",
				Text: message,
			},
		},
	))
	if err != nil {
		panic(err)
	}
}
```
この関数の引数に、イベントから取得したログを渡してSlackへ通知することができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2802860/3545e4a8-b366-6022-8679-233e1f4ddf61.jpeg" width=50%>

<details>
<summary>完成したコード</summary>

```go:main.go
package main

import (
	"context"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/slack-go/slack"
)

func SendMessage(message string) {
	c := slack.New(os.Getenv("SLACK_TOKEN"))

	_, _, err := c.PostMessage(os.Getenv("POST_CHANNEL"), slack.MsgOptionBlocks(
		&slack.SectionBlock{
			Type: slack.MBTSection,
			Text: &slack.TextBlockObject{
				Type: "mrkdwn",
				Text: message,
			},
		},
	))
	if err != nil {
		panic(err)
	}
}

func HandleRequest(ctx context.Context, event events.CloudwatchLogsEvent) (string, error) {
	e, _ := event.AWSLogs.Parse()
	message := e.LogEvents[0].Message

	SendMessage(message)

	return "", nil
}

func main() {
	lambda.Start(HandleRequest)
}

```
</details>

# まとめ
アプリが出した特定のログを検知してSlackへ通知する方法を紹介しました。
これは一例なので環境にあった実装を試してみてください！

# 宣伝
[KWC Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kwc)はまだまだ、続きますので良ければ他の記事もご覧ください！！