michimani.net

AWS SDK for Go で CloudWatch Logs にログイベントを送信する

2020-09-07

Go 言語で実装されたアプリケーションから AWS リソースを操作する際には AWS SDK for Go を使いますが、今回は その SDK を使って CloudWatch Logs にログイベントを送信してみます。

前に AWS CLI からログイベントを送信したときの記事はこちらです。

目次

概要

前回 の記事にも書きましたが、あらためて CloudWatch Logs を構成する 3 つの要素について簡単に書いておきます。

ロググループ

アプリケーション単位や画面単位といったくくりでログをグループ分けしている要素です。例えば Lambda の実行ログに関しては、関数単位でロググループが作成されています。

ログストリーム

ロググループ内でさらにログを分割している要素がログストリームです。同じアプリケーションであっても、別のバージョンや別日付などでストリームを分けることになると思います。Lambda の実行ログを例にすると、関数のバージョン単位、一定時間単位でストリームが分割されています。

ロググループ内には数の制限なくログストリームを作成することができます。一つだけでもいいです。ただし、最低でも日付ごととかで分割してあると、あとでログを閲覧するときに見つけやすいです。 また、ストリームを分けたとしても、マネジメントコンソール上ではストリームを横断したログの検索が可能なので、心配ありません。

ログイベント

いわゆるログのことです。

AWS SDK for Go による実装

では、 AWS SDK for Go を使った CloudWatch Logs へのログイベント送信の実装についてみていきます。

ちなみに、インポートするパッケージは次の通りです。

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
)

CloudWatch Logs クライアントの生成

まずは CloudWatch Logs のクライアントを生成します。

region := "ap-northeast-1"
client := cloudwatchlogs.New(
	session.Must(session.NewSession()),
	aws.NewConfig().WithRegion(region),
)

リージョンを指定してクライアントを生成します。

ロググループの作成

続いて、ロググループを作成します。作成には、 CreateLogGroup() を使います。

groupName := "/sample/group/name"
var createLogGroupIn *cloudwatchlogs.CreateLogGroupInput = &cloudwatchlogs.CreateLogGroupInput{
	LogGroupName: aws.String(groupName),
}

_, err := client.CreateLogGroup(createLogGroupIn)

既にロググループが存在している場合は、 ResourceAlreadyExistsException がエラーで返却されます。

ログストリームの生成

続いてはログストリームの作成です。作成には CreateLogStream() を使います。

groupName := "/sample/group/name"
streamName := "sample-stream-name"
var createLogStreamIn *cloudwatchlogs.CreateLogStreamInput = &cloudwatchlogs.CreateLogStreamInput{
	LogGroupName:  aws.String(groupName),
	LogStreamName: aws.String(streamName),
}

_, err := client.CreateLogStream(createLogStreamIn)

ログストリームに関しても、既に存在している場合は、 ResourceAlreadyExistsException がエラーで返却されます。また、指定したロググループが存在していない場合には ResourceNotFoundException が返却されます。

ログイベントの送信

最後に、ログイベントの送信です。送信には PutLogEvents() を使います。メソッド名からわかるように、複数のログイベントを送信することができます。

groupName := "/sample/group/name"
streamName := "sample-stream-name"
message := "This is a sample log message."
timestamp := time.Now().Unix()

timestampDigits := len(strconv.FormatInt(timestamp, 10))
var multipyNum int64 = 10
for n := 0; n < (13 - timestampDigits); n++ {
  timestamp = timestamp * multipyNum
}

var logEvents []*cloudwatchlogs.InputLogEvent = []*cloudwatchlogs.InputLogEvent{
	{
		Message:   aws.String(message),
		Timestamp: aws.Int64(timestamp),
	},
}

var putLogEventIn *cloudwatchlogs.PutLogEventsInput = &cloudwatchlogs.PutLogEventsInput{
  LogGroupName:  aws.String(groupName),
  LogStreamName: aws.String(streamName),
  LogEvents:     logEvents,
}

_, err := client.PutLogEvents(putLogEventIn)

CLI での操作のときも書きましたが、ログイベント用のタイムスタンプは 13 桁 である必要があります。 13 桁というか、 ミリ秒 まで含んだタイムスタンプというイメージです。

CloudWatch Logs では、過去または未来のタイムスタンプをもつイベントは記録されません。

  1. If the timestamp of log event is more than 2 hours in future, the log event is skipped.
  2. If the timestamp of log event is more than 14 days in past, the log event is skipped.

CloudWatch Logs Agent Reference - Amazon CloudWatch Logs

ログストリームにログイベントが既に存在している場合

既にログイベントが存在しているログストリームに対して追加でログイベントを送信する場合には、 InputLogEvent.SequenceToken に対して値を指定する必要があります。

ここで指定する値は、 PutLogEventsInput() のレスポンス内にある PutLogEventsOutput.NextSequenceToken です。が、連続したログイベント送信処理でない場合、この値をどこかに保持しておく必要があります。それは面倒なので、 DescribeLogStreams() で取得したログストリームの情報から取得します。

例えば次のような関数を用意して、ログストリーム名から NextSequenceToken を取得できるようにしておきます。

func GetNextSequenceToken(groupName string, streamName string) *string {
	var nextSeqToken string = ""

	var describeLogStreamsIn *cloudwatchlogs.DescribeLogStreamsInput = &cloudwatchlogs.DescribeLogStreamsInput{
		LogStreamNamePrefix: aws.String(streamName),
		LogGroupName:        aws.String(groupName),
	}
	out, err := client.DescribeLogStreams(describeLogStreamsIn)

	if err != nil {
		fmt.Println(err.Error())
		return aws.String("")
	}

	if len(out.LogStreams) == 0 {
		return aws.String("")
	}

	for _, logStream := range out.LogStreams {
		if *logStream.LogStreamName == streamName {
			if logStream.UploadSequenceToken != nil {
				nextSeqToken = *logStream.UploadSequenceToken
				break
			}
		}
	}

	return aws.String(nextSeqToken)
}

これで値が取得できた かつ 空文字でない場合には、先ほどの InputLogEventSequenceToken を追加して PutLogEvents() を実行します。

var nextSeqToken *string = GetNextSequenceToken(groupName, streamName)
if *nextSeqToken != "" {
	putLogEventIn.SequenceToken = nextSeqToken
}

まとめ

AWS SDK for Go を使った CloudWatch Logs にログイベントを送信してみた話でした。
SDK を使うとポインタ型への変換などが発生するので少しだけややこしいということで、今回紹介した処理をまとめたものを GitHub に置きました。

Go での自作パッケージの構成についてはまだまだ全然わかってないのですが、とりあえず go get して使える状態にはなっています。 今後、ぼちぼち良い感じにしていけたら良いなーと思ってます。


comments powered by Disqus