News

Monthly Log Driver in Laravel 13.23

Published
Monthly Log Driver in Laravel 13.23 image

Release updates

Never miss a Laravel release

Get an email whenever a new Laravel release lands.

Laravel 13.23 adds a monthly log driver that rotates one file per calendar month, per-message tenant selection for the SES v2 mail transport, support for PostgreSQL's USING clause when changing column types, and a timing-safe comparison for the maintenance mode bypass secret. The Laravel team released v13.23.0 on July 27, 2026.

  • New monthly log driver and matching monthly logging channel
  • X-SES-TENANT-NAME header support in the SES v2 transport
  • ->using() on ->change() migrations for PostgreSQL type casts
  • Timing-safe maintenance mode bypass secret comparison
  • A rewritten schedule:list timezone converter and assorted fixes

What's New

Monthly Log Driver

The daily driver rotates a file per day, which suits applications that log enough to fill one. Smaller applications end up with a directory of near-empty files instead. The new monthly driver sits between single and daily, writing one file per calendar month (laravel-2026-07.log, laravel-2026-08.log, and so on):

'monthly' => [
'driver' => 'monthly',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'max_files' => 3,
'replace_placeholders' => true,
],

Both rotating drivers now share a createRotatingDriver() method on the LogManager and differ only in the Monolog date format they pass (FILE_PER_DAY versus FILE_PER_MONTH) and their retention default: 14 files for daily, 3 for monthly.

Retention also has a new configuration key. The daily channel reads max_files first and falls back to the existing days key, so applications that already set days keep working and the LOG_DAILY_DAYS environment variable is unchanged. The shipped config now uses max_files for both channels. Contributed by @SjorsO in #60892, with the config channel added by @yazansalhi in #60895.

SES Tenant Support in the SES v2 Transport

Amazon SES tenants isolate sending reputation and configuration between groups of senders. Until now the transport could only work with a tenant configured for the whole application. The SES v2 transport now reads an X-SES-TENANT-NAME header off the outgoing message and forwards it as the TenantName parameter on the SDK call, following the pattern already used for X-SES-LIST-MANAGEMENT-OPTIONS.

You can set the header from a mailable:

use Illuminate\Mail\Mailables\Headers;
 
public function headers(): Headers
{
return new Headers(text: ['X-SES-TENANT-NAME' => 'tenant-id']);
}

Or from a notification's mail message:

return (new MailMessage)->withSymfonyMessage(function ($message) {
$message->getHeaders()->addTextHeader('X-SES-TENANT-NAME', 'tenant-id');
});

When the header is absent, sending behaves exactly as before, and the parameter is omitted on AWS SDK versions that predate the feature. Contributed by @atymic in #60886.

->using() for PostgreSQL Column Changes

PostgreSQL refuses to alter a column to an incompatible type without an explicit cast, returning an error like column "name" cannot be cast automatically to type date. There was no way to supply that cast through the schema builder, so migrations written against MySQL or SQLite would fail on PostgreSQL. Column definitions now accept a using() clause:

Schema::table('currency_rates', function (Blueprint $table) {
$table->date('name')->using('name::date')->change();
});

The clause applies to PostgreSQL only; other grammars ignore it. Contributed by @shemgp in #60901.

Timing-Safe Maintenance Mode Bypass Secret

The maintenance mode bypass flow compared the secret two different ways: hash_equals() when validating the incoming secret, and a plain === in the PreventRequestsDuringMaintenance middleware and the maintenance-mode.stub bootstrap path. Both comparisons now use hash_equals(), and the middleware's isset() guard was replaced with is_string() so a non-string cookie value cannot raise a TypeError.

This is a defense-in-depth change rather than a fix for an exploitable bug, since a wrong-length guess never reaches a byte-by-byte comparison. Behavior for valid input is unchanged. Contributed by @yazansalhi in #60896.

Other Fixes and Improvements

  • The schedule:list timezone converter was rewritten. It previously shifted only plain numeric cron fields, leaving ranges (8-23), steps (*/2), and wildcards untouched while still labeling the output with the converted timezone. It now expands each field, shifts the values, and collapses them back into compact syntax, splitting an event into multiple entries when a shifted range crosses midnight against a fixed day field. It also accounts for DST transitions and fractional offsets such as UTC+5:45 (#60877)
  • Failures inside the deprecation logger no longer escalate into fatal errors (#60893)
  • CookieJar::queued() returns the supplied default correctly (#60904), and Arr::last() handles null without erroring (#60887)
  • ImageManager::fromStorage() accepts a backed enum as the disk name (#60889), and the Image class received type improvements (#60890)
  • Collection::select() has more accurate generic types for static analysis (#60898)
  • The earlier fix for ShouldBeUniqueUntilProcessing jobs force-releasing locks they do not own was reverted (#60905)

References

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
Agent Run Observability in Laravel AI SDK 0.11 image

Agent Run Observability in Laravel AI SDK 0.11

Read article
Debounced Queued Event Listeners in Laravel image

Debounced Queued Event Listeners in Laravel

Read article
Statamic Mailables Viewer Previews Laravel Emails in the Control Panel image

Statamic Mailables Viewer Previews Laravel Emails in the Control Panel

Read article
Laravel Tackle: Run an AI Coding Agent in Your Laravel App image

Laravel Tackle: Run an AI Coding Agent in Your Laravel App

Read article
Queue::forward(): Reroute Laravel Queues in One Place image

Queue::forward(): Reroute Laravel Queues in One Place

Read article
Laravel Read-Through Filesystem: Lazy Storage Migration image

Laravel Read-Through Filesystem: Lazy Storage Migration

Read article