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]; } }