318 lines
9.5 KiB
PHP
318 lines
9.5 KiB
PHP
<?php
|
|
|
|
enum AnsiColor: int {
|
|
case BLACK = 0;
|
|
case RED = 1;
|
|
case GREEN = 2;
|
|
case YELLOW = 3;
|
|
case BLUE = 4;
|
|
case MAGENTA = 5;
|
|
case CYAN = 6;
|
|
case WHITE = 7;
|
|
}
|
|
|
|
function ansi(string $text,
|
|
?AnsiColor $fg = null,
|
|
?AnsiColor $bg = null,
|
|
bool $bold = false,
|
|
bool $fg_bright = false,
|
|
bool $bg_bright = false): string {
|
|
$codes = [];
|
|
if (!is_null($fg))
|
|
$codes[] = $fg->value + ($fg_bright ? 90 : 30);
|
|
if (!is_null($bg))
|
|
$codes[] = $bg->value + ($bg_bright ? 100 : 40);
|
|
if ($bold)
|
|
$codes[] = 1;
|
|
|
|
if (empty($codes))
|
|
return $text;
|
|
|
|
return "\033[".implode(';', $codes)."m".$text."\033[0m";
|
|
}
|
|
|
|
enum LogLevel: int {
|
|
case ERROR = 10;
|
|
case WARNING = 5;
|
|
case INFO = 3;
|
|
case DEBUG = 2;
|
|
}
|
|
|
|
function logDebug(...$args): void { global $__logger; $__logger?->log(LogLevel::DEBUG, null, ...$args); }
|
|
function logInfo(...$args): void { global $__logger; $__logger?->log(LogLevel::INFO, null, ...$args); }
|
|
function logWarning(...$args): void { global $__logger; $__logger?->log(LogLevel::WARNING, null, ...$args); }
|
|
function logError(...$args): void {
|
|
global $__logger;
|
|
if (array_key_exists('stacktrace', $args)) {
|
|
$st = $args['stacktrace'];
|
|
unset($args['stacktrace']);
|
|
} else {
|
|
$st = null;
|
|
}
|
|
if ($__logger?->canReport())
|
|
$__logger?->log(LogLevel::ERROR, $st, ...$args);
|
|
}
|
|
|
|
abstract class Logger {
|
|
protected bool $enabled = false;
|
|
protected int $counter = 0;
|
|
protected int $recursionLevel = 0;
|
|
|
|
/** @var ?callable $filter */
|
|
protected $filter = null;
|
|
|
|
public function setErrorFilter(callable $filter): void {
|
|
$this->filter = $filter;
|
|
}
|
|
|
|
public function disable(): void {
|
|
$this->enabled = false;
|
|
}
|
|
|
|
public 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);
|
|
});
|
|
|
|
set_exception_handler(function(\Throwable $e): void {
|
|
static::write(LogLevel::ERROR, get_class($e).': '.$e->getMessage(),
|
|
errfile: $e->getFile() ?: '?',
|
|
errline: $e->getLine() ?: 0,
|
|
stacktrace: $e->getTraceAsString());
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
public function log(LogLevel $level, ?string $stacktrace = null, ...$args): void {
|
|
if (!isDev() && $level == LogLevel::DEBUG)
|
|
return;
|
|
$this->write($level, strVars($args),
|
|
stacktrace: $stacktrace);
|
|
}
|
|
|
|
public function canReport(): bool {
|
|
return $this->recursionLevel < 3;
|
|
}
|
|
|
|
protected function write(LogLevel $level,
|
|
string $message,
|
|
?int $errno = null,
|
|
?string $errfile = null,
|
|
?string $errline = null,
|
|
?string $stacktrace = null): void {
|
|
$this->recursionLevel++;
|
|
|
|
if ($this->canReport())
|
|
$this->writer($level, $this->counter++, $message, $errno, $errfile, $errline, $stacktrace);
|
|
|
|
$this->recursionLevel--;
|
|
}
|
|
|
|
abstract protected function writer(LogLevel $level,
|
|
int $num,
|
|
string $message,
|
|
?int $errno = null,
|
|
?string $errfile = null,
|
|
?string $errline = null,
|
|
?string $stacktrace = null): void;
|
|
}
|
|
|
|
class FileLogger extends Logger {
|
|
|
|
public function __construct(protected string $logFile) {}
|
|
|
|
protected function writer(LogLevel $level,
|
|
int $num,
|
|
string $message,
|
|
?int $errno = null,
|
|
?string $errfile = null,
|
|
?string $errline = null,
|
|
?string $stacktrace = 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 = isCli() ? '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 .= ($stacktrace ?: backtraceAsString(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,
|
|
?string $stacktrace = null): void
|
|
{
|
|
$db = DB();
|
|
|
|
$data = [
|
|
'ts' => time(),
|
|
'num' => $num,
|
|
'time' => exectime(),
|
|
'errno' => $errno ?: 0,
|
|
'file' => $errfile ?: '?',
|
|
'line' => $errline ?: 0,
|
|
'text' => $message,
|
|
'level' => $level->value,
|
|
'stacktrace' => $stacktrace ?: backtraceAsString(2),
|
|
'is_cli' => intval(isCli()),
|
|
'admin_id' => isAdmin() ? admin::getId() : 0,
|
|
];
|
|
|
|
if (isCli()) {
|
|
$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] ?? null;
|
|
}
|
|
|
|
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 backtraceAsString(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);
|
|
} |