src/Debug/Framework/Yii1_1/LogRoute.php
<?php
/**
* This file is part of PHPDebugConsole
*
* @package PHPDebugConsole
* @author Brad Kent <bkfake-github@yahoo.com>
* @license http://opensource.org/licenses/MIT MIT
* @copyright 2014-2024 Brad Kent
* @version v3.0
*/
namespace bdk\Debug\Framework\Yii1_1;
use bdk\Debug;
use CLogger;
use CLogRoute;
use Exception;
use Yii;
/**
* Yii v1.1 log router
*/
class LogRoute extends CLogRoute
{
public $levels = 'error, info, profile, trace, warning';
private $debug;
private $levelMap = array(
CLogger::LEVEL_ERROR => 'error',
CLogger::LEVEL_INFO => 'log',
CLogger::LEVEL_PROFILE => 'time',
CLogger::LEVEL_TRACE => 'trace',
CLogger::LEVEL_WARNING => 'warn',
);
/** @var LogEntryMeta */
protected $meta;
/** @var array stack of yii begin-profile log entries */
private $stack;
/**
* @var array $except An array of categories to exclude from logging.
* Regex pattern matching is supported
* We exclude system.db categories... handled via pdo wrapper
*/
protected $except = array(
'/^system\.db\./',
);
/**
* Constructor
*
* @param Debug $debug Debug instance
* @param array $opts Route options
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public function __construct(Debug $debug = null, $opts = array())
{
if (!$debug) {
$debug = Debug::getChannel('Yii');
} elseif ($debug === $debug->rootInstance) {
$debug = $debug->getChannel('Yii');
}
$this->meta = new LogRouteMeta($debug);
foreach ($opts as $k => $v) {
$setter = 'set' . \ucfirst($k);
if (\method_exists($this, $setter)) {
$this->{$setter}($v);
continue;
}
$this->{$k} = $v;
}
$debug->backtrace->addInternalClass(array(
'CLogger',
'CLogRoute',
'YiiBase',
));
$this->debug = $debug;
}
/**
* Retrieves filtered log messages from logger for further processing.
*
* @param CLogger $logger logger instance
* @param bool $processLogs whether to process the logs after they are collected from the logger. ALWAYS TRUE NOW!
*
* @return void
*/
#[\Override]
public function collectLogs($logger, $processLogs = false)
{
$processLogs = true;
parent::collectLogs($logger, $processLogs);
}
/**
* Get instance of this route
*
* @return Yii11LogRoute
*/
public static function getInstance()
{
$routes = Yii::app()->log->routes; // CMap obj
foreach ($routes as $route) {
if ($route instanceof static) {
return $route;
}
}
$route = new static();
$route->init();
$routes['phpDebugConsole'] = $route;
Yii::app()->log->routes = $routes;
return $route;
}
/**
* Initialize component
*
* @return void
*/
public function init()
{
parent::init();
// send each entry to debugger immediately
Yii::getLogger()->autoFlush = 1;
}
/**
* Are we excluding category?
*
* @param array $logEntry raw/indexed Yii log entry
*
* @return bool
*/
protected function isExcluded(array $logEntry)
{
$category = $logEntry[2];
if (\strpos($category, 'system.db') === 0 && \preg_match('/^(Opening|Closing)/', $logEntry[0])) {
return false;
}
foreach ($this->except as $except) {
if ($this->isExcludedTest($except, $category)) {
return true;
}
}
return false;
}
/**
* Test except string against category
*
* @param string $except category or regex to match against category
* @param string $category logEntry category
*
* @return bool
*/
private function isExcludedTest($except, $category)
{
// If found, we skip
if (\trim(\strtolower($except)) === \trim(\strtolower($category))) {
return true;
}
// Check for regex
return $except[0] === '/' && \preg_match($except, $category);
}
/**
* Convert Yii's list to key/value'd array
*
* @param array $logEntry raw/indexed Yii log entry
*
* @return array key=>value
*/
protected function normalizeMessage(array $logEntry)
{
$logEntry = \array_combine(
array('message', 'level', 'category', 'time'),
$logEntry
);
$logEntry = \array_merge($logEntry, array(
'channel' => $this->debug,
'meta' => array(),
'trace' => array(),
));
$haveTrace = $logEntry['level'] === CLogger::LEVEL_TRACE || YII_DEBUG && YII_TRACE_LEVEL > 0;
return $haveTrace
? $this->parseTrace($logEntry)
: $logEntry;
}
/**
* Yii's logger appends trace info to log message as a string
* extract it and move to 'trace'
*
* @param array $logEntry key/valued logentry
*
* @return array
*/
private function parseTrace(array $logEntry)
{
// if YII_DEBUG is on, we may have trace info
$regex = '#^in (.+) \((\d+)\)$#m';
$matches = array();
\preg_match_all($regex, $logEntry['message'], $matches, PREG_SET_ORDER);
// remove the trace info from the message
$logEntry['message'] = \rtrim(\preg_replace($regex, '', $logEntry['message']));
foreach ($matches as $line) {
$file = $line[1];
if (\strpos($file, __DIR__) === 0) {
continue;
}
$logEntry['trace'][] = array(
'file' => $file,
'line' => (int) $line[2],
);
}
return $logEntry;
}
/**
* Route log messages to PHPDebugConsole
*
* Extends CLogRoute
*
* @param array $logs list of log messages
*
* @return void
*/
#[\Override]
protected function processLogs($logs = array())
{
try {
foreach ($logs as $message) {
if ($this->isExcluded($message)) {
continue;
}
$this->processLogEntry($message);
}
// Processed, clear!
$this->logs = null;
} catch (Exception $e) {
\trigger_error(__METHOD__ . ': Exception processing application logs: ' . $e->getMessage());
}
}
/**
* Handle Yii log entry
*
* @param array $logEntry our key/value'd log entry
*
* @return void
*/
protected function processLogEntry(array $logEntry)
{
$logEntry = $this->normalizeMessage($logEntry);
$logEntry = $this->meta->messageMeta($logEntry);
if ($logEntry['level'] === CLogger::LEVEL_PROFILE) {
$this->processLogEntryProfile($logEntry);
return;
}
if ($logEntry['level'] === CLogger::LEVEL_TRACE) {
if (\count($logEntry['trace']) > 1) {
$this->processLogEntryTrace($logEntry);
return;
}
$logEntry['level'] = CLogger::LEVEL_INFO;
}
$debug = $logEntry['channel'];
$args = \array_filter(array(
\ltrim($logEntry['category'] . ':', ':'),
$logEntry['message'],
));
if ($logEntry['meta']) {
$args[] = $debug->meta($logEntry['meta']);
}
$method = $this->levelMap[$logEntry['level']];
\call_user_func_array(array($debug, $method), $args);
}
/**
* Handle Yii profile log entry
*
* @param array $logEntry our key/value'd log entry
*
* @return void
*/
private function processLogEntryProfile(array $logEntry)
{
if (\strpos($logEntry['message'], 'begin:') === 0) {
// add to stack
$logEntry['message'] = \substr($logEntry['message'], 6);
$this->stack[] = $logEntry;
return;
}
$logEntryBegin = \array_pop($this->stack);
$message = $logEntryBegin['category']
? $logEntryBegin['category'] . ': ' . $logEntryBegin['message']
: $logEntryBegin['message'];
$duration = $logEntry['time'] - $logEntryBegin['time'];
$debug = $logEntry['channel'];
$method = $this->levelMap[$logEntry['level']];
$args = array($message, $duration);
\call_user_func_array(array($debug, $method), $args);
}
/**
* Handle Yii trace log entry
*
* @param array $logEntry our key/value'd log entry
*
* @return void
*/
private function processLogEntryTrace(array $logEntry)
{
$caption = $logEntry['category']
? $logEntry['category'] . ': ' . $logEntry['message']
: $logEntry['message'];
$logEntry['meta']['columns'] = array('file', 'line');
$logEntry['meta']['trace'] = $logEntry['trace'];
$debug = $logEntry['channel'];
$method = $this->levelMap[$logEntry['level']];
$args = array(false, $caption, $debug->meta($logEntry['meta']));
\call_user_func_array(array($debug, $method), $args);
}
}