【AWS】LaravelでCloudWatchへログを送る方法

2026年1月24日AWS,CloudWatch

aws

kamiです。
TwitterYoutubeもやってます。

今回はLaravelでCloudWatchへログを送る方法の紹介です。

CloudWatchへログを送る方法でやること

  • LaravelのログをAWS CloudWatchに送るためのカスタムロガーの作成
  • config/logging.phpにCloudWatchカスタムロガーを使用する記述
  • .env に AWS 関連の環境変数を設定す

AWS CloudWatchに送るためのカスタムロガーの作成

  • AWSの設定(リージョン・認証情報)を $awsConfig に構築
  • CloudWatchLogsClient を生成
  • .env の値を元にロググループ名とストリーム名を設定
  • ロググループが存在するか確認、なければ作成(保持期間も設定)
  • ログストリームが存在するか確認、なければ作成
  • CloudWatchLogsHandler を生成し、バッチサイズ・保持期間を指定
  • JSONフォーマッタを適用(構造化ログ)
  • Monolog Logger を作成し、ハンドラを登録
  • 各種プロセッサ(Web情報、メモリ、ファイル行番号)を追加
  • 環境情報 app_env を extra に追加するカスタムプロセッサを追加
  • 最終的に Logger を返却

__invokeメソッド

CloudWatch 用のロガーを作成する本体です。

public function __invoke(array $config) {
  // 以下省略
}

ensureLogGroupメソッド

ロググループを確認・作成します。

private function ensureLogGroup(CloudWatchLogsClient $client, string $groupName, $retentionDays) {
  // 以下省略
}

ensureLogStreamメソッド

ログストリームを確認・作成します。

private function ensureLogStream(CloudWatchLogsClient $client, string $groupName, string $streamName) {
  // 以下省略
}

CloudWatchLoggerの全体のコード

<?php

namespace App\Logging;

use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Aws\CloudWatchLogs\Exception\CloudWatchLogsException;
use Maxbanton\Cwh\Handler\CloudWatch as CloudWatchLogsHandler;
use Monolog\Logger;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\MemoryUsageProcessor;
use Illuminate\Support\Facades\Log;
use Exception;

class CloudWatchLogger
{
    /**
     * Create a custom Monolog instance.
     *
     * @param  array  $config
     * @return \Monolog\Logger
     */
    public function __invoke(array $config)
    {
        try {
            $awsConfig = [
                'region' => $config['region'], // AWS_DEFAULT_REGION
                'version' => 'latest'
            ];

            // 認証情報を設定
            $awsConfig['credentials'] = [
                'key' => $config['key'], // AWS_ACCESS_KEY_ID
                'secret' => $config['secret'] // AWS_SECRET_ACCESS_KEY
            ];

            // CloudWatchLogsクライアントの生成
            $client = new CloudWatchLogsClient($awsConfig);

            // ロググループとストリーム名の設定
            $groupName = $config['group'];
            $streamName = $config['stream'];

            // ロググループの確認と作成
            $this->ensureLogGroup($client, $groupName, $config['retention']);
            
            // ログストリームの確認と作成
            $this->ensureLogStream($client, $groupName, $streamName);

            // ハンドラの設定
            $handler = new CloudWatchLogsHandler(
                $client,
                $groupName,
                $streamName,
                $config['retention'],
                $config['batch_size']
            );

            // JSONフォーマッタを適用(構造化ログ)
            $handler->setFormatter(new JsonFormatter());

            // Loggerインスタンスの作成と追加情報の設定
            $logger = new Logger('cloudwatch');
            $logger->pushHandler($handler);

            // 追加のプロセッサで有用な情報を含める
            $logger->pushProcessor(new WebProcessor());
            $logger->pushProcessor(new IntrospectionProcessor());
            $logger->pushProcessor(new MemoryUsageProcessor());

            // カスタムプロセッサでアプリケーション情報を追加
            $logger->pushProcessor(function ($record) {
                $record['extra']['app_env'] = env('APP_ENV');
                return $record;
            });

            return $logger;

        } catch (Exception $e) {
            $today = date('Y-m-d');
            $logPath = storage_path('logs/laravel-' . $today . '.log');
            
            $logger = new Logger('cloudwatch-error');
            $handler = new StreamHandler($logPath);
            $logger->pushHandler($handler);

            return $logger;
        }
    }

