info('message', [...]); * Logger::get('admin')->warning('message', [...]); */ class Logger { /** @var array */ private static array $channels = []; /** * Get (or lazily create) a named Monolog channel. */ public static function get(string $channel): LoggerInterface { if (!isset(self::$channels[$channel])) { self::$channels[$channel] = self::create($channel); } return self::$channels[$channel]; } /** * Create a Monolog channel with rotating JSON file handler. * * Falls back to NullHandler if the log directory is not writable * (e.g. CLI scripts on a machine that doesn't have the production path). */ private static function create(string $channel): \Monolog\Logger { $logDir = self::logDir(); if (!is_dir($logDir) && !@mkdir($logDir, 0755, true) && !is_dir($logDir)) { // Directory can't be created — use null handler (graceful degradation) $logger = new \Monolog\Logger($channel); $logger->pushHandler(new NullHandler()); return $logger; } try { $handler = new RotatingFileHandler( $logDir . '/' . $channel . '.log', 30, // keep 30 days of logs self::level() ); } catch (\Throwable $e) { // Can't open log file — use null handler $logger = new \Monolog\Logger($channel); $logger->pushHandler(new NullHandler()); return $logger; } // Pass-through formatter: the facades (AppLogger, AdminLogger, etc.) // construct their own JSON lines and pass them as the log message. // %message% preserves the existing JSON format contract exactly. $handler->setFormatter(new LineFormatter("%message%\n", null, true)); $logger = new \Monolog\Logger($channel); $logger->pushHandler($handler); return $logger; } /** * Read the LOG_LEVEL env var with sensible defaults. */ private static function level(): Level { $level = strtoupper(getenv('LOG_LEVEL') ?: ''); // Default: WARNING in production (always set in .env), DEBUG otherwise if ($level === '') { return php_sapi_name() === 'cli-server' ? Level::Debug : Level::Warning; } return Level::fromName($level); } /** * Resolve the log directory. */ private static function logDir(): string { if (defined('STORAGE_ROOT')) { return STORAGE_ROOT . '/logs'; } return __DIR__ . '/../storage/logs'; } }