[4.x] Add LogTenancyBootstrapper#1381
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1381 +/- ##
============================================
+ Coverage 86.37% 86.57% +0.19%
- Complexity 1200 1221 +21
============================================
Files 186 187 +1
Lines 3524 3575 +51
============================================
+ Hits 3044 3095 +51
Misses 480 480 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ook used by the slack channel correctly)
There was a problem hiding this comment.
Pull Request Overview
This PR adds a new LogTenancyBootstrapper to provide tenant-specific logging configuration. The bootstrapper automatically configures storage path channels to use tenant-specific directories and supports custom channel overrides for more complex logging scenarios.
Key changes:
- Implements
LogTenancyBootstrapperwith automatic storage path channel configuration and custom override support - Adds comprehensive test coverage for default behavior, custom overrides, and real-world usage scenarios
- Registers the new bootstrapper in the test environment
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/Bootstrappers/LogTenancyBootstrapper.php |
Main implementation of the log tenancy bootstrapper with storage path handling and channel override functionality |
tests/Bootstrappers/LogTenancyBootstrapperTest.php |
Comprehensive test suite covering default behavior, custom overrides, stack channels, and real logging scenarios |
tests/TestCase.php |
Adds import and singleton registration for the new bootstrapper in test environment |
Comments suppressed due to low confidence (1)
tests/Bootstrappers/LogTenancyBootstrapperTest.php:345
- The test relies on catching exceptions to verify webhook URLs, but this approach is fragile and may not work reliably across different environments or Laravel versions. Consider mocking the HTTP client or using a more deterministic testing approach.
try {
| } 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" | ||
| $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); |
There was a problem hiding this comment.
The hardcoded filename 'laravel.log' should be made configurable or use the original filename from the channel configuration. This prevents customization of log filenames and could overwrite existing configurations.
| $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.
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.
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.
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.
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)
…is set (otherwise, just skip the override and keep the default config value)
…ehavior, improve test by making assertions more specific
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/Bootstrappers/LogTenancyBootstrapper.php (1)
136-138:⚠️ Potential issue | 🟠 MajorPreserve the relative log subpath, not just the filename.
basename($path)keeps only the last segment. If two tenantized channels are configured asstorage/logs/api/app.logandstorage/logs/web/app.log, they both collapse tostorage/.../logs/app.logand their output mixes. Rebase the path relative tostorage_path()when the original path already lives under storage, and only fall back tobasename()for non-storage paths.💡 Suggested fix
- $path = $this->config->get("logging.channels.{$channel}.path"); - - $this->config->set("logging.channels.{$channel}.path", storage_path('logs/' . ($path ? basename($path) : 'laravel.log'))); + $path = (string) $this->config->get( + "logging.channels.{$channel}.path", + storage_path('logs/laravel.log') + ); + $storageRoot = rtrim(storage_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + + $relativePath = str_starts_with($path, $storageRoot) + ? ltrim(substr($path, strlen($storageRoot)), DIRECTORY_SEPARATOR) + : 'logs/' . basename($path); + + $this->config->set("logging.channels.{$channel}.path", storage_path($relativePath));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Bootstrappers/LogTenancyBootstrapper.php` around lines 136 - 138, The current logic uses basename($path) which loses intermediate directories and causes different tenant channels (e.g., storage/logs/api/app.log vs storage/logs/web/app.log) to collide; update the code in LogTenancyBootstrapper where you fetch and set "logging.channels.{$channel}.path" to preserve a relative subpath when the original path resides under storage_path(): if $path starts with storage_path() compute the relative path by stripping storage_path() and trimming leading slashes and join that under storage_path('logs/' . $relative), otherwise fall back to using basename($path) (and default to 'laravel.log' when $path is empty); use existing symbols $this->config->get, $this->config->set, storage_path() and basename() to locate and change the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/Bootstrappers/LogTenancyBootstrapperTest.php`:
- Around line 32-34: In beforeEach capture the current static values of
LogTenancyBootstrapper::$channelOverrides and
LogTenancyBootstrapper::$storagePathChannels into local variables (e.g.
$originalChannelOverrides, $originalStoragePathChannels) and then in afterEach
restore those exact captured values back to
LogTenancyBootstrapper::$channelOverrides and
LogTenancyBootstrapper::$storagePathChannels instead of hard-coding arrays so
the tests revert to the true originals; apply the same pattern for the other
reset points that currently hard-code values.
---
Duplicate comments:
In `@src/Bootstrappers/LogTenancyBootstrapper.php`:
- Around line 136-138: The current logic uses basename($path) which loses
intermediate directories and causes different tenant channels (e.g.,
storage/logs/api/app.log vs storage/logs/web/app.log) to collide; update the
code in LogTenancyBootstrapper where you fetch and set
"logging.channels.{$channel}.path" to preserve a relative subpath when the
original path resides under storage_path(): if $path starts with storage_path()
compute the relative path by stripping storage_path() and trimming leading
slashes and join that under storage_path('logs/' . $relative), otherwise fall
back to using basename($path) (and default to 'laravel.log' when $path is
empty); use existing symbols $this->config->get, $this->config->set,
storage_path() and basename() to locate and change the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8006ceab-0634-4428-a3b7-6ac00a203f75
📒 Files selected for processing (2)
src/Bootstrappers/LogTenancyBootstrapper.phptests/Bootstrappers/LogTenancyBootstrapperTest.php
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Bootstrappers/LogTenancyBootstrapper.php`:
- Around line 133-141: The code currently resolves and bakes an absolute central
storage path into tenant channels; add a runtime guard in LogTenancyBootstrapper
(the branch handling static::$storagePathChannels where you call
$this->config->get("logging.channels.{$channel}.path") and set
storage_path(...)) that detects if the existing channel path is already an
absolute/central storage path (e.g., starts with storage_path() or is an
absolute filesystem path) and immediately throw a clear RuntimeException
instructing the user to enable/position FilesystemTenancyBootstrapper earlier in
the bootstrappers list; do this check before calling storage_path(...) and avoid
mutating the config if the guard triggers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6db5d9fe-f993-4381-b44e-a9709a300325
📒 Files selected for processing (1)
src/Bootstrappers/LogTenancyBootstrapper.php
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/Bootstrappers/LogTenancyBootstrapper.php (1)
145-155:⚠️ Potential issue | 🟠 MajorSkip storage-path rewrites when storage suffixing is disabled.
This branch still rewrites
logging.channels.{channel}.patheven whentenancy.filesystem.suffix_storage_pathisfalse. In that modestorage_path()stays central, so these channels keep writing to shared storage instead of being tenant-isolated. Please gate the default path-rewrite logic on that flag and silently skip it when suffixing is off.💡 Suggested fix
foreach ($channels as $channel) { if (isset(static::$channelOverrides[$channel])) { $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); - } elseif (in_array($channel, static::$storagePathChannels)) { + } elseif (in_array($channel, static::$storagePathChannels, true)) { + if ($this->config->get('tenancy.filesystem.suffix_storage_path') === false) { + continue; + } + // 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). $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey());Based on learnings, in
archtechx/tenancyLogTenancyBootstrapper, the$storagePathChannelslogic depends on bothFilesystemTenancyBootstrapperrunning first andtenancy.filesystem.suffix_storage_pathbeingtrue; whensuffix_storage_pathisfalse, storage-path channels should be silently skipped.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Bootstrappers/LogTenancyBootstrapper.php` around lines 145 - 155, The branch in LogTenancyBootstrapper that rewrites logging.channels.{channel}.path for entries in $storagePathChannels must be skipped when tenancy.filesystem.suffix_storage_path is false; update the conditional around the in_array($channel, static::$storagePathChannels) block to also check config('tenancy.filesystem.suffix_storage_path') (or $this->config->get(...)) and return/continue silently when suffixing is disabled so no storage_path() rewrite occurs and channels remain on central storage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/Bootstrappers/LogTenancyBootstrapperTest.php`:
- Around line 23-26: The cleanup only globs one-level *.log files into $logFiles
so nested files like storage/logs/single/*.log, storage/logs/daily/*.log and
tenantized storage/tenant*/logs/*/*.log are missed; update the globbing used
where $logFiles is constructed (and the similar block at the other occurrence)
to include nested directories (e.g. add patterns for logs/*/*.log and
tenant*/logs/*/*.log or switch to a recursive iterator/RegexIterator using
storage_path()) so all nested .log files are collected and removed during
teardown.
---
Duplicate comments:
In `@src/Bootstrappers/LogTenancyBootstrapper.php`:
- Around line 145-155: The branch in LogTenancyBootstrapper that rewrites
logging.channels.{channel}.path for entries in $storagePathChannels must be
skipped when tenancy.filesystem.suffix_storage_path is false; update the
conditional around the in_array($channel, static::$storagePathChannels) block to
also check config('tenancy.filesystem.suffix_storage_path') (or
$this->config->get(...)) and return/continue silently when suffixing is disabled
so no storage_path() rewrite occurs and channels remain on central storage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fd32f838-0fe9-4557-bf08-fe6b4594b276
📒 Files selected for processing (2)
src/Bootstrappers/LogTenancyBootstrapper.phptests/Bootstrappers/LogTenancyBootstrapperTest.php
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
too many mentions of "default" behavior when there are just different approaches for different channels
|
Seems some of the minor logic changes here b51d5ca (the getAttribute thing perhaps?) made a test start failing. |
The bootstrapper should be able to map nested tenant attributes to config, and `getAttribute()` doesn't support dot notation, so e.g. `$tenant->getAttribute('logging.slackUrl')` would return `null` instead of the actual attribute, so the test failed.
`data_get()` supports dot notation, so we'll use that.
…nels Reverting the LogTenancyBootstrapper changes will make the new "stack channels that include any configured channel are re-resolved" test fali
Yes, the test started failing because in the test, // 'slack channel uses correct webhook urls'
LogTenancyBootstrapper::$channelOverrides = [
'slack' => ['url' => 'logging.slackUrl'],
]
Fixed by using |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/Bootstrappers/LogTenancyBootstrapperTest.php`:
- Around line 373-390: The test currently verifies `custom_stack` only while
tenancy is active, so it does not cover the channel reset path after
`tenancy()->end()`. After the existing tenant log assertions in
`LogTenancyBootstrapperTest`, exercise `Log::channel('custom_stack')` again once
tenancy has ended and add an assertion that the post-revert log goes back to the
central log (using the existing `centralLogPath` and `tenantLogPath` checks).
This will validate the `forgetChannel()` behavior triggered by `TenancyEnded`
and ensure the stack channel is re-resolved back to the non-tenant
configuration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f50ddf7a-bc8b-43b4-a5a0-df590bf44372
📒 Files selected for processing (2)
src/Bootstrappers/LogTenancyBootstrapper.phptests/Bootstrappers/LogTenancyBootstrapperTest.php
we use FilesystemTenancyBootstrapper::getBoundCentralStoragePath() in LogTenancyBootstrapper to simplify the logic for extracting a path inside the central storage directory we could use this method in more places, such as DeleteTenantStorage: $centralStoragePath = tenancy()->central(fn () => storage_path()); $tenantStoragePath = tenancy()->run($this->tenant, fn () => storage_path()); however for consistency (we're getting both the central and tenant paths there) we're keeping it simple as it is now using the newly added method would also introduce more coupling to FilesystemTenancyBootstrapper - its specific implementation. this isn't a big issue in LogTenancyBootstrapper which already has several dependencies on the FS bootstrapper, and we do use app(static::class) in the added method so extending the bootstrapper should work
the main change here is adding similar assertions related to *re-resolving* channels to the 'stack channels that include any configured channel are re-resolved' test, similar to the 'channels are forgotten and re-resolved during bootstrap and revert' test
This PR adds the LogTenancyBootstrapper to provide tenant-specific logging configuration. The bootstrapper automatically configures storage path channels to use tenant-specific directories (NOTE: for this to work correctly, the bootstrapper has to run AFTER FilesystemTenancyBootstrapper, otherwise, the logs still won't be separated, unless you use overrides) and supports custom channel overrides for custom logging scenarios -- mapping tenant properties to the channel config, or using custom closures an array with the logging config, e.g. for making the slack channel (that's not handled by the bootstrapper by default) tenant-specific.
The bootstrapper first modifies the channel config, then forgets the channel from LogManager so that on the next logging attempt, the channel is re-resolved with the modified config. Otherwise, the channel would just use the initial config if the channel was resolved before. If the channel wasn't resolved before, it'll always be resolved with the updated (tenant) config, unless the configuration fails. In that case, the config will be reverted (the central config will be restored) and the error will be logged using the original channel.
When using a channel stack, the
stackchannel itself also has to be forgotten, since the LogManager could retain e.g. the originalstackchannel's webhook URL, while the underlyingslackchannel would use the updated one, and while logging, the app would actually use the initial webhook URL instead of the updated one (encountered this issue while testing).Note that all channels in
$storagePathChannelsand$channelOverridesare affected.Also, adding
'attachment' => 'false'to the slack channel's config makes the slack channel work with Discord webhooks (just a cool thing we figured out whlle testing the bootstrapper).Summary by CodeRabbit
single/dailyandstackmembers), and supports configurable per-tenant channel overrides with validation.