Search by

norialabs / cloudwatch

thekiharani

AWS CloudWatch log driver for Laravel. Drop-in replacement — just switch LOG_CHANNEL=cloudwatch.

Package info

github.com/norialabs/cloudwatch

pkg:composer/norialabs/cloudwatch

Statistics

Installs: 245

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.2 2026-10-01 01:33 UTC

This package is auto-updated.

Last update: 2026-10-01 01:34:38 UTC


README

CI Packagist

AWS CloudWatch Logs driver for Laravel. Drop-in replacement — just switch your log channel.

Requirements

  • PHP 8.2+
  • Laravel 11, 12, or 13
  • An AWS account with CloudWatch Logs access

Installation

composer require norialabs/cloudwatch

The service provider is auto-discovered. Publish the config file:

php artisan vendor:publish --provider="NoriaLabs\CloudWatch\CloudWatchServiceProvider"

This creates config/cloudwatch.php.

Quick Start

  1. Add a cloudwatch channel to config/logging.php:
'channels' => [
    // ...

    'cloudwatch' => [
        'driver' => 'cloudwatch',
        'level'  => env('LOG_LEVEL', 'debug'),
    ],
],
  1. Set your environment:
LOG_CHANNEL=cloudwatch
  1. Use Laravel's logger as normal:
Log::info('Payment processed', ['amount' => 1000, 'currency' => 'KES']);

That's it. Logs go to CloudWatch.

Configuration

All options are configured via environment variables with sensible defaults.

AWS Credentials

Variable Default Description
AWS_ACCESS_KEY_ID null AWS access key
AWS_SECRET_ACCESS_KEY null AWS secret key
AWS_DEFAULT_REGION us-east-1 AWS region for CloudWatch

If both key and secret are null, the SDK uses the default credential provider chain — IAM roles (EC2/ECS/Lambda), environment variables, ~/.aws/credentials, etc. This is the recommended approach for production.

Log Group

Variable Default Description
CLOUDWATCH_LOG_GROUP APP_NAME (or laravel) CloudWatch log group name

The log group is auto-created on the first log event if it doesn't exist.

Log Stream

Variable Default Description
CLOUDWATCH_LOG_STREAM {app}-{env} Stream name or template

The stream name supports dynamic placeholders that are resolved at flush time (not at boot time):

Placeholder Resolves to Example
{app} APP_NAME config value noria
{env} APP_ENV config value production
{date} Current date (Y-m-d) 2026-04-01
{hostname} Machine hostname web-01

Examples:

# Static stream (default)
CLOUDWATCH_LOG_STREAM="{app}-{env}"
# Result: noria-production

# Daily streams — each day gets its own stream automatically
CLOUDWATCH_LOG_STREAM="{app}-{env}-{date}"
# Result: noria-production-2026-04-01

# Per-host streams — useful for multi-server deploys
CLOUDWATCH_LOG_STREAM="{app}-{hostname}"
# Result: noria-web-01

# Combine all
CLOUDWATCH_LOG_STREAM="{app}-{env}-{hostname}-{date}"
# Result: noria-production-web-01-2026-04-01

Long-running processes: Placeholders are resolved every time the buffer is flushed, not once at startup. This means queue workers and schedulers will automatically create a new stream at midnight when using {date} — no restart required.

Log streams are auto-created on first use.

Log Retention

Variable Default Description
CLOUDWATCH_LOG_RETENTION 30 Retention in days, or null for forever

The retention policy is applied when the log group is created. Valid values: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653.

Set to null to retain logs indefinitely:

CLOUDWATCH_LOG_RETENTION=

Batch Size

Variable Default Description
CLOUDWATCH_BATCH_SIZE 25 Events buffered before flushing to CloudWatch

Logs are buffered in memory and sent in batches to reduce API calls. The buffer also flushes once it holds 1 MB, and every flush is split into requests CloudWatch accepts: at most 10,000 events, 1,048,576 bytes (26 bytes of overhead counted per event) and 24 hours apart. A single event over 256 KB is cut to fit. The buffer is flushed on close() and on reset(), so long-running workers (Octane, queue workers) should reset their loggers after each request or job.

  • Higher values (50-100): fewer API calls, better throughput, slightly delayed delivery
  • Lower values (1-5): near-real-time delivery, more API calls
  • Set to 1: every log is sent immediately (useful for debugging)

Log Level

Variable Default Description
CLOUDWATCH_LOG_LEVEL debug Minimum level to send to CloudWatch

Standard Monolog/PSR-3 levels: debug, info, notice, warning, error, critical, alert, emergency.

You can also override the level per-channel in config/logging.php:

'cloudwatch' => [
    'driver' => 'cloudwatch',
    'level'  => 'warning', // Only warnings and above
],

Tags

Tags are key-value pairs applied to the log group when it is created. Useful for cost allocation, filtering, and organization in the AWS console.

Configure in config/cloudwatch.php:

'tags' => [
    'team'    => 'backend',
    'project' => 'noria',
    'env'     => env('APP_ENV', 'production'),
],

Tags are only applied when the log group is first created. To update tags on an existing group, use the AWS console or CLI.

Usage Examples

Basic logging

use Illuminate\Support\Facades\Log;

Log::info('User registered', ['user_id' => 42]);
Log::error('Payment failed', ['order_id' => 'ORD-123', 'reason' => 'insufficient_funds']);
Log::debug('Cache miss', ['key' => 'user:42:profile']);

Stack driver

Send logs to multiple channels simultaneously:

// config/logging.php
'channels' => [
    'stack' => [
        'driver'   => 'stack',
        'channels' => ['single', 'cloudwatch'],
    ],
],

Local development with fallback

Use CloudWatch in production, local file in development:

# .env (local)
LOG_CHANNEL=single

# .env (production)
LOG_CHANNEL=cloudwatch

Multiple CloudWatch channels

Send different log types to different groups:

// config/logging.php
'channels' => [
    'cloudwatch' => [
        'driver' => 'cloudwatch',
        'level'  => 'info',
    ],

    'cloudwatch-errors' => [
        'driver' => 'cloudwatch',
        'level'  => 'error',
    ],
],

Override the group/stream per channel by setting different env vars or adjusting config/cloudwatch.php.

How It Works

  1. Driver registration — The service provider registers a cloudwatch driver with Laravel's log manager via Monolog.
  2. Buffering — Log events are buffered in memory until the batch size is reached or the application shuts down.
  3. Stream resolution — On each flush, the stream name template is resolved with current values ({date} = today). If the resolved name changed since the last flush, a new stream is created automatically.
  4. Auto-creation — The log group and stream are created on first use. If they already exist, the ResourceAlreadyExistsException is silently ignored.
  5. Flushing: Buffered events are sorted by timestamp, split into batches within CloudWatch's limits and sent via putLogEvents. The buffer is emptied before sending, so a failure is never retried on the next log call.
  6. Self-healing — If the stream is deleted externally mid-run, the handler catches the ResourceNotFoundException, recreates the group/stream, and retries the flush.
  7. Refusals: A batch CloudWatch refuses is written once to the fallback file with a warning, when a fallback_path is set; without one the error is thrown after the batch is dropped.
  8. Shutdown: Any remaining buffered logs are flushed on close() and reset().

All log entries are JSON-formatted via Monolog's JsonFormatter.

IAM Permissions

The AWS credentials you provide need the following permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:PutRetentionPolicy"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:YOUR_LOG_GROUP:*"
        }
    ]
}

Replace YOUR_LOG_GROUP with your actual log group name, or use * for all groups.

If you set retention to null, you can omit logs:PutRetentionPolicy.

Full Configuration Reference

# AWS (omit for IAM role)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1

# CloudWatch
CLOUDWATCH_LOG_GROUP=my-app                    # Default: APP_NAME
CLOUDWATCH_LOG_STREAM={app}-{env}              # Default: {app}-{env}
CLOUDWATCH_LOG_RETENTION=30                    # Default: 30 (days, null = forever)
CLOUDWATCH_BATCH_SIZE=25                       # Default: 25
CLOUDWATCH_LOG_LEVEL=debug                     # Default: debug
// config/cloudwatch.php — tags (not env-configurable)
'tags' => [
    'team'    => 'backend',
    'project' => 'noria',
],

Testing

composer test
# or
./vendor/bin/pest
./vendor/bin/pest --coverage

License

MIT

Fallback

CloudWatch being unreachable is exactly when you want the logs, so a line it will not take should not simply vanish. Give the channel a path and the handler is wrapped: CloudWatch first, a rotating file after it.

'cloudwatch' => [
    'driver' => 'cloudwatch',
    'level' => env('LOG_LEVEL', 'debug'),
    'fallback_path' => storage_path('logs/cloudwatch.log'),
    'fallback_days' => 14,
],

Set it once in config/cloudwatch.php to cover every channel instead. Without a path the old behaviour stands and an unreachable endpoint loses the line.

Per channel settings

Every setting may be given on the channel and falls back to config/cloudwatch.php, so two channels can differ - an application channel batching at 25 and an audit channel sending each line immediately:

'audit' => [
    'driver' => 'cloudwatch',
    'log_group' => 'noria-audit',
    'batch_size' => 1,
],

A level nobody recognises falls back to debug rather than throwing out of channel construction: a typo in one environment variable should not stop the application booting, and too much logging is the safer wrong answer.