Files
server/core/lib/Logger/TenantAwareLogger.php
Sebastian Krupinski 1f3e87535b
All checks were successful
JS Unit Tests / test (pull_request) Successful in 21s
Build Test / build (pull_request) Successful in 25s
PHP Unit Tests / test (pull_request) Successful in 46s
refactor: documents
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-03-03 22:13:36 -05:00

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