Laravel 8 logging AWS Cloudwatch
- Tram Ho
Preamble
Logging is an extremely important thing, and Cloudwatch is a pretty good visualization and query logging platform from AWS, surely systems on AWS often log to this email, today I would like to introduce how to make Laravel 8 can easily push log to Cloudwatch, let’s get started
How to implement
Install packages
1 2 | composer require maxbanton/cwh:^2.0 |
Create class CloudWatchLoggerFactory to create a custom drive that pushes to cloudwatch
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 | class CloudWatchLoggerFactory { /** * Create a custom Monolog instance. * * @param array $config * @return \Monolog\Logger */ public function __invoke(array $config) { $sdkParams = $config['sdk']; $tags = $config['tags'] ?? []; $name = $config['name'] ?? 'cloudwatch'; // Instantiate AWS SDK CloudWatch Logs Client $client = new CloudWatchLogsClient($sdkParams); // Log group name, will be created if none $groupName = $config['group']; // Log stream name, will be created if none $streamName = $config['stream']; // Days to keep logs, 14 by default. Set to `null` to allow indefinite retention. $retentionDays = $config['retention']; // Instantiate handler (tags are optional) $handler = new CloudWatch($client, $groupName, $streamName, $retentionDays, 10000, $tags); // Create a log channel $logger = new Logger($name); // Set handler $logger->pushHandler($handler); return $logger; } } |
In config/logging.php, add custom driver setting for this type of log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['single'], 'ignore_exceptions' => false, ], 'cloudwatch_error_log' => [ 'driver' => 'custom', 'via' => \App\Services\CloudWatchLoggerFactory::class, 'sdk' => [ 'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-1'), 'version' => 'latest', ], 'retention' => 30, 'level' => 'info', 'group' => env('CLOUDWATCH_LOG_GROUP', 'group-log'), 'stream' => env('CLOUDWATCH_LOG_STREAM', 'error-log'), ], |
Choose a reasonable exception to push to cloudwatch, here I will edit it in app/Exceptions/Handler.php, this is where laravel supports us to customize the entire Exception handling process, including logging error and response format API error, like 404 error, what 500 error is returned, … for my part, I attached the request param to the error, so we will capture exactly which request has been requested. cause errors, more convenient for debugging.
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 | /** * Report or log an exception. * * @param Throwable $e * @return void * * @throws Throwable */ public function report(Throwable $e) { $exceptionExcluse = [ RouteNotFoundException::class, NotFoundHttpException::class, AuthorizationException::class, ValidationException::class, ]; if (!in_array(get_class($e), $exceptionExcluse)) { $this->logPrettyError($e); } parent::report($e); } |
As you can see, common errors like RouteNotFoundException or NotFoundHttpException or ValidationException, are caused by the user entering the wrong url or form input, so it’s not important enough for us to log back to investigate :#)
Error log function add request param:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private function logPrettyError(Throwable $e) { $request = request(); $log = [ 'access' => [ 'request' => $request->all(), 'method' => $request->method(), 'path' => $request->path(), ], 'error' => [ 'class' => get_class($e), 'code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ], ]; getLogger()->error(json_encode($log)); } |
Don’t forget to choose the correct logging driver for the environment you need to log cloudwatch (usually STG and PROD)
1 2 3 4 | LOG_CHANNEL=cloudwatch_error_log CLOUDWATCH_LOG_GROUP= CLOUDWATCH_LOG_STREAM= |
Add 2 helpers to make logging in code more convenient
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 | /** * Get logger */ if (!function_exists('getLogger')) { function getLogger() { return Log::channel(env('LOG_CHANNEL', 'daily')); } } /** * Log info */ if (!function_exists('logInfo')) { function logInfo($info) { getLogger()->info($info); } } /** * Log error */ if (!function_exists('logError')) { function logError($e) { $logger = getLogger(); if ($e instanceof Exception) { $logger->error($e->getMessage() . ' on line ' . $e->getLine() . ' of file ' . $e->getFile()); } else { $logger->error($e); } } } |
How to filter on Cloudwatch convenient for reading logs
Based on the log above, I have a few more convenient ways to query the log
- query log get the last 20
1 2 | fields @timestamp, @message | sort @timestamp desc | limit 25 |
- query get number of messages containing Exception within 1h
1 2 3 4 | filter @message like /Exception/ | stats count(*) as exceptionCount by bin(1h) | sort exceptionCount desc |

- query log does not contain Exception
1 2 | fields @message | filter @message not like /Exception/ |
- query read error log coming from product API
1 2 3 | fields @timestamp, @message | sort @timestamp desc | filter @message like /ERROR(.*)api\\\/v1\\\/products/ |

Refer: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html