94 lines
4.4 KiB
PHP
94 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace KTXC\Logger;
|
|
|
|
use KTXC\SessionTenant;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* PSR-3 decorator with two responsibilities:
|
|
*
|
|
* 1. Tenant-id injection — every log record is enriched with a `tenant` field.
|
|
* When a tenant session is active the real tenant identifier is used; otherwise
|
|
* the value is "system" (boot phase, CLI, bad-domain rejections, etc.).
|
|
* The id is passed to inner loggers via the reserved `__tenant` context key,
|
|
* which each concrete logger (FileLogger, SystemdLogger, SyslogLogger) extracts
|
|
* and renders as a top-level field, then removes from context before output.
|
|
*
|
|
* 2. Per-tenant file routing (optional, controlled by $perTenant) — when enabled,
|
|
* writes for an active tenant are routed to:
|
|
* {logDir}/tenant/{tenantIdentifier}/{channel}.jsonl
|
|
* Messages that carry tenant "system" always go to the global logger.
|
|
*
|
|
* Both behaviours rely on a live SessionTenant reference that is populated lazily
|
|
* by TenantMiddleware — the same pattern the cache stores use in Kernel::configureContainer().
|
|
*/
|
|
class TenantAwareLogger implements LoggerInterface
|
|
{
|
|
/** @var array<string, LoggerInterface> Per-tenant logger cache, keyed by tenant identifier. */
|
|
private array $tenantLoggers = [];
|
|
|
|
/**
|
|
* @param LoggerInterface $globalLogger Fallback logger (also used when perTenant = false).
|
|
* @param SessionTenant $sessionTenant Live reference configured by TenantMiddleware.
|
|
* @param string $logDir Base log directory (e.g. /var/www/app/var/log).
|
|
* @param string $channel Log file basename (e.g. 'app' → app.jsonl).
|
|
* @param string $minLevel Minimum PSR-3 level for lazily-created per-tenant loggers.
|
|
* @param bool $perTenant When true, route tenant writes to per-tenant files.
|
|
*/
|
|
public function __construct(
|
|
private readonly LoggerInterface $globalLogger,
|
|
private readonly SessionTenant $sessionTenant,
|
|
private readonly string $logDir,
|
|
private readonly string $channel = 'app',
|
|
private readonly string $minLevel = 'debug',
|
|
private readonly bool $perTenant = false,
|
|
) {
|
|
LogLevelSeverity::validate($minLevel);
|
|
}
|
|
|
|
public function emergency($message, array $context = []): void { $this->log('emergency', $message, $context); }
|
|
public function alert($message, array $context = []): void { $this->log('alert', $message, $context); }
|
|
public function critical($message, array $context = []): void { $this->log('critical', $message, $context); }
|
|
public function error($message, array $context = []): void { $this->log('error', $message, $context); }
|
|
public function warning($message, array $context = []): void { $this->log('warning', $message, $context); }
|
|
public function notice($message, array $context = []): void { $this->log('notice', $message, $context); }
|
|
public function info($message, array $context = []): void { $this->log('info', $message, $context); }
|
|
public function debug($message, array $context = []): void { $this->log('debug', $message, $context); }
|
|
|
|
public function log($level, $message, array $context = []): void
|
|
{
|
|
// Resolve current tenant id; fall back to 'system' for CLI / boot phase.
|
|
$tenantId = 'system';
|
|
if ($this->sessionTenant->configured()) {
|
|
$tenantId = $this->sessionTenant->identifier() ?? 'system';
|
|
}
|
|
|
|
// Inject tenant id as a reserved context key that concrete loggers extract.
|
|
$context['__tenant'] = $tenantId;
|
|
|
|
$this->resolveLogger($tenantId)->log($level, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Returns the logger to write to for the given tenant.
|
|
* When per-tenant routing is disabled (or tenant is "system"), always returns the global logger.
|
|
*/
|
|
private function resolveLogger(string $tenantId): LoggerInterface
|
|
{
|
|
if (!$this->perTenant || $tenantId === 'system') {
|
|
return $this->globalLogger;
|
|
}
|
|
|
|
if (!isset($this->tenantLoggers[$tenantId])) {
|
|
$tenantLogDir = rtrim($this->logDir, '/') . '/tenant/' . $tenantId;
|
|
$this->tenantLoggers[$tenantId] = new LevelFilterLogger(
|
|
new FileLogger($tenantLogDir, $this->channel),
|
|
$this->minLevel,
|
|
);
|
|
}
|
|
|
|
return $this->tenantLoggers[$tenantId];
|
|
}
|
|
}
|