LOG_EMERG, LogLevel::ALERT => LOG_ALERT, LogLevel::CRITICAL => LOG_CRIT, LogLevel::ERROR => LOG_ERR, LogLevel::WARNING => LOG_WARNING, LogLevel::NOTICE => LOG_NOTICE, LogLevel::INFO => LOG_INFO, LogLevel::DEBUG => LOG_DEBUG, ]; public function __construct( private readonly string $ident = 'ktrix', private readonly int $facility = LOG_USER, private readonly string $channel = 'app', ) {} public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } public function log($level, $message, array $context = []): void { // Extract tenant id injected by TenantAwareLogger; default to 'system'. $tenantId = isset($context['__tenant']) && is_string($context['__tenant']) ? $context['__tenant'] : 'system'; unset($context['__tenant']); $level = strtolower((string) $level); $priority = self::PRIORITY[$level] ?? LOG_DEBUG; $interpolated = $this->interpolate((string) $message, $context); $contextStr = empty($context) ? '' : ' ' . $this->encodeContext($context); $entry = sprintf('[%s] [%s] %s%s', $this->channel, $tenantId, $interpolated, $contextStr); openlog($this->ident, LOG_NDELAY | LOG_PID, $this->facility); syslog($priority, $entry); closelog(); } private function interpolate(string $message, array $context): string { if (!str_contains($message, '{')) { return $message; } $replace = []; foreach ($context as $key => $val) { if (!is_array($val) && !is_object($val)) { $replace['{' . $key . '}'] = (string) $val; } } return strtr($message, $replace); } private function encodeContext(array $context): string { $clean = []; foreach ($context as $k => $v) { if ($v instanceof \Throwable) { $clean[$k] = ['type' => get_class($v), 'message' => $v->getMessage()]; } elseif (is_resource($v)) { $clean[$k] = 'resource(' . get_resource_type($v) . ')'; } elseif (is_object($v)) { $clean[$k] = method_exists($v, '__toString') ? (string) $v : ['object' => get_class($v)]; } else { $clean[$k] = $v; } } return json_encode($clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{}'; } }