diff options
Diffstat (limited to 'includes/debug')
-rw-r--r-- | includes/debug/MWDebug.php | 33 | ||||
-rw-r--r-- | includes/debug/logger/LegacyLogger.php | 380 | ||||
-rw-r--r-- | includes/debug/logger/LegacySpi.php | 59 | ||||
-rw-r--r-- | includes/debug/logger/LoggerFactory.php | 121 | ||||
-rw-r--r-- | includes/debug/logger/MonologSpi.php | 251 | ||||
-rw-r--r-- | includes/debug/logger/NullSpi.php | 64 | ||||
-rw-r--r-- | includes/debug/logger/Spi.php | 47 | ||||
-rw-r--r-- | includes/debug/logger/monolog/LegacyFormatter.php | 48 | ||||
-rw-r--r-- | includes/debug/logger/monolog/LegacyHandler.php | 243 | ||||
-rw-r--r-- | includes/debug/logger/monolog/SyslogHandler.php | 96 | ||||
-rw-r--r-- | includes/debug/logger/monolog/WikiProcessor.php | 47 |
11 files changed, 1377 insertions, 12 deletions
diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index c2f22233..ae2d9954 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -26,8 +26,6 @@ * By default, most methods do nothing ( self::$enabled = false ). You have * to explicitly call MWDebug::init() to enabled them. * - * @todo Profiler support - * * @since 1.19 */ class MWDebug { @@ -46,7 +44,7 @@ class MWDebug { protected static $debug = array(); /** - * SQL statements of the databses queries. + * SQL statements of the database queries. * * @var array $query */ @@ -311,12 +309,25 @@ class MWDebug { * * @since 1.19 * @param string $str + * @param array $context */ - public static function debugMsg( $str ) { + public static function debugMsg( $str, $context = array() ) { global $wgDebugComments, $wgShowDebug; if ( self::$enabled || $wgDebugComments || $wgShowDebug ) { - self::$debug[] = rtrim( UtfNormal::cleanUp( $str ) ); + if ( $context ) { + $prefix = ''; + if ( isset( $context['prefix'] ) ) { + $prefix = $context['prefix']; + } elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) { + $prefix = "[{$context['channel']}] "; + } + if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) { + $prefix .= "{$context['seconds_elapsed']} {$context['memory_used']} "; + } + $str = $prefix . $str; + } + self::$debug[] = rtrim( UtfNormal\Validator::cleanUp( $str ) ); } } @@ -529,12 +540,11 @@ class MWDebug { MWDebug::log( 'MWDebug output complete' ); $debugInfo = self::getDebugInfo( $context ); - $result->setIndexedTagName( $debugInfo, 'debuginfo' ); - $result->setIndexedTagName( $debugInfo['log'], 'line' ); - $result->setIndexedTagName( $debugInfo['debugLog'], 'msg' ); - $result->setIndexedTagName( $debugInfo['queries'], 'query' ); - $result->setIndexedTagName( $debugInfo['includes'], 'queries' ); - $result->setIndexedTagName( $debugInfo['profile'], 'function' ); + ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' ); + ApiResult::setIndexedTagName( $debugInfo['log'], 'line' ); + ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' ); + ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' ); + ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' ); $result->addValue( null, 'debuginfo', $debugInfo ); } @@ -578,7 +588,6 @@ class MWDebug { 'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ), 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ), 'includes' => self::getFilesIncluded( $context ), - 'profile' => Profiler::instance()->getRawData(), ); } } diff --git a/includes/debug/logger/LegacyLogger.php b/includes/debug/logger/LegacyLogger.php new file mode 100644 index 00000000..edaef4a7 --- /dev/null +++ b/includes/debug/logger/LegacyLogger.php @@ -0,0 +1,380 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +use DateTimeZone; +use MWDebug; +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; +use UDPTransport; + +/** + * PSR-3 logger that mimics the historic implementation of MediaWiki's + * wfErrorLog logging implementation. + * + * This logger is configured by the following global configuration variables: + * - `$wgDebugLogFile` + * - `$wgDebugLogGroups` + * - `$wgDBerrorLog` + * - `$wgDBerrorLogTZ` + * + * See documentation in DefaultSettings.php for detailed explanations of each + * variable. + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class LegacyLogger extends AbstractLogger { + + /** + * @var string $channel + */ + protected $channel; + + /** + * Convert Psr\Log\LogLevel constants into int for sane comparisons + * These are the same values that Monlog uses + * + * @var array + */ + protected static $levelMapping = array( + LogLevel::DEBUG => 100, + LogLevel::INFO => 200, + LogLevel::NOTICE => 250, + LogLevel::WARNING => 300, + LogLevel::ERROR => 400, + LogLevel::CRITICAL => 500, + LogLevel::ALERT => 550, + LogLevel::EMERGENCY => 600, + ); + + + /** + * @param string $channel + */ + public function __construct( $channel ) { + $this->channel = $channel; + } + + /** + * Logs with an arbitrary level. + * + * @param string|int $level + * @param string $message + * @param array $context + */ + public function log( $level, $message, array $context = array() ) { + if ( self::shouldEmit( $this->channel, $message, $level, $context ) ) { + $text = self::format( $this->channel, $message, $context ); + $destination = self::destination( $this->channel, $message, $context ); + self::emit( $text, $destination ); + } + // Add to debug toolbar + MWDebug::debugMsg( $message, array( 'channel' => $this->channel ) + $context ); + } + + + /** + * Determine if the given message should be emitted or not. + * + * @param string $channel + * @param string $message + * @param string|int $level Psr\Log\LogEvent constant or Monlog level int + * @param array $context + * @return bool True if message should be sent to disk/network, false + * otherwise + */ + public static function shouldEmit( $channel, $message, $level, $context ) { + global $wgDebugLogFile, $wgDBerrorLog, $wgDebugLogGroups; + + if ( $channel === 'wfLogDBError' ) { + // wfLogDBError messages are emitted if a database log location is + // specfied. + $shouldEmit = (bool)$wgDBerrorLog; + + } elseif ( $channel === 'wfErrorLog' ) { + // All messages on the wfErrorLog channel should be emitted. + $shouldEmit = true; + + } elseif ( isset( $wgDebugLogGroups[$channel] ) ) { + $logConfig = $wgDebugLogGroups[$channel]; + + if ( is_array( $logConfig ) ) { + $shouldEmit = true; + if ( isset( $logConfig['sample'] ) ) { + // Emit randomly with a 1 in 'sample' chance for each message. + $shouldEmit = mt_rand( 1, $logConfig['sample'] ) === 1; + } + + if ( isset( $logConfig['level'] ) ) { + if ( is_string( $level ) ) { + $level = self::$levelMapping[$level]; + } + $shouldEmit = $level >= self::$levelMapping[$logConfig['level']]; + } + } else { + // Emit unless the config value is explictly false. + $shouldEmit = $logConfig !== false; + } + + } elseif ( isset( $context['private'] ) && $context['private'] ) { + // Don't emit if the message didn't match previous checks based on + // the channel and the event is marked as private. This check + // discards messages sent via wfDebugLog() with dest == 'private' + // and no explicit wgDebugLogGroups configuration. + $shouldEmit = false; + } else { + // Default return value is the same as the historic wfDebug + // method: emit if $wgDebugLogFile has been set. + $shouldEmit = $wgDebugLogFile != ''; + } + + return $shouldEmit; + } + + + /** + * Format a message. + * + * Messages to the 'wfDebug', 'wfLogDBError' and 'wfErrorLog' channels + * receive special fomatting to mimic the historic output of the functions + * of the same name. All other channel values are formatted based on the + * historic output of the `wfDebugLog()` global function. + * + * @param string $channel + * @param string $message + * @param array $context + * @return string + */ + public static function format( $channel, $message, $context ) { + global $wgDebugLogGroups; + + if ( $channel === 'wfDebug' ) { + $text = self::formatAsWfDebug( $channel, $message, $context ); + + } elseif ( $channel === 'wfLogDBError' ) { + $text = self::formatAsWfLogDBError( $channel, $message, $context ); + + } elseif ( $channel === 'wfErrorLog' ) { + $text = "{$message}\n"; + + } elseif ( $channel === 'profileoutput' ) { + // Legacy wfLogProfilingData formatitng + $forward = ''; + if ( isset( $context['forwarded_for'] )) { + $forward = " forwarded for {$context['forwarded_for']}"; + } + if ( isset( $context['client_ip'] ) ) { + $forward .= " client IP {$context['client_ip']}"; + } + if ( isset( $context['from'] ) ) { + $forward .= " from {$context['from']}"; + } + if ( $forward ) { + $forward = "\t(proxied via {$context['proxy']}{$forward})"; + } + if ( $context['anon'] ) { + $forward .= ' anon'; + } + if ( !isset( $context['url'] ) ) { + $context['url'] = 'n/a'; + } + + $log = sprintf( "%s\t%04.3f\t%s%s\n", + gmdate( 'YmdHis' ), $context['elapsed'], $context['url'], $forward ); + + $text = self::formatAsWfDebugLog( + $channel, $log . $context['output'], $context ); + + } elseif ( !isset( $wgDebugLogGroups[$channel] ) ) { + $text = self::formatAsWfDebug( + $channel, "[{$channel}] {$message}", $context ); + + } else { + // Default formatting is wfDebugLog's historic style + $text = self::formatAsWfDebugLog( $channel, $message, $context ); + } + + return self::interpolate( $text, $context ); + } + + + /** + * Format a message as `wfDebug()` would have formatted it. + * + * @param string $channel + * @param string $message + * @param array $context + * @return string + */ + protected static function formatAsWfDebug( $channel, $message, $context ) { + $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $message ); + if ( isset( $context['seconds_elapsed'] ) ) { + // Prepend elapsed request time and real memory usage with two + // trailing spaces. + $text = "{$context['seconds_elapsed']} {$context['memory_used']} {$text}"; + } + if ( isset( $context['prefix'] ) ) { + $text = "{$context['prefix']}{$text}"; + } + return "{$text}\n"; + } + + + /** + * Format a message as `wfLogDBError()` would have formatted it. + * + * @param string $channel + * @param string $message + * @param array $context + * @return string + */ + protected static function formatAsWfLogDBError( $channel, $message, $context ) { + global $wgDBerrorLogTZ; + static $cachedTimezone = null; + + if ( $wgDBerrorLogTZ && !$cachedTimezone ) { + $cachedTimezone = new DateTimeZone( $wgDBerrorLogTZ ); + } + + // Workaround for https://bugs.php.net/bug.php?id=52063 + // Can be removed when min PHP > 5.3.6 + if ( $cachedTimezone === null ) { + $d = date_create( 'now' ); + } else { + $d = date_create( 'now', $cachedTimezone ); + } + $date = $d->format( 'D M j G:i:s T Y' ); + + $host = wfHostname(); + $wiki = wfWikiID(); + + $text = "{$date}\t{$host}\t{$wiki}\t{$message}\n"; + return $text; + } + + + /** + * Format a message as `wfDebugLog() would have formatted it. + * + * @param string $channel + * @param string $message + * @param array $context + */ + protected static function formatAsWfDebugLog( $channel, $message, $context ) { + $time = wfTimestamp( TS_DB ); + $wiki = wfWikiID(); + $host = wfHostname(); + $text = "{$time} {$host} {$wiki}: {$message}\n"; + return $text; + } + + + /** + * Interpolate placeholders in logging message. + * + * @param string $message + * @param array $context + * @return string Interpolated message + */ + public static function interpolate( $message, array $context ) { + if ( strpos( $message, '{' ) !== false ) { + $replace = array(); + foreach ( $context as $key => $val ) { + $replace['{' . $key . '}'] = $val; + } + $message = strtr( $message, $replace ); + } + return $message; + } + + + /** + * Select the appropriate log output destination for the given log event. + * + * If the event context contains 'destination' + * + * @param string $channel + * @param string $message + * @param array $context + * @return string + */ + protected static function destination( $channel, $message, $context ) { + global $wgDebugLogFile, $wgDBerrorLog, $wgDebugLogGroups; + + // Default destination is the debug log file as historically used by + // the wfDebug function. + $destination = $wgDebugLogFile; + + if ( isset( $context['destination'] ) ) { + // Use destination explicitly provided in context + $destination = $context['destination']; + + } elseif ( $channel === 'wfDebug' ) { + $destination = $wgDebugLogFile; + + } elseif ( $channel === 'wfLogDBError' ) { + $destination = $wgDBerrorLog; + + } elseif ( isset( $wgDebugLogGroups[$channel] ) ) { + $logConfig = $wgDebugLogGroups[$channel]; + + if ( is_array( $logConfig ) ) { + $destination = $logConfig['destination']; + } else { + $destination = strval( $logConfig ); + } + } + + return $destination; + } + + + /** + * Log to a file without getting "file size exceeded" signals. + * + * Can also log to UDP with the syntax udp://host:port/prefix. This will send + * lines to the specified port, prefixed by the specified prefix and a space. + * + * @param string $text + * @param string $file Filename + * @throws MWException + */ + public static function emit( $text, $file ) { + if ( substr( $file, 0, 4 ) == 'udp:' ) { + $transport = UDPTransport::newFromString( $file ); + $transport->emit( $text ); + } else { + wfSuppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || + ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) + ) { + file_put_contents( $file, $text, FILE_APPEND ); + } + wfRestoreWarnings(); + } + } + +} diff --git a/includes/debug/logger/LegacySpi.php b/includes/debug/logger/LegacySpi.php new file mode 100644 index 00000000..1bf39e41 --- /dev/null +++ b/includes/debug/logger/LegacySpi.php @@ -0,0 +1,59 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +/** + * LoggerFactory service provider that creates LegacyLogger instances. + * + * Usage: + * @code + * $wgMWLoggerDefaultSpi = array( + * 'class' => '\\MediaWiki\\Logger\\LegacySpi', + * ); + * @endcode + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class LegacySpi implements Spi { + + /** + * @var array $singletons + */ + protected $singletons = array(); + + + /** + * Get a logger instance. + * + * @param string $channel Logging channel + * @return \Psr\Log\LoggerInterface Logger instance + */ + public function getLogger( $channel ) { + if ( !isset( $this->singletons[$channel] ) ) { + $this->singletons[$channel] = new LegacyLogger( $channel ); + } + return $this->singletons[$channel]; + } + +} diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php new file mode 100644 index 00000000..b3078b9a --- /dev/null +++ b/includes/debug/logger/LoggerFactory.php @@ -0,0 +1,121 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +use ObjectFactory; + +/** + * PSR-3 logger instance factory. + * + * Creation of \Psr\Log\LoggerInterface instances is managed via the + * LoggerFactory::getInstance() static method which in turn delegates to the + * currently registered service provider. + * + * A service provider is any class implementing the Spi interface. + * There are two possible methods of registering a service provider. The + * LoggerFactory::registerProvider() static method can be called at any time + * to change the service provider. If LoggerFactory::getInstance() is called + * before any service provider has been registered, it will attempt to use the + * $wgMWLoggerDefaultSpi global to bootstrap Spi registration. + * $wgMWLoggerDefaultSpi is expected to be an array usable by + * ObjectFactory::getObjectFromSpec() to create a class. + * + * @see \MediaWiki\Logger\Spi + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class LoggerFactory { + + /** + * Service provider. + * @var Spi $spi + */ + private static $spi; + + + /** + * Register a service provider to create new \Psr\Log\LoggerInterface + * instances. + * + * @param Spi $provider Provider to register + */ + public static function registerProvider( Spi $provider ) { + self::$spi = $provider; + } + + + /** + * Get the registered service provider. + * + * If called before any service provider has been registered, it will + * attempt to use the $wgMWLoggerDefaultSpi global to bootstrap + * Spi registration. $wgMWLoggerDefaultSpi is expected to be an + * array usable by ObjectFactory::getObjectFromSpec() to create a class. + * + * @return Spi + * @see registerProvider() + * @see ObjectFactory::getObjectFromSpec() + */ + public static function getProvider() { + if ( self::$spi === null ) { + global $wgMWLoggerDefaultSpi; + $provider = ObjectFactory::getObjectFromSpec( + $wgMWLoggerDefaultSpi + ); + self::registerProvider( $provider ); + } + return self::$spi; + } + + + /** + * Get a named logger instance from the currently configured logger factory. + * + * @param string $channel Logger channel (name) + * @return \Psr\Log\LoggerInterface + */ + public static function getInstance( $channel ) { + if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + $message = ( + 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' . + "library</a> to be present. This library is not embedded directly in MediaWiki's " . + "git repository and must be installed separately by the end user.\n\n" . + 'Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git' . + '#Fetch_external_libraries">mediawiki.org</a> for help on installing ' . + 'the required components.' + ); + echo $message; + trigger_error( $message, E_USER_ERROR ); + die( 1 ); + } + + return self::getProvider()->getLogger( $channel ); + } + + + /** + * Construction of utility class is not allowed. + */ + private function __construct() { + // no-op + } +} diff --git a/includes/debug/logger/MonologSpi.php b/includes/debug/logger/MonologSpi.php new file mode 100644 index 00000000..a07fdc4a --- /dev/null +++ b/includes/debug/logger/MonologSpi.php @@ -0,0 +1,251 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +use Monolog\Logger; +use ObjectFactory; + +/** + * LoggerFactory service provider that creates loggers implemented by + * Monolog. + * + * Configured using an array of configuration data with the keys 'loggers', + * 'processors', 'handlers' and 'formatters'. + * + * The ['loggers']['@default'] configuration will be used to create loggers + * for any channel that isn't explicitly named in the 'loggers' configuration + * section. + * + * Configuration will most typically be provided in the $wgMWLoggerDefaultSpi + * global configuration variable used by LoggerFactory to construct its + * default SPI provider: + * @code + * $wgMWLoggerDefaultSpi = array( + * 'class' => '\\MediaWiki\\Logger\\MonologSpi', + * 'args' => array( array( + * 'loggers' => array( + * '@default' => array( + * 'processors' => array( 'wiki', 'psr', 'pid', 'uid', 'web' ), + * 'handlers' => array( 'stream' ), + * ), + * 'runJobs' => array( + * 'processors' => array( 'wiki', 'psr', 'pid' ), + * 'handlers' => array( 'stream' ), + * ) + * ), + * 'processors' => array( + * 'wiki' => array( + * 'class' => '\\MediaWiki\\Logger\\Monolog\\WikiProcessor', + * ), + * 'psr' => array( + * 'class' => '\\Monolog\\Processor\\PsrLogMessageProcessor', + * ), + * 'pid' => array( + * 'class' => '\\Monolog\\Processor\\ProcessIdProcessor', + * ), + * 'uid' => array( + * 'class' => '\\Monolog\\Processor\\UidProcessor', + * ), + * 'web' => array( + * 'class' => '\\Monolog\\Processor\\WebProcessor', + * ), + * ), + * 'handlers' => array( + * 'stream' => array( + * 'class' => '\\Monolog\\Handler\\StreamHandler', + * 'args' => array( 'path/to/your.log' ), + * 'formatter' => 'line', + * ), + * 'redis' => array( + * 'class' => '\\Monolog\\Handler\\RedisHandler', + * 'args' => array( function() { + * $redis = new Redis(); + * $redis->connect( '127.0.0.1', 6379 ); + * return $redis; + * }, + * 'logstash' + * ), + * 'formatter' => 'logstash', + * ), + * 'udp2log' => array( + * 'class' => '\\MediaWiki\\Logger\\Monolog\\LegacyHandler', + * 'args' => array( + * 'udp://127.0.0.1:8420/mediawiki + * ), + * 'formatter' => 'line', + * ), + * ), + * 'formatters' => array( + * 'line' => array( + * 'class' => '\\Monolog\\Formatter\\LineFormatter', + * ), + * 'logstash' => array( + * 'class' => '\\Monolog\\Formatter\\LogstashFormatter', + * 'args' => array( 'mediawiki', php_uname( 'n' ), null, '', 1 ), + * ), + * ), + * ) ), + * ); + * @endcode + * + * @see https://github.com/Seldaek/monolog + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class MonologSpi implements Spi { + + /** + * @var array $singletons + */ + protected $singletons; + + /** + * Configuration for creating new loggers. + * @var array $config + */ + protected $config; + + + /** + * @param array $config Configuration data. + */ + public function __construct( array $config ) { + $this->config = $config; + $this->reset(); + } + + + /** + * Reset internal caches. + * + * This is public for use in unit tests. Under normal operation there should + * be no need to flush the caches. + */ + public function reset() { + $this->singletons = array( + 'loggers' => array(), + 'handlers' => array(), + 'formatters' => array(), + 'processors' => array(), + ); + } + + + /** + * Get a logger instance. + * + * Creates and caches a logger instance based on configuration found in the + * $wgMWLoggerMonologSpiConfig global. Subsequent request for the same channel + * name will return the cached instance. + * + * @param string $channel Logging channel + * @return \Psr\Log\LoggerInterface Logger instance + */ + public function getLogger( $channel ) { + if ( !isset( $this->singletons['loggers'][$channel] ) ) { + // Fallback to using the '@default' configuration if an explict + // configuration for the requested channel isn't found. + $spec = isset( $this->config['loggers'][$channel] ) ? + $this->config['loggers'][$channel] : + $this->config['loggers']['@default']; + + $monolog = $this->createLogger( $channel, $spec ); + $this->singletons['loggers'][$channel] = $monolog; + } + + return $this->singletons['loggers'][$channel]; + } + + + /** + * Create a logger. + * @param string $channel Logger channel + * @param array $spec Configuration + * @return \Monolog\Logger + */ + protected function createLogger( $channel, $spec ) { + $obj = new Logger( $channel ); + + if ( isset( $spec['processors'] ) ) { + foreach ( $spec['processors'] as $processor ) { + $obj->pushProcessor( $this->getProcessor( $processor ) ); + } + } + + if ( isset( $spec['handlers'] ) ) { + foreach ( $spec['handlers'] as $handler ) { + $obj->pushHandler( $this->getHandler( $handler ) ); + } + } + return $obj; + } + + + /** + * Create or return cached processor. + * @param string $name Processor name + * @return callable + */ + public function getProcessor( $name ) { + if ( !isset( $this->singletons['processors'][$name] ) ) { + $spec = $this->config['processors'][$name]; + $processor = ObjectFactory::getObjectFromSpec( $spec ); + $this->singletons['processors'][$name] = $processor; + } + return $this->singletons['processors'][$name]; + } + + + /** + * Create or return cached handler. + * @param string $name Processor name + * @return \Monolog\Handler\HandlerInterface + */ + public function getHandler( $name ) { + if ( !isset( $this->singletons['handlers'][$name] ) ) { + $spec = $this->config['handlers'][$name]; + $handler = ObjectFactory::getObjectFromSpec( $spec ); + if ( isset( $spec['formatter'] ) ) { + $handler->setFormatter( + $this->getFormatter( $spec['formatter'] ) + ); + } + $this->singletons['handlers'][$name] = $handler; + } + return $this->singletons['handlers'][$name]; + } + + + /** + * Create or return cached formatter. + * @param string $name Formatter name + * @return \Monolog\Formatter\FormatterInterface + */ + public function getFormatter( $name ) { + if ( !isset( $this->singletons['formatters'][$name] ) ) { + $spec = $this->config['formatters'][$name]; + $formatter = ObjectFactory::getObjectFromSpec( $spec ); + $this->singletons['formatters'][$name] = $formatter; + } + return $this->singletons['formatters'][$name]; + } +} diff --git a/includes/debug/logger/NullSpi.php b/includes/debug/logger/NullSpi.php new file mode 100644 index 00000000..a82d2c4c --- /dev/null +++ b/includes/debug/logger/NullSpi.php @@ -0,0 +1,64 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +use Psr\Log\NullLogger; + +/** + * LoggerFactory service provider that creates \Psr\Log\NullLogger + * instances. A NullLogger silently discards all log events sent to it. + * + * Usage: + * @code + * $wgMWLoggerDefaultSpi = array( + * 'class' => '\\MediaWiki\\Logger\\NullSpi', + * ); + * @endcode + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class NullSpi implements Spi { + + /** + * @var \Psr\Log\NullLogger $singleton + */ + protected $singleton; + + + public function __construct() { + $this->singleton = new NullLogger(); + } + + + /** + * Get a logger instance. + * + * @param string $channel Logging channel + * @return \Psr\Log\NullLogger Logger instance + */ + public function getLogger( $channel ) { + return $this->singleton; + } + +} diff --git a/includes/debug/logger/Spi.php b/includes/debug/logger/Spi.php new file mode 100644 index 00000000..044789f2 --- /dev/null +++ b/includes/debug/logger/Spi.php @@ -0,0 +1,47 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger; + +/** + * Service provider interface for \Psr\Log\LoggerInterface implementation + * libraries. + * + * MediaWiki can be configured to use a class implementing this interface to + * create new \Psr\Log\LoggerInterface instances via either the + * $wgMWLoggerDefaultSpi global variable or code that constructs an instance + * and registers it via the LoggerFactory::registerProvider() static method. + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +interface Spi { + + /** + * Get a logger instance. + * + * @param string $channel Logging channel + * @return \Psr\Log\LoggerInterface Logger instance + */ + public function getLogger( $channel ); + +} diff --git a/includes/debug/logger/monolog/LegacyFormatter.php b/includes/debug/logger/monolog/LegacyFormatter.php new file mode 100644 index 00000000..9ec15cb8 --- /dev/null +++ b/includes/debug/logger/monolog/LegacyFormatter.php @@ -0,0 +1,48 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger\Monolog; + +use MediaWiki\Logger\LegacyLogger; +use Monolog\Formatter\NormalizerFormatter; + +/** + * Log message formatter that mimics the legacy log message formatting of + * `wfDebug`, `wfDebugLog`, `wfLogDBError` and `wfErrorLog` global functions by + * delegating the formatting to \MediaWiki\Logger\LegacyLogger. + * + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2013 Bryan Davis and Wikimedia Foundation. + * @see \MediaWiki\Logger\LegacyLogger + */ +class LegacyFormatter extends NormalizerFormatter { + + public function __construct() { + parent::__construct( 'c' ); + } + + public function format( array $record ) { + $normalized = parent::format( $record ); + return LegacyLogger::format( + $normalized['channel'], $normalized['message'], $normalized + ); + } +} diff --git a/includes/debug/logger/monolog/LegacyHandler.php b/includes/debug/logger/monolog/LegacyHandler.php new file mode 100644 index 00000000..8405819d --- /dev/null +++ b/includes/debug/logger/monolog/LegacyHandler.php @@ -0,0 +1,243 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger\Monolog; + +use LogicException; +use MediaWiki\Logger\LegacyLogger; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use UnexpectedValueException; + +/** + * Log handler that replicates the behavior of MediaWiki's wfErrorLog() + * logging service. Log output can be directed to a local file, a PHP stream, + * or a udp2log server. + * + * For udp2log output, the stream specification must have the form: + * "udp://HOST:PORT[/PREFIX]" + * where: + * - HOST: IPv4, IPv6 or hostname + * - PORT: server port + * - PREFIX: optional (but recommended) prefix telling udp2log how to route + * the log event. The special prefix "{channel}" will use the log event's + * channel as the prefix value. + * + * When not targeting a udp2log stream this class will act as a drop-in + * replacement for Monolog's StreamHandler. + * + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2013 Bryan Davis and Wikimedia Foundation. + */ +class LegacyHandler extends AbstractProcessingHandler { + + /** + * Log sink descriptor + * @var string $uri + */ + protected $uri; + + /** + * Filter log events using legacy rules + * @var bool $useLegacyFilter + */ + protected $useLegacyFilter; + + /** + * Log sink + * @var resource $sink + */ + protected $sink; + + /** + * @var string $error + */ + protected $error; + + /** + * @var string $host + */ + protected $host; + + /** + * @var int $port + */ + protected $port; + + /** + * @var string $prefix + */ + protected $prefix; + + + /** + * @param string $stream Stream URI + * @param bool $useLegacyFilter Filter log events using legacy rules + * @param int $level Minimum logging level that will trigger handler + * @param bool $bubble Can handled meesages bubble up the handler stack? + */ + public function __construct( + $stream, + $useLegacyFilter = false, + $level = Logger::DEBUG, + $bubble = true + ) { + parent::__construct( $level, $bubble ); + $this->uri = $stream; + $this->useLegacyFilter = $useLegacyFilter; + } + + /** + * Open the log sink described by our stream URI. + */ + protected function openSink() { + if ( !$this->uri ) { + throw new LogicException( + 'Missing stream uri, the stream can not be opened.' ); + } + $this->error = null; + set_error_handler( array( $this, 'errorTrap' ) ); + + if ( substr( $this->uri, 0, 4 ) == 'udp:' ) { + $parsed = parse_url( $this->uri ); + if ( !isset( $parsed['host'] ) ) { + throw new UnexpectedValueException( sprintf( + 'Udp transport "%s" must specify a host', $this->uri + ) ); + } + if ( !isset( $parsed['port'] ) ) { + throw new UnexpectedValueException( sprintf( + 'Udp transport "%s" must specify a port', $this->uri + ) ); + } + + $this->host = $parsed['host']; + $this->port = $parsed['port']; + $this->prefix = ''; + + if ( isset( $parsed['path'] ) ) { + $this->prefix = ltrim( $parsed['path'], '/' ); + } + + if ( filter_var( $this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { + $domain = AF_INET6; + + } else { + $domain = AF_INET; + } + + $this->sink = socket_create( $domain, SOCK_DGRAM, SOL_UDP ); + + } else { + $this->sink = fopen( $this->uri, 'a' ); + } + restore_error_handler(); + + if ( !is_resource( $this->sink ) ) { + $this->sink = null; + throw new UnexpectedValueException( sprintf( + 'The stream or file "%s" could not be opened: %s', + $this->uri, $this->error + ) ); + } + } + + + /** + * Custom error handler. + * @param int $code Error number + * @param string $msg Error message + */ + protected function errorTrap( $code, $msg ) { + $this->error = $msg; + } + + + /** + * Should we use UDP to send messages to the sink? + * @return bool + */ + protected function useUdp() { + return $this->host !== null; + } + + + protected function write( array $record ) { + if ( $this->useLegacyFilter && + !LegacyLogger::shouldEmit( + $record['channel'], $record['message'], + $record['level'], $record + ) ) { + // Do not write record if we are enforcing legacy rules and they + // do not pass this message. This used to be done in isHandling(), + // but Monolog 1.12.0 made a breaking change that removed access + // to the needed channel and context information. + return; + } + + if ( $this->sink === null ) { + $this->openSink(); + } + + $text = (string)$record['formatted']; + if ( $this->useUdp() ) { + + // Clean it up for the multiplexer + if ( $this->prefix !== '' ) { + $leader = ( $this->prefix === '{channel}' ) ? + $record['channel'] : $this->prefix; + $text = preg_replace( '/^/m', "{$leader} ", $text ); + + // Limit to 64KB + if ( strlen( $text ) > 65506 ) { + $text = substr( $text, 0, 65506 ); + } + + if ( substr( $text, -1 ) != "\n" ) { + $text .= "\n"; + } + + } elseif ( strlen( $text ) > 65507 ) { + $text = substr( $text, 0, 65507 ); + } + + socket_sendto( + $this->sink, $text, strlen( $text ), 0, $this->host, $this->port + ); + + } else { + fwrite( $this->sink, $text ); + } + } + + + public function close() { + if ( is_resource( $this->sink ) ) { + if ( $this->useUdp() ) { + socket_close( $this->sink ); + + } else { + fclose( $this->sink ); + } + } + $this->sink = null; + } +} diff --git a/includes/debug/logger/monolog/SyslogHandler.php b/includes/debug/logger/monolog/SyslogHandler.php new file mode 100644 index 00000000..008efbc1 --- /dev/null +++ b/includes/debug/logger/monolog/SyslogHandler.php @@ -0,0 +1,96 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger\Monolog; + +use Monolog\Handler\SyslogUdpHandler; +use Monolog\Logger; + +/** + * Log handler that supports sending log events to a syslog server using RFC + * 3164 formatted UDP packets. + * + * Monolog's SyslogUdpHandler creates a partial RFC 5424 header (PRI and + * VERSION) and relies on the associated formatter to complete the header and + * message payload. This makes using it with a fixed format formatter like + * Monolog\Formatter\LogstashFormatter impossible. Additionally, the direct + * syslog input for Logstash only handles RFC 3164 syslog packets. + * + * This Handler should work with any Formatter. The formatted message will be + * prepended with an RFC 3164 message header and a partial message body. The + * resulting packet will looks something like: + * + * <PRI>DATETIME HOSTNAME PROGRAM: MESSAGE + * + * This format works as input to rsyslog and can also be processed by the + * default Logstash syslog input handler. + * + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2015 Bryan Davis and Wikimedia Foundation. + */ +class SyslogHandler extends SyslogUdpHandler { + + /** + * @var string $appname + */ + private $appname; + + /** + * @var string $hostname + */ + private $hostname; + + + /** + * @param string $appname Application name to report to syslog + * @param string $host Syslog host + * @param int $port Syslog port + * @param int $facility Syslog message facility + * @param string $level The minimum logging level at which this handler + * will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up + * the stack or not + */ + public function __construct( + $appname, + $host, + $port = 514, + $facility = LOG_USER, + $level = Logger::DEBUG, + $bubble = true + ) { + parent::__construct( $host, $port, $facility, $level, $bubble ); + $this->appname = $appname; + $this->hostname = php_uname( 'n' ); + } + + protected function makeCommonSyslogHeader( $severity ) { + $pri = $severity + $this->facility; + + // Goofy date format courtesy of RFC 3164 :( + // RFC 3164 actually specifies that the day of month should be space + // padded rather than unpadded but this seems to work with rsyslog and + // Logstash. + $timestamp = date( 'M j H:i:s' ); + + return "<{$pri}>{$timestamp} {$this->hostname} {$this->appname}: "; + } +} diff --git a/includes/debug/logger/monolog/WikiProcessor.php b/includes/debug/logger/monolog/WikiProcessor.php new file mode 100644 index 00000000..a52f6366 --- /dev/null +++ b/includes/debug/logger/monolog/WikiProcessor.php @@ -0,0 +1,47 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\Logger\Monolog; + +/** + * Injects `wfHostname()` and `wfWikiID()` in all records. + * + * @since 1.25 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2013 Bryan Davis and Wikimedia Foundation. + */ +class WikiProcessor { + + /** + * @param array $record + * @return array + */ + public function __invoke( array $record ) { + $record['extra'] = array_merge( + $record['extra'], + array( + 'host' => wfHostname(), + 'wiki' => wfWikiID(), + ) + ); + return $record; + } + +} |