# CloudWatch Logsのログ本文をDiscordへ送るAWS Lambda関数

2021/10/16

Slackへ送るサンプルはネットに大量に上がっているのですが、Discordへ送る例がネットに転がってなかったのでメモ。

# リポジトリ

uda-cha/cloudwatch_logs_to_discord (opens new window)

# 構成

おおむねリポジトリのREADME.mdに書いてある通りです。

CloudWatch Log の Subscription Filterを作成し、そこでフィルタに引っかかったログデータをlambdaへ送ります。

lambdaでは、そのログデータから送りたいデータのみを取りだし、DiscordのWebhookへ送りつけます。

言語はなんでもいいのですが、今回はGoで書いてみました。

package main

import (
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/DisgoOrg/disgo/discord"
	"github.com/DisgoOrg/disgo/webhook"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-lambda-go/lambdacontext"
)

type DiscordWebHookConfig struct {
	webhookID    string
	webhookToken string
}

func (c DiscordWebHookConfig) Validate() (err error) {
	if len(c.webhookID) == 0 || len(c.webhookToken) == 0 {
		return fmt.Errorf("environment variables must be set")
	}

	return
}

func CreateDiscordWebHookConfig() (config DiscordWebHookConfig, err error) {
	config = DiscordWebHookConfig{
		webhookID:    os.Getenv("WEBHOOK_ID"),
		webhookToken: os.Getenv("WEBHOOK_TOKEN"),
	}

	err = config.Validate()

	return config, err
}

func CreateDiscordWebHookClient() (client *webhook.Client, err error) {
	config, err := CreateDiscordWebHookConfig()

	if err != nil {
		return nil, err
	}

	client = webhook.NewClient(discord.Snowflake(config.webhookID), config.webhookToken)

	return client, nil
}

// Discord Webhookで一度に送ることができるサイズである2000バイト以内にスライスの各要素を再構築する
func ReconstructSlicesforDiscordLimit(msgSlice []string) (newMsgSlice []string) {
	var currentSlice []string

	for i, msg := range msgSlice {
		if i == 0 {
			currentSlice = append(currentSlice, msg)
		} else {
			if len(strings.Join(currentSlice, "\r")+msg) >= 2000 {
				str := strings.Join(currentSlice, "\r")
				newMsgSlice = append(newMsgSlice, str)
				currentSlice = []string{msg}
			} else {
				currentSlice = append(currentSlice, msg)
			}
		}
	}

	if len(currentSlice) > 0 {
		newMsgSlice = append(newMsgSlice, strings.Join(currentSlice, "\r"))
	}

	return newMsgSlice
}

func SendToDiscord(msgSlice []string) (err error) {
	msgUnits := ReconstructSlicesforDiscordLimit(msgSlice)

	client, err := CreateDiscordWebHookClient()

	if err != nil {
		return err
	}

	var errs []error

	for _, msg := range msgUnits {
		if _, err := client.CreateContent(msg); err != nil {
			errs = append(errs, err)
		}
	}

	if len(errs) > 0 {
		return fmt.Errorf("errors: %v", errs)
	}

	return
}

func ParseAWSLogsToStringSlice(awsLogs events.CloudwatchLogsRawData) (msgSlice []string, err error) {
	data, err := awsLogs.Parse()
	if err != nil {
		return nil, err
	}

	for _, logEvent := range data.LogEvents {
		msgSlice = append(msgSlice, logEvent.Message)
	}

	return msgSlice, nil
}

func HandleRequest(ctx context.Context, event events.CloudwatchLogsEvent) (string, error) {
	lc, _ := lambdacontext.FromContext(ctx)

	msgSlice, err := ParseAWSLogsToStringSlice(event.AWSLogs)

	if err != nil {
		return fmt.Sprintf("Failed to parse log. request id: %s.", lc.AwsRequestID), err
	}

	if err := SendToDiscord(msgSlice); err != nil {
		return fmt.Sprintf("Failed to send log. request id: %s.", lc.AwsRequestID), err
	}

	return fmt.Sprintf("Success to send %d logs.", len(msgSlice)), nil
}

func main() {
	lambda.Start(HandleRequest)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

# ハマったこと

# DiscordのWebhookで投げることができる文字数は2000文字まで

400 bad requestとか返ってきていたらこの制限に引っかかっている可能性が高いです。

おそらくこの制限に引っかかっため、CloudWatchのLogEventsのMessageたちを2000文字ごとに分割してリクエストを投げるようにしました。

あらゆるAPIの4xxエラーレスポンスにはきちんとその理由を書いてほしいですね。。。

自分もAPI実装するとき気を付けないといけないですが。

# DiscordのWebhookはレートリミットがある

そりゃあるよね。

短い時間にバンバカ大量にリクエスト送るとtoo many requestsと怒られます。

# 感想

マイクラサーバをECSで立てる (opens new window)の記事で書いたログ監視をこれでやってます。

複雑なinputが来るものは静的型付け言語が楽でいいですね。

https://github.com/aws/aws-lambda-go (opens new window)に、lambdacontext/CloudwatchLogsEventの型定義があったので、自分で頑張ってinputされたJsonをパースする構造体を書かなくて良いのが楽でした。

コメント

コメントする

name
content