-
-
Notifications
You must be signed in to change notification settings - Fork 487
[4.x] Add LogTenancyBootstrapper #1381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 41 commits
01a06c9
96a05cd
43cf6d2
50853a3
b80d7b3
a13110c
718afd3
a806df0
ec47528
8cd35d3
62a0e39
582243c
bd44036
63bf4bf
81daa9d
42c837d
7bdbe9d
c180c2c
412c1d0
0b3f698
f878aaf
b36f3ce
108e0d1
e133c87
58a2447
ae39e4d
39fc72b
aedb33b
c68b91c
89b0d1c
9660faf
221a995
f705f58
697ba65
b744167
cdea112
8fda84f
34115e8
1ae418c
95fd046
06472d5
9ea3813
23ae15a
b234308
42d60e9
c2a80c2
2f60e76
fa075ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||||
| <?php | ||||||||
|
|
||||||||
| declare(strict_types=1); | ||||||||
|
|
||||||||
| namespace Stancl\Tenancy\Bootstrappers; | ||||||||
|
|
||||||||
| use Closure; | ||||||||
| use Illuminate\Contracts\Config\Repository as Config; | ||||||||
| use Illuminate\Database\Eloquent\Model; | ||||||||
| use Illuminate\Log\LogManager; | ||||||||
| use Illuminate\Support\Arr; | ||||||||
| use InvalidArgumentException; | ||||||||
| use Stancl\Tenancy\Contracts\TenancyBootstrapper; | ||||||||
| use Stancl\Tenancy\Contracts\Tenant; | ||||||||
|
|
||||||||
| /** | ||||||||
| * This bootstrapper makes it possible to configure tenant-specific logging. | ||||||||
| * | ||||||||
| * By default, the storage path channels ('single' and 'daily' by default, | ||||||||
| * but feel free to customize that using the $storagePathChannels property) | ||||||||
| * are configured to use tenant storage directories. | ||||||||
| * For this to work correctly, this bootstrapper must run *after* FilesystemTenancyBootstrapper. | ||||||||
| * FilesystemTenancyBootstrapper alters how storage_path() works in the tenant context. | ||||||||
| * | ||||||||
| * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the property's docblock). | ||||||||
| * | ||||||||
| * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper | ||||||||
| */ | ||||||||
| class LogTenancyBootstrapper implements TenancyBootstrapper | ||||||||
| { | ||||||||
| protected array $defaultConfig = []; | ||||||||
|
|
||||||||
| protected array $configuredChannels = []; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Log channels that use the storage_path() helper for storing the logs. Requires FilesystemTenancyBootstrapper to run before this bootstrapper. | ||||||||
| * Or you can bypass this default behavior by using overrides, since they take precedence over the default behavior. | ||||||||
| * | ||||||||
| * All channels included here will be configured to use tenant-specific storage paths. | ||||||||
| */ | ||||||||
| public static array $storagePathChannels = ['single', 'daily']; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Custom channel configuration overrides. | ||||||||
| * | ||||||||
| * All channels included here will be configured using the provided override. | ||||||||
| * | ||||||||
| * Examples: | ||||||||
| * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) | ||||||||
| * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] (the closure should return the whole channel's config) | ||||||||
| * | ||||||||
| * In both cases, the override should be an array. | ||||||||
| */ | ||||||||
| public static array $channelOverrides = []; | ||||||||
|
Comment on lines
+53
to
+68
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the key in this array is e.g. function (Config\Repository $config, Tenant $tenant): void {
$config->set('something', something based on $tenant);
}As opposed to returning a value that'd directly override the channel: function (array $channel, Tenant $tenant): array {
return array_merge($channel, [overrides based on $tenant]);
}The current approach would let you do for instance
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be resolved now f878aaf
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swapped the parameter order. Example for current usage of overrides: LogTenancyBootstrapper::$channelOverrides = [
'single' => function (Tenant $tenant, array $channel) {
return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]);
},
];Also updated the comments to clarify the bootstrapper's behavior. So I think this review can be resolved now |
||||||||
|
|
||||||||
| public function __construct( | ||||||||
| protected Config $config, | ||||||||
| protected LogManager $logManager, | ||||||||
| ) {} | ||||||||
|
|
||||||||
| public function bootstrap(Tenant $tenant): void | ||||||||
| { | ||||||||
| $this->defaultConfig = $this->config->get('logging.channels'); | ||||||||
| $this->configuredChannels = $this->getChannels(); | ||||||||
|
|
||||||||
| try { | ||||||||
| $this->configureChannels($this->configuredChannels, $tenant); | ||||||||
| $this->forgetChannels($this->configuredChannels); | ||||||||
| } catch (\Throwable $exception) { | ||||||||
| // Revert to default config if anything goes wrong during channel configuration | ||||||||
| $this->config->set('logging.channels', $this->defaultConfig); | ||||||||
| $this->forgetChannels($this->configuredChannels); | ||||||||
|
|
||||||||
| throw $exception; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| public function revert(): void | ||||||||
| { | ||||||||
| $this->config->set('logging.channels', $this->defaultConfig); | ||||||||
|
|
||||||||
| $this->forgetChannels($this->configuredChannels); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Channels to configure and forget so they can be re-resolved afterwards. | ||||||||
| * | ||||||||
| * Includes: | ||||||||
| * - the default channel | ||||||||
| * - all channels in the $storagePathChannels array | ||||||||
| * - all channels that have custom overrides in the $channelOverrides property | ||||||||
| */ | ||||||||
| protected function getChannels(): array | ||||||||
| { | ||||||||
| /** | ||||||||
| * Include the default channel in the list of channels to configure/re-resolve. | ||||||||
| * | ||||||||
| * Including the default channel is harmless (if it's not overridden or not in $storagePathChannels, | ||||||||
| * it'll just be forgotten and re-resolved on the next use), and for the case where 'stack' is the default, | ||||||||
| * this is necessary since the 'stack' channel will be resolved and saved in the log manager, | ||||||||
| * and its stale config could accidentally be used instead of the stack member channels. | ||||||||
| * | ||||||||
| * For example, when you use 'stack' with the 'slack' channel and you want to configure the webhook URL, | ||||||||
| * both 'stack' and 'slack' must be re-resolved after updating the config for the channels to use the correct webhook URLs. | ||||||||
| * If only one of the mentioned channels would be re-resolved, the other's (stale) webhook URL could be used for logging. | ||||||||
| */ | ||||||||
| $defaultChannel = $this->config->get('logging.default'); | ||||||||
|
|
||||||||
| return array_filter( | ||||||||
| array_unique([ | ||||||||
| $defaultChannel, | ||||||||
| ...static::$storagePathChannels, | ||||||||
| ...array_keys(static::$channelOverrides), | ||||||||
| ]), | ||||||||
| fn (string $channel): bool => $this->config->has("logging.channels.{$channel}") | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Configure channels for the tenant context. | ||||||||
| * | ||||||||
| * Only the channels that are in the $storagePathChannels array | ||||||||
| * or have custom overrides in the $channelOverrides property | ||||||||
| * will be configured. | ||||||||
| */ | ||||||||
| protected function configureChannels(array $channels, Tenant $tenant): void | ||||||||
| { | ||||||||
| foreach ($channels as $channel) { | ||||||||
| if (isset(static::$channelOverrides[$channel])) { | ||||||||
| $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); | ||||||||
| } elseif (in_array($channel, static::$storagePathChannels)) { | ||||||||
| // Set storage path channels to use tenant-specific directory (default behavior) | ||||||||
| // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" (assuming FilesystemTenancyBootstrapper is used before this bootstrapper) | ||||||||
| $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); | ||||||||
|
||||||||
| $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); | |
| $logFilename = $this->getTenantLogFilename($tenant); | |
| $this->config->set("logging.channels.{$channel}.path", storage_path("logs/{$logFilename}")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, this is fine for the default behavior, it can still be customized. Resolving this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of the box, if no customization is used, $storagePathChannels includes daily which does not use laravel.log names, but day-specific names. Seems like something that should be checked and tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is checked and tested already, I'd just add a short comment with an explanation of how daily works.
daily driver uses RotatingFileHandler that parses the file name. The current code (= storage_path('logs/laravel.log')) corresponds to the daily log channel config. It is correct, so I'd just clarify this since this can indeed be quite confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify further, 'logging.channels.daily.path' => storage_path('logs/laravel.log') is correct, consistent with Laravel's default daily channel path. When using a config like this, the log will be created e.g. at storage/tenantfoo/logs/laravel-2026-04-14.log (this is tested in stack logs are written to all configured channels with tenant-specific paths)
Uh oh!
There was an error while loading. Please reload this page.