Skip to content

LogChannelBootstrapper

The LogChannelBootstrapper makes logging tenant-aware. By default, it points Laravel’s single and daily channels at the tenant’s storage directory, so each tenant’s logs end up in their own folder rather than in the central log file. It does this by reconfiguring the channels and forgetting them from the LogManager.

Channels that don’t write to the filesystem — e.g. slack — can be given tenant-specific config using channel overrides.

To enable this bootstrapper, uncomment the LogChannelBootstrapper line in the bootstrappers section of the tenancy config:

config/tenancy.php
'bootstrappers' => [
// Bootstrappers\LogChannelBootstrapper::class,
],

Channels listed in the $storagePathChannels static property get their path rewritten to the tenant’s storage directory:

LogChannelBootstrapper::$storagePathChannels = ['single', 'daily'];

The part of the path that follows the central storage path is preserved, so a channel configured as storage_path('logs/laravel.log') writes to storage/tenant{$tenantKey}/logs/laravel.log in the tenant context.

Note that this uses FilesystemTenancyBootstrapper::getTenantStoragePath(), which is a public static method — you don’t need to have the FilesystemTenancyBootstrapper enabled for this to work though keeping that bootstrapper enabled is recommended in general.

The $channelOverrides static property configures channels that aren’t filesystem-based. It accepts two syntaxes.

An array maps tenant attributes to the channel’s config keys:

LogChannelBootstrapper::$channelOverrides = [
'slack' => ['url' => 'webhookUrl'],
];

This maps $tenant->webhookUrl to the channel’s url config. If the attribute is null, the override is ignored and the channel keeps its central config.

A closure receives the tenant and the channel’s current config, and returns the full config to use:

LogChannelBootstrapper::$channelOverrides = [
'slack' => fn (Tenant $tenant, array $channel) => array_merge(
$channel, ['url' => $tenant->slackUrl]
),
];

Unlike the array syntax, a closure is fully in control — null values are not ignored and the returned value isn’t merged into the existing config, it replaces it.

Overrides take precedence over $storagePathChannels when a channel is listed in both.

Once a stack channel has been used, it keeps logging to wherever its members pointed at that moment. A stack used in the central context would therefore keep writing to the central logs even after tenancy is initialized.

To avoid this, the bootstrapper also forgets any stack channel that has one of the configured channels among its members, forcing it to be re-resolved with the tenant config. Stacks are only inspected one level deep — they aren’t traversed recursively, so a stack whose only configured channel sits inside another nested stack won’t be picked up.