yf_logger classApplication logger with the PSR-3 method set, usable as logger().
Handlers are named in $HANDLERS: error_log (default), file, db, monolog, or a callable.
Sites configure it with $PROJECT_CONF['logger'][...] and may override it as site__logger.
| File | plugins/logs/classes/yf_logger.class.php |
|---|---|
| Use | _class('logger') |
A site sets them in its config with $PROJECT_CONF['logger']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$FILEstring |
'' |
File of the "file" handler, empty means LOGS_PATH . 'app.log' |
$HANDLERSarray |
['error_log'] |
Handler names (error_log|file|db|monolog) or callables taking (level, message, context) |
$LEVELstring |
'debug' |
Least severe level that is logged (PSR-3 name) |
$LEVELSarray |
|
@conf_skip PSR-3 levels and their order, most severe first |
$MONOLOG_STREAMstring |
'' |
Stream (path or php://stderr) for a Monolog logger built on demand when none was set |
$_busybool |
false |
@conf_skip Re-entrancy guard: a failing handler must not log about itself |
$_monologmixed |
null |
@conf_skip Monolog logger or any object with a log(level, message, context) method |
$_monolog_classstring |
'Monolog\\Logger' |
@conf_skip Optional library: class names are properties so that static analysis does not need it |
$_monolog_stream_classstring |
'Monolog\\Handler\\StreamHandler' |
@conf_skip Handler of the Monolog logger built from MONOLOG_STREAM |
public function alert($message, array $context = [])
$context array public function alert($message, array $context = [])
{
return $this->log('alert', $message, $context);
}
public function critical($message, array $context = [])
$context array public function critical($message, array $context = [])
{
return $this->log('critical', $message, $context);
}
public function debug($message, array $context = [])
$context array public function debug($message, array $context = [])
{
return $this->log('debug', $message, $context);
}
public function emergency($message, array $context = [])
$context array public function emergency($message, array $context = [])
{
return $this->log('emergency', $message, $context);
}
public function error($message, array $context = [])
$context array public function error($message, array $context = [])
{
return $this->log('error', $message, $context);
}
public function handlers_active()
Handlers that will really receive records: unknown names and a monolog without a logger are listed as missing.
Returns array ['active' => [...], 'skipped' => [...]]
public function handlers_active()
{
$out = ['active' => [], 'skipped' => []];
foreach ((array) $this->HANDLERS as $handler) {
$name = is_string($handler) ? $handler : 'callback';
if (is_string($handler) && ! in_array($handler, ['error_log', 'file', 'db', 'monolog'], true)) {
$out['skipped'][] = $name;
} elseif ($handler === 'monolog' && ! $this->_get_monolog()) {
$out['skipped'][] = $name;
} elseif ( ! is_string($handler) && ! is_callable($handler)) {
$out['skipped'][] = $name;
} else {
$out['active'][] = $name;
}
}
return $out;
}
public function info($message, array $context = [])
$context array public function info($message, array $context = [])
{
return $this->log('info', $message, $context);
}
public function interpolate($message, array $context = [])
PSR-3 placeholders: {key} is replaced for scalars, null (empty) and objects with __toString.
$context array public function interpolate($message, array $context = [])
{
$message = (string) (is_scalar($message) || $message instanceof Stringable ? $message : '');
if ($context === [] || strpos($message, '{') === false) {
return $message;
}
$replace = [];
foreach ($context as $key => $val) {
if ($val === null) {
$replace['{' . $key . '}'] = '';
} elseif (is_bool($val)) {
$replace['{' . $key . '}'] = $val ? 'true' : 'false';
} elseif (is_scalar($val) || $val instanceof Stringable) {
$replace['{' . $key . '}'] = (string) $val;
}
}
return strtr($message, $replace);
}
public function log($level, $message, array $context = [])
Log with an arbitrary level. Never throws: helpers are called blindly by sites, so an unknown level is logged as info. Returns true when at least one handler took the record.
$context array public function log($level, $message, array $context = [])
{
if ($this->_busy) {
return false;
}
$level = is_string($level) ? strtolower($level) : '';
if ( ! isset($this->LEVELS[$level])) {
$level = 'info';
}
$limit = $this->LEVELS[strtolower((string) $this->LEVEL)] ?? $this->LEVELS['debug'];
if ($this->LEVELS[$level] > $limit) {
return false;
}
$this->_busy = true;
$done = false;
try {
$redactor = $this->_redactor();
$redactor && $context = $redactor->_log_redact_array($context, $redactor->_log_redact_pattern());
$message = $this->interpolate($message, $context);
$exception = $context['exception'] ?? null;
if ($exception instanceof Throwable) {
$message .= ' ' . $this->_exception_line($exception);
}
foreach ((array) $this->HANDLERS as $handler) {
try {
$done = $this->_dispatch($handler, $level, $message, $context) || $done;
} catch (Throwable $e) {
// a broken sink must not take the request down or hide the other sinks
}
}
defined('DEBUG_MODE') && DEBUG_MODE && $this->_debug_record($level, $message, $context, $done);
} catch (Throwable $e) {
$done = false;
}
$this->_busy = false;
return $done;
}
public function notice($message, array $context = [])
$context array public function notice($message, array $context = [])
{
return $this->log('notice', $message, $context);
}
public function set_handler_callback(callable $callback)
Replace all handlers by one callable fn($level, $message, $context).
$callback callable public function set_handler_callback(callable $callback)
{
$this->HANDLERS = [$callback];
return $this;
}
public function set_monolog($logger)
Use any Monolog logger, or an object with log($level, $message, $context).
public function set_monolog($logger)
{
$this->_monolog = is_object($logger) ? $logger : null;
return $this;
}
public function warning($message, array $context = [])
$context array public function warning($message, array $context = [])
{
return $this->log('warning', $message, $context);
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function _debug_record($level, $message, array $context, $done)
The record for the debug console's logger tab (DEBUG_MODE only; debug() keeps at most 500), after redaction and interpolation, as the handlers got it.
$context array public function _debug_record($level, $message, array $context, $done)
{
// full: no trace to build for a record debug() would drop
if (count((array) debug('logger')) >= 500) {
return false;
}
$handlers = [];
foreach ((array) $this->HANDLERS as $handler) {
$handlers[] = is_string($handler) ? $handler : 'callable';
}
return debug('logger[]', [
'level' => (string) $level,
'message' => (string) $message,
'context' => $this->_normalize($context),
'handlers' => $handlers,
'done' => (bool) $done,
'time' => microtime(true),
'trace' => main()->trace_string(),
]);
}
public function _dispatch($handler, $level, $message, array $context)
$context array public function _dispatch($handler, $level, $message, array $context)
{
if (is_string($handler)) {
$method = '_handler_' . $handler;
if (in_array($handler, ['error_log', 'file', 'db', 'monolog'], true)) {
return (bool) $this->$method($level, $message, $context);
}
}
if (is_callable($handler)) {
$handler($level, $message, $context);
return true;
}
return false;
}
public function _exception_line(Throwable $e)
$e Throwable public function _exception_line(Throwable $e)
{
return get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
}
public function _get_monolog()
Returns null|object
public function _get_monolog()
{
if ($this->_monolog === null && $this->MONOLOG_STREAM && class_exists($this->_monolog_class)) {
$monolog = new $this->_monolog_class('yf');
$monolog->pushHandler(new $this->_monolog_stream_class($this->MONOLOG_STREAM));
$this->_monolog = $monolog;
}
return $this->_monolog;
}
public function _handler_db($level, $message, array $context)
Writes error and above into sys_log_core_errors: message to error_text, context JSON to env_data, file / line from the context or the exception. The yf_logs_core_error class is an empty stub, so no API of it is used.
$context array public function _handler_db($level, $message, array $context)
{
if ($this->LEVELS[$level] > $this->LEVELS['error'] || ! is_object(main()->db ?? null)) {
return false;
}
$exception = $context['exception'] ?? null;
$file = $context['file'] ?? ($exception instanceof Throwable ? $exception->getFile() : '');
$line = $context['line'] ?? ($exception instanceof Throwable ? $exception->getLine() : 0);
$type = $context['type'] ?? null;
$redactor = $this->_redactor();
$pattern = $redactor ? $redactor->_log_redact_pattern() : '';
$request = function ($key) use ($redactor, $pattern) {
$value = (string) ($_SERVER[$key] ?? '');
if ($redactor) {
$value = $key === 'QUERY_STRING' ? $redactor->_log_redact_query($value, $pattern) : $redactor->_log_redact_url($value, $pattern);
}
return substr($value, 0, 255);
};
$trace = '';
if ($exception instanceof Throwable) {
$trace = $redactor ? $redactor->_log_exception_trace($exception) : $exception->getTraceAsString();
}
$res = db()->insert_safe('log_core_errors', [
'error_level' => is_int($type) ? $type : E_ERROR,
'error_text' => $message,
'source_file' => substr((string) $file, 0, 255),
'source_line' => min(65535, max(0, (int) $line)),
'env_data' => $this->_json($this->_normalize($context, true)),
'user_id' => (int) ($_SESSION['user_id'] ?? 0),
'is_admin' => defined('MAIN_TYPE_ADMIN') && MAIN_TYPE_ADMIN ? '1' : '0',
'site_id' => (int) conf('SITE_ID'),
'date' => time(),
'ip' => substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 16),
'query_string' => $request('QUERY_STRING'),
'request_uri' => $request('REQUEST_URI'),
'user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
'referer' => $request('HTTP_REFERER'),
'object' => substr((string) ($_GET['object'] ?? ''), 0, 255),
'action' => substr((string) ($_GET['action'] ?? ''), 0, 255),
'trace' => substr($trace, 0, 1024),
]);
return (bool) $res;
}
public function _handler_error_log($level, $message, array $context)
$context array public function _handler_error_log($level, $message, array $context)
{
unset($context['exception']);
$json = $context ? $this->_json($this->_normalize($context)) : '';
return error_log('[' . $level . '] ' . $message . ($json !== '' ? ' ' . $json : ''));
}
public function _handler_file($level, $message, array $context)
$context array public function _handler_file($level, $message, array $context)
{
$file = $this->FILE ?: (defined('LOGS_PATH') ? LOGS_PATH : '') . 'app.log';
$dir = dirname($file);
if ( ! is_dir($dir) && ! @mkdir($dir, 0755, true) && ! is_dir($dir)) {
return false;
}
$line = $this->_json([
'time' => date('Y-m-d H:i:s'),
'level' => $level,
'message' => $message,
'context' => $this->_normalize($context, true),
]);
if ($line === '') {
return false;
}
return @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX) !== false;
}
public function _handler_monolog($level, $message, array $context)
$context array public function _handler_monolog($level, $message, array $context)
{
$monolog = $this->_get_monolog();
if ( ! $monolog) {
return false;
}
$monolog->log($level, $message, $context);
return true;
}
public function _json($data)
public function _json($data)
{
$json = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR);
return $json === false ? '' : $json;
}
public function _normalize($value, $with_trace = false, $depth = 0)
Context that json_encode() can take: throwables become arrays (with the trace on demand), other objects their class name, resources a type tag.
public function _normalize($value, $with_trace = false, $depth = 0)
{
if ($value instanceof Throwable) {
$out = ['class' => get_class($value), 'message' => $value->getMessage(), 'file' => $value->getFile(), 'line' => $value->getLine()];
if ($with_trace) {
$redactor = $this->_redactor();
$out['trace'] = $redactor ? $redactor->_log_exception_trace($value) : $value->getTraceAsString();
}
return $out;
}
if (is_array($value)) {
if ($depth > 5) {
return '[array]';
}
foreach ($value as $k => $v) {
$value[$k] = $this->_normalize($v, $with_trace, $depth + 1);
}
return $value;
}
if ($value instanceof Stringable) {
return (string) $value;
}
if (is_object($value)) {
return '[object ' . get_class($value) . ']';
}
if (is_resource($value)) {
return '[resource]';
}
return $value;
}
public function _redactor()
The error handler when its LOG_REDACT is on: context keys, the request columns of the db handler and exception traces are then masked like the error log rows, once before the handlers run (a Throwable passed to monolog or a callable keeps its own trace).
Returns null|object
public function _redactor()
{
try {
$handler = function_exists('main') && is_object(main()) ? (main()->error_handler ?? null) : null;
return is_object($handler) && ! empty($handler->LOG_REDACT) && method_exists($handler, '_log_exception_trace') ? $handler : null;
} catch (Throwable $e) {
return null;
}
}