yf_logger class

Application 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.

Fileplugins/logs/classes/yf_logger.class.php
Use_class('logger')

Public properties

A site sets them in its config with $PROJECT_CONF['logger']['NAME'] = ...;

PropertyDefaultDescription
$FILE
string
'' File of the "file" handler, empty means LOGS_PATH . 'app.log'
$HANDLERS
array
['error_log'] Handler names (error_log|file|db|monolog) or callables taking (level, message, context)
$LEVEL
string
'debug' Least severe level that is logged (PSR-3 name)
$LEVELS
array
['emergency' => 0, 'alert' => 1, 'critical' => 2, 'error' => 3, 'warning' => 4, 'notice' => 5, ... (8 items)]
array (
  'emergency' => 0,
  'alert' => 1,
  'critical' => 2,
  'error' => 3,
  'warning' => 4,
  'notice' => 5,
  'info' => 6,
  'debug' => 7,
)
@conf_skip PSR-3 levels and their order, most severe first
$MONOLOG_STREAM
string
'' Stream (path or php://stderr) for a Monolog logger built on demand when none was set
$_busy
bool
false @conf_skip Re-entrancy guard: a failing handler must not log about itself
$_monolog
mixed
null @conf_skip Monolog logger or any object with a log(level, message, context) method
$_monolog_class
string
'Monolog\\Logger' @conf_skip Optional library: class names are properties so that static analysis does not need it
$_monolog_stream_class
string
'Monolog\\Handler\\StreamHandler' @conf_skip Handler of the Monolog logger built from MONOLOG_STREAM

Public methods 25

alert

public function alert($message, array $context = [])
  • $context array
    public function alert($message, array $context = [])
    {
        return $this->log('alert', $message, $context);
    }

critical

public function critical($message, array $context = [])
  • $context array
    public function critical($message, array $context = [])
    {
        return $this->log('critical', $message, $context);
    }

debug

public function debug($message, array $context = [])
  • $context array
    public function debug($message, array $context = [])
    {
        return $this->log('debug', $message, $context);
    }

emergency

public function emergency($message, array $context = [])
  • $context array
    public function emergency($message, array $context = [])
    {
        return $this->log('emergency', $message, $context);
    }

error

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;
    }

info

public function info($message, array $context = [])
  • $context array
    public function info($message, array $context = [])
    {
        return $this->log('info', $message, $context);
    }

interpolate

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);
    }

log

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;
    }

notice

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;
    }

set_monolog

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;
    }

warning

public function warning($message, array $context = [])
  • $context array
    public function warning($message, array $context = [])
    {
        return $this->log('warning', $message, $context);
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)
    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }

_debug_record

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(),
        ]);
    }

_dispatch

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();
    }

_get_monolog

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;
    }

_handler_db

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 : ''));
    }

_handler_file

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;
    }

_json

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;
    }

_normalize

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;
    }

_redactor

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;
        }
    }