-
-
Notifications
You must be signed in to change notification settings - Fork 490
Expand file tree
/
Copy pathLogTenancyBootstrapper.php
More file actions
204 lines (181 loc) · 8.75 KB
/
Copy pathLogTenancyBootstrapper.php
File metadata and controls
204 lines (181 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<?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;
/**
* Enable tenant-specific logging.
*
* All the storage path channels are configured to use tenant
* directories by default (see the $storagePathChannels property).
*
* For this to work 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 LogTenancyBootstrapper implements TenancyBootstrapper
{
protected array $defaultConfig = [];
protected array $configuredChannels = [];
/**
* Logging channels whose paths use storage_path() by default in the logging config.
*
* All channels included here will be configured to use tenant-specific storage paths
* generated using storage_path() in the tenant context.
*
* This is the default behavior. The $channelOverrides property can be used to override
* this behavior (the overrides take precedence over $storagePathChannels).
*
* 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.
*
* All 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 not null; otherwise the override is ignored)
* - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])]
* - this merges ['url' => $tenant->slackUrl] into the channel's config.
*
* So the channel overrides can be arrays and closures that return arrays.
*/
public static array $channelOverrides = [];
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:
* - the default channel (primarily because it can be 'stack')
* - 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 and not in $storagePathChannels,
* it'll just be forgotten and re-resolved on the next use with the original config), 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,
* if only 'slack' is forgotten, 'stack' would still use the stale cached 'slack' driver,
* and if only 'stack' is forgotten, the 'slack' channel's config would remain unchanged (central).
*/
$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 (overrides take precedence over storage path channels).
*/
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 (default behavior).
// The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log".
$originalChannelPath = $this->config->get("logging.channels.{$channel}.path");
$centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey());
// 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)));
}
}
}
protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void
{
if (is_array($override)) {
// Map tenant attributes to channel config keys.
// If the tenant attribute is null,
// the override is ignored and the channel config key's value remains unchanged.
foreach ($override as $configKey => $tenantAttributeName) {
/** @var Tenant&Model $tenant */
$tenantAttribute = $tenant->getAttribute($tenantAttributeName);
if ($tenantAttribute !== null) {
$this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute);
}
}
} 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);
}
}
/**
* Forget all passed channels from the log manager so that
* they can be re-resolved with the updated (tenant-specific)
* config on the next logging attempt.
*/
protected function forgetChannels(array $channels): void
{
foreach ($channels as $channel) {
$this->logManager->forgetChannel($channel);
}
}
}