【CloudWatch / Laravel / Fargate】特定のログだけ別のロググループに出力したい


## 特定のログだけ別のロググループに出力したい

Fargateでアプリを動かしている場合、標準エラーを拾ってそれをCloudWatchに流しています。
しかし、基本的なログとは別に、特定の操作だけロググループを別にしてログを収集したくなることがあります。
また、吐き出し先のログストリーム名は決まっていて、オートスケーリングでコンテナの数が増えても、全てのコンテナから同じログストリームに出力して欲しい。
何かいい方法がないかなと色々調べていました。

https://qiita.com/ggg-mzkr/items/0201b411425a75deca52

あんまりログの出し分けをしているものがなく、現在のプロジェクトではログチャネルはstderrで使ってしまっているので、
とりあえず自分でハンドラーを作ってみました。

## 実装

```php: App\Aws\CloudWatchLogsHandler
<?php

namespace App\Aws;

use Aws\CloudWatchLogs\Exception\CloudWatchLogsException;
use Exception;
use Illuminate\Support\Facades\Log;

class CloudWatchLogsHandler
{
    private $logClient;
    private $hogeLogGroupName;
    private $fugaLogStreamName;

    public function __construct()
    {
        // aws-sdk-php-laravel を使用しています。
        $this->logClient = app('aws')->createClient('logs');
        
        // ログストリームとグループ名を環境変数に設定し、 config/aws.php からそれを取得する。
        $this->hogeLogGroupName = config('aws.cloudwatch_logs.hoge_log_group_name');
        $this->fugaLogStreamName = config('aws.cloudwatch_logs.fuga_log_stream_name');
    }

    public function putFugaLogsToCloudWatch(string $data, int $retryNumber): void
    {
        // タイムスタンプ x 1000 した値でないと受け付けてくれないので整形しています。
        $timestamp = now()->getTimestamp() * 1000;

        // 出力したいデータを整形
        $logMessage = 'Id:' . $data->id
            . "\nattr1:" . $data->attr1
            . "\nattr2:" . $data->attr2;

        // あらかじめログストリームに1つ以上ログを入れておく必要があります。
        for ($i = 1; $i <= $retryNumber; $i++) {
            try {
                $sequenceToken = $this->logClient->describeLogStreams([
                    'logGroupName' => $this->hogeLogGroupName,
                    'logStreamNamePrefix' => $this->fugaLogStreamName,
                ])->get('logStreams')[0]['uploadSequenceToken'];

                $this->logClient->putLogEvents(array(
                    'logGroupName' => $this->hogeLogGroupName,
                    'logStreamName' => $this->fugaLogStreamName,
                    'logEvents' => [
                        [
                            'timestamp' => $timestamp,
                            'message' => $logMessage,
                        ],
                    ],
                    'sequenceToken' => $sequenceToken,
                ));

                break;
            } catch (CloudWatchLogsException $e) {
                if ($i === $retryNumber) {
                    Log::Error('CloudWatchLogsへのログ送信に失敗しました', [$data, $e->getMessage()]);
                }
            } catch (Exception $e) {
                Log::Error('CloudWatchLogsへのログ送信に失敗しました', [$data, $e->getMessage()]);
                break;
            }
        }
    }
}

```

実際に使う場合にはこんな感じ↓

``` php
$handler = new CloudWatchLogsHandler;
$handler->putFugaLogsToCloudWatch($data, 20);
```

## 考慮すべき点

#### 1. SequenceToken

CloudWatchへログをPUTするためには、SequenceToken という値が必要になります。ログストリームに最後にPUTした際に発行され、それを一緒に送信しないとPUTができません。（初回は不要)
今回はあらかじめ作ったログストリームに追加していくだけなので、初回かそうでないかを考慮しなくてもいいのですが、新しくログストリームを作成する場合はその辺りも考慮する必要があります。
この辺りはクラメソさんの記事がわかりやすいです。

https://dev.classmethod.jp/articles/operate-cloudwatch-log-group-using-cli-for-beginners/

#### 2. リトライ

複数の人が同時にアクセスするなど、ログ出力をする処理が頻繁に叩かれる場合、SequenceTokenの都合上、ログのPUTが失敗する可能性があります。
そのため、失敗した場合にリトライさせる仕組みが必要になります。
処理が頻繁に発生する場合はこの辺りをキューにする必要がでてきます。

#### 3. タイムスタンプ

putLogEvents は timestamp の引数を取りますが、その値は int を要求します。
しかし、単純にunixtimeを入れるのではなく、それを1000倍した値でないと受け付けてくれないので注意が必要です。

## 総評

2.がかなりネックになっているので、上記の実装だとあまりよろしくないですね。
SequenceTokenが厄介です。
ちゃんと綺麗に実装するなら、キューを一元処理させる仕組みが必要そう。
今のところは頻繁に処理が走る部分ではないのでこれでもいいですが...

何かおすすめの方法があったら教えていただけると嬉しいです :bow: