Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cwh Laravel Package

maxbanton/cwh

Monolog handler for AWS CloudWatch Logs. Send PHP application logs directly to CloudWatch with configurable log groups and streams, batching, and easy integration into Monolog-based stacks, including Laravel.

Deep Wiki
Context7

Getting Started

maxbanton/cwh is a thin Monolog handler that ships log records to AWS CloudWatch Logs via the official aws/aws-sdk-php. You construct a CloudWatchLogsClient, hand it to Maxbanton\Cwh\Handler\CloudWatch, and push it onto any Monolog logger. The handler buffers records and flushes them in batches, creating the log group and stream on first write if they don't exist.

1. Install:

composer require maxbanton/cwh

This pulls in aws/aws-sdk-php and expects Monolog ^2. Confirm the Monolog constraint in composer.json if you're also using Laravel's bundled Monolog.

2. Grant IAM permissions — the minimum set is:

{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogGroup",
    "logs:CreateLogStream",
    "logs:DescribeLogStreams",
    "logs:DescribeLogGroups",
    "logs:PutLogEvents",
    "logs:PutRetentionPolicy"
  ],
  "Resource": "arn:aws:logs:*:*:log-group:/your-app/*"
}

Prefer credential resolution via the SDK's default provider chain (IAM role, AWS_* env vars, ~/.aws/credentials) over hardcoding keys.

3. First log, outside a framework:

use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Maxbanton\Cwh\Handler\CloudWatch;
use Monolog\Logger;

$client = new CloudWatchLogsClient([
    'region'  => 'eu-west-1',
    'version' => 'latest',
]);

// v2 signature:
// ($client, $groupName, $streamName, $retentionDays = 14, $batchSize = 10000,
//  array $tags = [], $level = Logger::DEBUG, $bubble = true)
$handler = new CloudWatch(
    $client,
    '/app/my-service',
    'web-' . gethostname() . '-' . date('Y-m-d'),
    30,          // retention in days
    1000,        // batch size — see gotchas
    ['env' => 'prod'],
    Logger::INFO,
    true
);

$logger = new Logger('cloudwatch');
$logger->pushHandler($handler);

$logger->info('Order created', ['order_id' => 1234]);

The first request that writes to a brand-new log group/stream will incur a small latency spike (the group, stream, and retention policy are created lazily). Trigger one warm-up log at deploy time so your first real request isn't the one paying for it.

Where to look first: the CloudWatch class itself (constructor args, write/close, its create-or-get logic for groups/streams) and the README's framework examples. The handler is small — reading it once tells you exactly what happens on flush.

Implementation Patterns

Laravel: a cloudwatch channel. Bind the client in a service provider so Laravel's container can inject it into the handler, then reference the handler class in config/logging.php:

// app/Providers/AppServiceProvider.php
use Aws\CloudWatchLogs\CloudWatchLogsClient;

public function register(): void
{
    $this->app->singleton(CloudWatchLogsClient::class, fn () => new CloudWatchLogsClient([
        'region'  => config('services.cloudwatch.region', 'eu-west-1'),
        'version' => 'latest',
        // no credentials key -> default provider chain
    ]));
}
// config/logging.php
'cloudwatch' => [
    'driver'  => 'monolog',
    'handler' => \Maxbanton\Cwh\Handler\CloudWatch::class,
    'handler_with' => [
        'groupName'     => '/app/' . env('APP_NAME'),
        'streamName'    => sprintf('%s-%s-%s', env('APP_ENV'), gethostname(), date('Y-m-d')),
        'retentionDays' => 30,
        'batchSize'     => 500,
        'tags'          => ['laravel', env('APP_ENV')],
        'level'         => env('LOG_LEVEL', 'debug'),
    ],
    'formatter' => \Monolog\Formatter\JsonFormatter::class,
    'level'     => env('LOG_LEVEL', 'debug'),
],

Laravel resolves the handler with app()->make(...), merging handler_with; the CloudWatchLogsClient type hint is satisfied by the binding and the scalars come from the array. If your Laravel version trips on this, fall back to a custom channel closure:

Log::extend('cloudwatch', function (Application $app, array $config) {
    return (new Logger('cloudwatch'))
        ->pushHandler($app->make(CloudWatch::class, [
            'groupName'  => $config['group_name'],
            'streamName' => $config['stream_name'],
        ]));
});

Split log groups by concern, not by log level. CloudWatch pricing and query ergonomics reward separate groups: /app/{service} for application logs, /app/{service}/audit for security-relevant events, /aws/lambda/... for infrastructure. Within a group, encode environment and host in the stream name, since streams are how you slice a group in the console.

Stream rotation strategy. CloudWatch streams are append-only and never compacted. Use daily streams (date('Y-m-d')) or hourly streams for high-volume services. Never use a static stream name across many workers — concurrent writers to the same stream can collide (see gotchas). A good default: {env}-{hostname}-{date}.

Use JsonFormatter for CloudWatch Insights. Logs are only as useful as your ability to query them. JSON-formatted records let you run:

fields @timestamp, level_name, message, context.order_id
| filter level_name >= "ERROR"
| stats count() by context.order_id

Attach IDs you'll want to filter on directly in the context array — ['user_id' => ..., 'request_id' => ..., 'order_id' => ...] — rather than burying them in a message string.

Isolate failures from application flow. A CloudWatch outage, IAM mistake, or throttle shouldn't 500 your request. Wrap the handler:

use Monolog\Handler\WhatFailureGroupHandler;

$logger->pushHandler(new WhatFailureGroupHandler([$cloudWatchHandler]));

WhatFailureGroupHandler swallows handler exceptions. For critical audit logs, do the opposite: bubble the exception and fail loudly. Choose deliberately per channel.

Compose with deduplication in noisy paths. If you log inside a retry loop, push a DeduplicationHandler ahead of the CloudWatch handler, or set batchSize low and rate-limit context payloads — CloudWatch bills by bytes ingested, and stack traces are expensive.

Long-running processes (queue workers, Octane, Swoole). The handler batches. In a web request, PHP shutdown flushes the buffer. In a worker that runs for hours, the buffer may sit for hours — logs appear stale and a crash loses them entirely. Close explicitly:

// Octane
app('events')->listen(
    \Laravel\Octane\Events\RequestTerminated::class,
    fn () => app(\Monolog\Logger::class)->close()
);

For queue workers, call $logger->close() after each job (a small listener on JobProcessed) or accept a small batchSize so you flush frequently.

Local/dev parity. Never point dev at CloudWatch. Keep a stack or null channel for local; the CloudWatch channel should be production-only. Test the handler itself against a fake or with TestHandler, not against AWS.

Gotchas and Tips

The 256 KB limit beats the 10,000-event limit. PutLogEvents accepts at most 10,000 events and 256 KB per call. The handler's default batchSize is 10,000, which is meaningless for anything but trivial payloads — a few hundred lines with stack traces will exceed the byte limit and the call fails. Set batchSize to 500–1000 for typical apps, or trim context and truncate exceptions in your formatter. This is the single most common production failure with this package.

Stream-name collisions cause InvalidSequenceTokenException. AWS expects a sequence token per stream; the SDK handles it, but concurrent writers racing on the same stream can still conflict on the legacy API. Give each process its own stream (hostname, container ID, or PID in the name). If you see DataAlreadyAcceptedException, you're almost certainly double-flushing or racing streams.

Retention is set at creation time in practice. retentionDays is applied when the group is created. If you change the value in config later, verify the retention policy actually updated in the AWS console — otherwise your old groups keep the original retention. Set it via an infra tool (Terraform/CloudFormation) if retention must be auditable.

Tags only apply to new groups. Same story as retention — tags passed in the constructor are attached on creation and not retroactively.

Exceptions propagate by default. The handler does not catch SDK errors unless you wrap it. In production this means: revoked IAM permissions, a throttled account, or a transient AWS error becomes a 500. Decide per channel and wrap with WhatFailureGroupHandler (or a custom handler overriding write) for non-critical logs.

Latency. Every flush is a synchronous HTTPS round trip to AWS. On batch-heavy logging this can add 50–300 ms to a request at shutdown. Don't log at DEBUG on hot paths in production, and don't push per-query logs through this handler.

Timezone and timestamps. Monolog timestamps carry your PHP timezone; CloudWatch stores UTC milliseconds. If you log from hosts with mixed date.timezone settings, your Insights queries will be misaligned. Pin date.timezone = UTC in production php.ini — this is good practice regardless of logging.

Cost profile. Roughly $0.50/GB ingested and $0.03/GB stored (pricing changes; check current). A verbose app logging full request bodies can easily cost more than the infrastructure it runs on. Truncate payloads, prefer structured fields over verbose strings, use WhatFailureGroupHandler in dev, and consider a SamplingHandler from monolog for high-volume DEBUG paths.

No native async. Unlike some newer Monolog transports, this handler doesn't hand off to a background worker. If blocking shutdown flushes are unacceptable, run a log shipper sidecar (Fluent Bit, FireLens) and have the app write to stdout — usually the better architecture on ECS/EKS. Use this package when you specifically want direct-to-CloudWatch from PHP without a sidecar.

Monolog version lockstep. v2 of this package targets Monolog ^2. If you're upgrading to Monolog 3, check the constraint first — the handler API (AbstractProcessingHandler::write) is stable but constructors and formatter interfaces have shifted across major versions. Pin both and test the flush path in staging.

Extension points. The handler is small: subclass it and override write() to add redaction (strip password, token, authorization from context before formatting), or override close() to add instrumentation around flush timing. For anything more elaborate — per-record routing, custom retry — consider writing a small dedicated handler around CloudWatchLogsClient::putLogEvents directly, since the surface area you need is tiny.

Verify the flush, not just the call. In dev, test that $logger->close() actually pushes records by checking the stream in the console; a misconfigured handler that silently fails inside WhatFailureGroupHandler is a common "we're logging but nothing appears" incident. Log one boot-time record through every configured channel and assert it lands before you ship.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core