    /**
     * ロググループの確認と作成
     * @param CloudWatchLogsClient $client
     * @param string $groupName
     * @param int $retentionDays
     * @return bool
     */
    private function ensureLogGroup(CloudWatchLogsClient $client, string $groupName, $retentionDays)
    {
        try {
            // 既存のロググループを検索
            $response = $client->describeLogGroups([
                'logGroupNamePrefix' => $groupName
            ]);

            $exists = false;
            foreach ($response['logGroups'] as $group) {
                if ($group['logGroupName'] === $groupName) {
                    $exists = true;
                    break;
                }
            }

            // 存在しない場合は作成
            if (!$exists) {
                $client->createLogGroup([
                    'logGroupName' => $groupName
                ]);

                // 保持期間が失効しない場合は設定しない
                if ($retentionDays !== null) {
                    // 保持期間を設定
                    $client->putRetentionPolicy([
                      'logGroupName' => $groupName,
                      'retentionInDays' => $retentionDays
                    ]);
                }

            }

            return true;
        } catch (CloudWatchLogsException $e) {
            // 既に存在する場合は無視(競合状態の対応)
            if (strpos($e->getMessage(), 'ResourceAlreadyExistsException') !== false) {
                return true;
            }

            throw $e;
        }
    }

    /**
     * ログストリームの確認と作成
     * @param CloudWatchLogsClient $client
     * @param string $groupName
     * @param string $streamName
     * @return bool
   
    private function ensureLogStream(CloudWatchLogsClient $client, string $groupName, string $streamName)
    {
        try {
            // 既存のログストリームを検索
            $response = $client->describeLogStreams([
                'logGroupName' => $groupName,
                'logStreamNamePrefix' => $streamName
            ]);

            $exists = false;
            foreach ($response['logStreams'] as $stream) {
                if ($stream['logStreamName'] === $streamName) {
                    $exists = true;
                    break;
                }
            }

            // 存在しない場合は作成
            if (!$exists) {
                $client->createLogStream([
                    'logGroupName' => $groupName,
                    'logStreamName' => $streamName
                ]);
            }

            return true;
        } catch (CloudWatchLogsException $e) {
            // 既に存在する場合は無視(競合状態の対応)
            if (strpos($e->getMessage(), 'ResourceAlreadyExistsException') !== false) {
                return true;
            }

            throw $e;
        }
    }

}

スポンサードサーチ

config/logging.phpの設定

default

  • アプリ全体のデフォルトログチャネルを指定します。
  • .env で LOG_CHANNEL を指定しなければ、stack が使われます。
'default' => env('LOG_CHANNEL', 'stack'),

channels

ログチャネル(出力先)を一覧で定義します。

この例ではcloudwatchのみにしていますが、複数のチャネルを記述すると、まとめてログを送ることができます。

'stack' => [
    'driver' => 'stack',
    'channels' => ['cloudwatch'],
    'ignore_exceptions' => false,
],

config/logging.phpの全体のコード

<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [

    /*
    |--------------------------------------------------------------------------
    | Default Log Channel
    |--------------------------------------------------------------------------
    |
    | This option defines the default log channel that gets used when writing
    | messages to the logs. The name specified in this option should match
    | one of the channels defined in the "channels" configuration array.
    |
    */

    'default' => env('LOG_CHANNEL', 'stack'),

    /*
    |--------------------------------------------------------------------------
    | Log Channels
    |--------------------------------------------------------------------------
    |
    | Here you may configure the log channels for your application. Out of
    | the box, Laravel uses the Monolog PHP logging library. This gives
    | you a variety of powerful log handlers / formatters to utilize.
    |
    | Available Drivers: "single", "daily", "slack", "syslog",
    |                    "errorlog", "monolog",
    |                    "custom", "stack"
    |
    */

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['cloudwatch'],
            'ignore_exceptions' => false,
        ],

        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'info',
            'days' => 5,
        ],

        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => 'critical',
        ],

        'papertrail' => [
            'driver' => 'monolog',
            'level' => 'debug',
            'handler' => SyslogUdpHandler::class,
            'handler_with' => [
                'host' => env('PAPERTRAIL_URL'),
                'port' => env('PAPERTRAIL_PORT'),
            ],
        ],

        'stderr' => [
            'driver' => 'monolog',
            'level' => env('APP_DEBUG', false) ? 'debug' : 'info',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
        ],

        'syslog' => [
            'driver' => 'syslog',
            'level' => 'debug',
        ],

        'errorlog' => [
            'driver' => 'errorlog',
            'level' => 'debug',
        ],

        'null' => [
            'driver' => 'monolog',
            'handler' => NullHandler::class,
        ],

        'emergency' => [
            'path' => storage_path('logs/laravel.log'),
        ],

        'cloudwatch' => [
            'driver' => 'custom',
            'via' => \App\Logging\CloudWatchLogger::class,
            'region' => env('AWS_DEFAULT_REGION'),
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'group' => '/ecs/' . env('APP_ENV') . '-logs',
            'stream' => date('Y-m-d'),
            'retention' => null,
            'batch_size' => 10000,
        ],

    ],
];

.env

.envで設定内容を記述します。

LOG_CHANNEL=stack
APP_ENV=production
AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=your_aws_access_key_id
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key

以上です。

AWS,CloudWatchAWS

Posted by kami