42]); * Log::error('Сбой оплаты'); */ class Log { public const DEBUG = 'debug'; public const INFO = 'info'; public const WARNING = 'warning'; public const ERROR = 'error'; /** @var array Веса уровней для фильтрации по порогу */ private const WEIGHTS = [ self::DEBUG => 100, self::INFO => 200, self::WARNING => 300, self::ERROR => 400, ]; /** @var array Канал (файл) для каждого уровня */ private const CHANNELS = [ self::DEBUG => 'action', // действия пользователя (аудит) self::INFO => 'action', self::WARNING => 'error', // ошибки и предупреждения self::ERROR => 'error', ]; /** @var string|null Каталог логов; null → Config('log','path') → APPPATH/logs */ public static ?string $directory = null; /** @var string|null Минимальный уровень записи; null → Config('log','threshold') → debug */ public static ?string $threshold = null; /** @var array Ключи, значения которых маскируются (***) при логировании query/post */ public static array $mask_keys = ['password', 'pass', 'csrf_token', 'token']; /** * Пишет сообщение в лог, если его уровень не ниже порога. * * @param string $level Уровень (см. константы) * @param string $message Сообщение, может содержать плейсхолдеры для $variables * @param array $variables Карта замен [':key' => 'value'] для strtr * @return void */ public static function write(string $level, string $message, array $variables = []): void { if (self::weight($level) < self::weight(self::threshold())) { return; } if ($variables) { $message = strtr($message, $variables); } $dir = self::directory(); if (!is_dir($dir)) { mkdir($dir, 0775, true); } $line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message); $file = $dir . '/' . self::channel($level) . '-' . date('Y-m-d') . '.log'; file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } /** * @param string $message Сообщение * @param array $variables Карта замен для strtr * @return void */ public static function debug(string $message, array $variables = []): void { self::write(self::DEBUG, $message, $variables); } /** * @param string $message Сообщение * @param array $variables Карта замен для strtr * @return void */ public static function info(string $message, array $variables = []): void { self::write(self::INFO, $message, $variables); } /** * @param string $message Сообщение * @param array $variables Карта замен для strtr * @return void */ public static function warning(string $message, array $variables = []): void { self::write(self::WARNING, $message, $variables); } /** * @param string $message Сообщение * @param array $variables Карта замен для strtr * @return void */ public static function error(string $message, array $variables = []): void { self::write(self::ERROR, $message, $variables); } /** * Собирает строку контекста текущего запроса для аудита/лога ошибок. * Чувствительные значения query/post маскируются. * * @return string '' если текущего запроса нет (CLI/тесты) */ public static function requestInfo(): string { $request = Request::$current; if ($request === null) { return ''; } $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $params = json_encode($request->params(), $flags); $query = json_encode(self::mask($request->query()), $flags); $post = json_encode(self::mask($request->post()), $flags); return sprintf( '%s /%s %s::%s params=%s query=%s post=%s', $request->method(), $request->uri(), $request->controller(), $request->action(), $params, $query, $post ); } /** * Минимальный уровень записи (из свойства, конфига или дефолт debug). * * @return string */ private static function threshold(): string { return self::$threshold ?? Config::get('log', 'threshold') ?? self::DEBUG; } /** * Каталог логов (из свойства, конфига или дефолт APPPATH/logs). * * @return string */ private static function directory(): string { return self::$directory ?? Config::get('log', 'path') ?? APPPATH . '/logs'; } /** * Вес уровня для сравнения с порогом. * * @param string $level Уровень * @return int */ private static function weight(string $level): int { return self::WEIGHTS[strtolower($level)] ?? self::WEIGHTS[self::DEBUG]; } /** * Канал (имя файла без даты) для уровня: error/warning → 'error', * info/debug → 'action'. * * @param string $level Уровень * @return string */ private static function channel(string $level): string { return self::CHANNELS[strtolower($level)] ?? 'action'; } /** * Рекурсивно маскирует значения чувствительных ключей (без учёта регистра). * * @param array $data Данные * @return array */ private static function mask(array $data): array { $mask = array_map('strtolower', self::$mask_keys); $result = []; foreach ($data as $key => $value) { if (in_array(strtolower((string)$key), $mask, true)) { $result[$key] = '***'; } elseif (is_array($value)) { $result[$key] = self::mask($value); } else { $result[$key] = $value; } } return $result; } }