-
-
Notifications
You must be signed in to change notification settings - Fork 487
[4.x] Add LogChannelBootstrapper #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
Merged
Merged
Changes from all commits
Commits
Show all changes
63 commits
Select commit
Hold shift + click to select a range
01a06c9
Add LogTenancyBootstrapper
lukinovec 96a05cd
Fix code style (php-cs-fixer)
github-actions[bot] 43cf6d2
Merge branch 'master' into add-log-bootstrapper
lukinovec 50853a3
Test LogTenancyBootstrapper logic (low-level tests)
lukinovec b80d7b3
Test real usage with storage path-based channels
lukinovec a13110c
Test real usage with slack channel (the bootstrapper updates the webh…
lukinovec 718afd3
Simplify the slack channel usage test
lukinovec a806df0
Stop using real domains in the tests
lukinovec ec47528
Refactor bootstrapper, make comments more concise
lukinovec 8cd35d3
Add @see to bootstrapper docblock
lukinovec 62a0e39
Delete redundant test, test the same logic in the one larger test
lukinovec 582243c
Clarify test name
lukinovec bd44036
By default, only override the config if the override tenant property …
lukinovec 63bf4bf
Clarify bootstrapper comments
lukinovec 81daa9d
Simplify test
lukinovec 42c837d
Refactor bootstrapper, provide more info in comments
lukinovec 7bdbe9d
Improve checking if tenant attribute is set
lukinovec c180c2c
Use more accurate terminology
lukinovec 412c1d0
Merge branch 'master' into add-log-bootstrapper
stancl 0b3f698
Merge branch 'master' into add-log-bootstrapper
lukinovec f878aaf
Improve closure overrides
lukinovec b36f3ce
Fix typo
lukinovec 108e0d1
Swap closure param order, add/update comments
lukinovec e133c87
Make test priovide sufficient context for understanding the default b…
lukinovec 58a2447
Use more direct assertions in the tests that assert the actual behavi…
lukinovec ae39e4d
Clarify behavior in log bootstrapper comments
lukinovec 39fc72b
Merge branch 'master' into add-log-bootstrapper
stancl aedb33b
Clean up log files in before/afterEach
lukinovec c68b91c
Make tests not depend on setting the default logging channel
lukinovec 89b0d1c
Include all storage path channels and overrides in `getChannels()`
lukinovec 9660faf
Ensure Slack throws cURL exception in test
lukinovec 221a995
Support channel overrides using dot notation
lukinovec f705f58
Fix code style (php-cs-fixer)
github-actions[bot] 697ba65
Correct log file cleanup
lukinovec b744167
Fix PHPStan error
lukinovec cdea112
Fix code style (php-cs-fixer)
github-actions[bot] 8fda84f
Throw exception if override closure doesn't return array
lukinovec 34115e8
Rollback config if bootstrap fails
lukinovec 1ae418c
Store configured channels in a property, forget only the stored channels
lukinovec 95fd046
Import InvalidArgumentException
lukinovec 06472d5
Fix code style (php-cs-fixer)
github-actions[bot] 9ea3813
Improve $channelOverrides docblock
lukinovec 23ae15a
Preserve filename from central log path in tenant context
lukinovec b234308
Add comment about log path customization
lukinovec 42d60e9
Make tenant log channels inherit paths from central config, improve c…
lukinovec c2a80c2
Fix code style (php-cs-fixer)
github-actions[bot] 2f60e76
Clean up nested log files created by tests
lukinovec fa075ef
Merge branch 'master' into add-log-bootstrapper
lukinovec c5683d8
Extract cleanup in test file
lukinovec 8276f3b
Improve comments in tests
lukinovec 6e474ac
Improve comments, test reverting on failure during configuration
lukinovec 55ee7d8
Merge branch 'master' into add-log-bootstrapper
stancl b51d5ca
clarify language in docblocks
stancl 8107ed4
Fix code style (php-cs-fixer)
github-actions[bot] dc331bf
Use `data_get()` instead of `getAttribute()` in `overrideChannelConfig`
lukinovec eaedd4a
Clarify docblocks
lukinovec 68e6fd6
Re-resolve all stack channels that include any of the configured chan…
lukinovec a6ce0e2
Test that ending tenancy forgets the stack channels properly
lukinovec e114e1b
update comments, add getBoundCentralStoragePath()
stancl ce5b363
Merge branch 'master' into add-log-bootstrapper
stancl f2b7c7d
minor test improvements
stancl cef7f11
tests: remove comment
stancl 24029f7
rename to LogChannelBootstrapper, add to config
stancl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Stancl\Tenancy\Bootstrappers; | ||
|
|
||
| use Closure; | ||
| use Illuminate\Contracts\Config\Repository; | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Log\LogManager; | ||
| use Illuminate\Support\Str; | ||
| use InvalidArgumentException; | ||
| use Stancl\Tenancy\Contracts\TenancyBootstrapper; | ||
| use Stancl\Tenancy\Contracts\Tenant; | ||
|
|
||
| /** | ||
| * Use tenant-specific logging channels. | ||
| * | ||
| * Channels included in the $storagePathChannels property will be configured | ||
| * to write logs into the tenant's storage directory. The list includes | ||
| * Laravel's 'single' and 'daily' channels by default. To customize it, | ||
| * see the property's docblock. | ||
| * | ||
| * For the storage path channels to be scoped correctly: | ||
| * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, | ||
| * since FilesystemTenancyBootstrapper adjusts storage_path() for the tenant | ||
| * - storage path suffixing has to be enabled (= config('tenancy.filesystem.suffix_storage_path') | ||
| * must be true), since the storage path suffix is what separates filesystem-based logs | ||
| * | ||
| * For logging channels that are not filesystem-based, see the $channelOverrides logic. | ||
| * | ||
| * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper | ||
| */ | ||
| class LogChannelBootstrapper implements TenancyBootstrapper | ||
| { | ||
| protected array $defaultConfig = []; | ||
|
|
||
| protected array $configuredChannels = []; | ||
|
|
||
| /** | ||
| * Logging channels whose path is built using storage_path() (e.g. Laravel's 'single' and 'daily'). | ||
| * | ||
| * Channels included here will be configured to use tenant-specific storage paths | ||
| * created using storage_path() in the tenant context. Overrides in the $channelOverrides | ||
| * property take precedence over $storagePathChannels when a channel is included in both. | ||
| * | ||
| * Requires FilesystemTenancyBootstrapper to run before this bootstrapper, | ||
| * and storage path suffixing to be enabled. | ||
| * | ||
| * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper | ||
| */ | ||
| public static array $storagePathChannels = ['single', 'daily']; | ||
|
|
||
| /** | ||
| * Custom channel configuration overrides. | ||
| * | ||
| * Channels included here will be configured using the provided override. | ||
| * The overrides take precedence over the $storagePathChannels behavior | ||
| * when both approaches are used for the same channel. | ||
| * | ||
| * You can either map tenant attributes to channel config keys using an array, | ||
| * or provide a closure that returns the full channel config array. | ||
| * | ||
| * Examples: | ||
| * - Array mapping: ['slack' => ['url' => 'webhookUrl']] | ||
| * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is null, the override is ignored) | ||
| * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] | ||
| * - this manually merges ['url' => $tenant->slackUrl] into the channel's config | ||
| * - null is not ignored, the closure controls the override fully | ||
| * | ||
| * So the channel overrides can be arrays and closures that return arrays. | ||
| */ | ||
| public static array $channelOverrides = []; | ||
|
stancl marked this conversation as resolved.
|
||
|
|
||
| public function __construct( | ||
| protected Repository $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) { | ||
| // If an exception is thrown while updating the logging config, the logging config | ||
| // could be left in a corrupt state, so we revert to the original config to | ||
| // to avoid logging the exception in a tenant channel or a broken channel. | ||
| $this->revert(); | ||
|
|
||
| // We re-throw the exception after having reverted the logging config to central. | ||
| throw $exception; | ||
| } | ||
| } | ||
|
|
||
| public function revert(): void | ||
| { | ||
| $this->config->set('logging.channels', $this->defaultConfig); | ||
|
|
||
| $this->forgetChannels($this->configuredChannels); | ||
| } | ||
|
|
||
| /** | ||
| * Channels to configure and forget from the log manager so they can be | ||
| * re-resolved with the new, tenant-specific config on the next use. | ||
| * | ||
| * Includes: | ||
| * - all channels in the $storagePathChannels array | ||
| * - all channels that have custom overrides in the $channelOverrides property | ||
| * - any 'stack' channel that includes one of the above as a member | ||
| * | ||
| * Stack channels are included because once a stack has been used, it keeps logging | ||
| * to wherever its members pointed to at that moment. So a stack used in the central | ||
| * context would keep writing to the central logs, even after tenancy is initialized | ||
| * and its member channels are configured for the tenant. | ||
| * Forgetting the stack forces it to be re-resolved with its members' updated (tenant) | ||
| * config. | ||
| * | ||
| * Importantly, stacks are only inspected one level deep - they are not traversed recursively. | ||
| */ | ||
| protected function getChannels(): array | ||
| { | ||
| $configuredChannels = array_unique([ | ||
| ...static::$storagePathChannels, | ||
| ...array_keys(static::$channelOverrides), | ||
| ]); | ||
|
|
||
| $stackChannels = []; | ||
|
|
||
| foreach ($this->config->get('logging.channels') as $channel => $config) { | ||
| // Include stack channels that have at least one configured channel as a member | ||
| if (($config['driver'] ?? null) === 'stack' && array_intersect($config['channels'] ?? [], $configuredChannels)) { | ||
| $stackChannels[] = $channel; | ||
| } | ||
| } | ||
|
|
||
| return array_filter( | ||
| array_unique([...$configuredChannels, ...$stackChannels]), | ||
| fn (string $channel): bool => $this->config->has("logging.channels.{$channel}") | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Configure channels for the tenant context. | ||
| * | ||
| * This handles both $storagePathChannels and $channelOverrides. | ||
| */ | ||
| 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 a tenant-specific directory. | ||
| // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log". | ||
| $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); | ||
| $centralStoragePath = FilesystemTenancyBootstrapper::getBoundCentralStoragePath(); | ||
|
|
||
| // The tenant log will inherit the segment that follows the storage path from the central channel path config. | ||
| // For example, if a channel's path is configured to storage_path('logs/foo.log') (storage/logs/foo.log), | ||
| // the 'logs/foo.log' segment will be passed to storage_path() in the tenant context (storage/tenant123/logs/foo.log). | ||
| $this->config->set("logging.channels.{$channel}.path", storage_path(Str::after($originalChannelPath, $centralStoragePath))); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update channel configurations per $channelOverrides. | ||
| * | ||
| * For overrides set in array format, update individual keys of the channel. | ||
| * - This ignores cases where the value of the respective tenant attribute is null. | ||
| * For overrides set as closures, replace the entire channel with the returned config override. | ||
| * - This does not ignore cases where parts of the config may be null - the closure fully controls the override. | ||
| */ | ||
| protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void | ||
| { | ||
| if (is_array($override)) { | ||
| // Map tenant attributes to channel config keys. | ||
| foreach ($override as $configKey => $tenantAttributeName) { | ||
| /** @var Tenant&Model $tenant */ | ||
| $tenantAttribute = data_get($tenant, $tenantAttributeName); | ||
|
|
||
| // If the tenant attribute is null, the override is ignored | ||
| // and the channel config key's value remains unchanged. | ||
| if ($tenantAttribute !== null) { | ||
| $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } elseif ($override instanceof Closure) { | ||
| $channelConfigKey = "logging.channels.{$channel}"; | ||
|
|
||
| $result = $override($tenant, $this->config->get($channelConfigKey)); | ||
|
|
||
| if (! is_array($result)) { | ||
| throw new InvalidArgumentException("Channel override closure for '{$channel}' must return an array."); | ||
| } | ||
|
|
||
| $this->config->set($channelConfigKey, $result); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Forget all passed channels from the log manager so that they can be | ||
| * re-resolved with the updated config on the next logging attempt. | ||
| */ | ||
| protected function forgetChannels(array $channels): void | ||
| { | ||
| foreach ($channels as $channel) { | ||
| $this->logManager->forgetChannel($channel); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.