log(LogLevel::DEBUG, ...$args); } function logInfo(...$args): void { global $__logger; $__logger->log(LogLevel::INFO, ...$args); } function logWarning(...$args): void { global $__logger; $__logger->log(LogLevel::WARNING, ...$args); } function logError(...$args): void { global $__logger; $__logger->log(LogLevel::ERROR, ...$args); } abstract class Logger { protected bool $enabled = false; protected int $counter = 0; protected int $recursionLevel = 0; /** @var ?callable $filter */ protected $filter = null; function setErrorFilter(callable $filter): void { $this->filter = $filter; } function disable(): void { $this->enabled = false; } function enable(): void { static $error_handler_set = false; $this->enabled = true; if ($error_handler_set) return; $self = $this; set_error_handler(function($no, $str, $file, $line) use ($self) { if (!$self->enabled) return; if (is_callable($self->filter) && !($self->filter)($no, $file, $line, $str)) return; static::write(LogLevel::ERROR, $str, errno: $no, errfile: $file, errline: $line); }); register_shutdown_function(function () use ($self) { if (!$self->enabled || !($error = error_get_last())) return; if (is_callable($self->filter) && !($self->filter)($error['type'], $error['file'], $error['line'], $error['message'])) { return; } static::write(LogLevel::ERROR, $error['message'], errno: $error['type'], errfile: $error['file'], errline: $error['line']); }); $error_handler_set = true; } function log(LogLevel $level, ...$args): void { if (!is_dev() && $level == LogLevel::DEBUG) return; $this->write($level, strVars($args)); } protected function canReport(): bool { return $this->recursionLevel < 3; } protected function write(LogLevel $level, string $message, ?int $errno = null, ?string $errfile = null, ?string $errline = null): void { $this->recursionLevel++; if ($this->canReport()) $this->writer($level, $this->counter++, $message, $errno, $errfile, $errline); $this->recursionLevel--; } abstract protected function writer(LogLevel $level, int $num, string $message, ?int $errno = null, ?string $errfile = null, ?string $errline = null): void; } class FileLogger extends Logger { function __construct(protected string $logFile) {} protected function writer(LogLevel $level, int $num, string $message, ?int $errno = null, ?string $errfile = null, ?string $errline = null): void { if (is_null($this->logFile)) { fprintf(STDERR, __METHOD__.': logfile is not set'); return; } $time = time(); // TODO rewrite using sprintf $exec_time = strval(exectime()); if (strlen($exec_time) < 6) $exec_time .= str_repeat('0', 6 - strlen($exec_time)); $title = is_cli() ? 'cli' : $_SERVER['REQUEST_URI']; $date = date('d/m/y H:i:s', $time); $buf = ''; if ($num == 0) { $buf .= ansi(" $title ", fg: AnsiColor::WHITE, bg: AnsiColor::MAGENTA, bold: true, fg_bright: true); $buf .= ansi(" $date ", fg: AnsiColor::WHITE, bg: AnsiColor::BLUE, fg_bright: true); $buf .= "\n"; } $letter = strtoupper($level->name[0]); $color = match ($level) { LogLevel::ERROR => AnsiColor::RED, LogLevel::INFO => AnsiColor::GREEN, LogLevel::DEBUG => AnsiColor::WHITE, LogLevel::WARNING => AnsiColor::YELLOW }; $buf .= ansi($letter.ansi('='.ansi($num, bold: true)), fg: $color).' '; $buf .= ansi($exec_time, fg: AnsiColor::CYAN).' '; if (!is_null($errno)) { $buf .= ansi($errfile, fg: AnsiColor::GREEN); $buf .= ansi(':', fg: AnsiColor::WHITE); $buf .= ansi($errline, fg: AnsiColor::GREEN, fg_bright: true); $buf .= ' ('.getPHPErrorName($errno).') '; } $buf .= $message."\n"; if (in_array($level, [LogLevel::ERROR, LogLevel::WARNING])) $buf .= backtrace_as_string(2)."\n"; $set_perm = false; if (!file_exists($this->logFile)) { $set_perm = true; $dir = dirname($this->logFile); echo "dir: $dir\n"; if (!file_exists($dir)) { mkdir($dir); setperm($dir); } } $f = fopen($this->logFile, 'a'); if (!$f) { fprintf(STDERR, __METHOD__.': failed to open file \''.$this->logFile.'\' for writing'); return; } fwrite($f, $buf); fclose($f); if ($set_perm) setperm($this->logFile); } } class DatabaseLogger extends Logger { protected function writer(LogLevel $level, int $num, string $message, ?int $errno = null, ?string $errfile = null, ?string $errline = null): void { $db = DB(); $data = [ 'ts' => time(), 'num' => $num, 'time' => exectime(), 'errno' => $errno, 'file' => $errfile, 'line' => $errline, 'text' => $message, 'level' => $level->value, 'stacktrace' => backtrace_as_string(2), 'is_cli' => intval(is_cli()), 'user_id' => 0, ]; if (is_cli()) { $data += [ 'ip' => '', 'ua' => '', 'url' => '', ]; } else { $data += [ 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']), 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'url' => $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] ]; } $db->insert('backend_errors', $data); } } function getPHPErrorName(int $errno): string { static $errors = null; if (is_null($errors)) $errors = array_flip(array_slice(get_defined_constants(true)['Core'], 0, 15, true)); return $errors[$errno]; } function strVarDump($var, bool $print_r = false): string { ob_start(); $print_r ? print_r($var) : var_dump($var); return trim(ob_get_clean()); } function strVars(array $args): string { $args = array_map(fn($a) => match (gettype($a)) { 'string' => $a, 'array', 'object' => strVarDump($a, true), default => strVarDump($a) }, $args); return implode(' ', $args); } function backtrace_as_string(int $shift = 0): string { $bt = debug_backtrace(); $lines = []; foreach ($bt as $i => $t) { if ($i < $shift) continue; if (!isset($t['file'])) { $lines[] = 'from ?'; } else { $lines[] = 'from '.$t['file'].':'.$t['line']; } } return implode("\n", $lines); }