ch1p_io_web/engine/logging.php
2024-01-31 20:45:40 +03:00

267 lines
7.9 KiB
PHP

<?php
require_once 'lib/ansi.php';
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, ...$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);
}