diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/debug |
first commit
Diffstat (limited to 'www/wiki/includes/debug')
18 files changed, 2812 insertions, 0 deletions
diff --git a/www/wiki/includes/debug/MWDebug.php b/www/wiki/includes/debug/MWDebug.php new file mode 100644 index 00000000..74798414 --- /dev/null +++ b/www/wiki/includes/debug/MWDebug.php @@ -0,0 +1,558 @@ +<?php +/** + * Debug toolbar related code. + * + * 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 + */ + +use MediaWiki\Logger\LegacyLogger; + +/** + * New debugger system that outputs a toolbar on page view. + * + * By default, most methods do nothing ( self::$enabled = false ). You have + * to explicitly call MWDebug::init() to enabled them. + * + * @since 1.19 + */ +class MWDebug { + /** + * Log lines + * + * @var array $log + */ + protected static $log = []; + + /** + * Debug messages from wfDebug(). + * + * @var array $debug + */ + protected static $debug = []; + + /** + * SQL statements of the database queries. + * + * @var array $query + */ + protected static $query = []; + + /** + * Is the debugger enabled? + * + * @var bool $enabled + */ + protected static $enabled = false; + + /** + * Array of functions that have already been warned, formatted + * function-caller to prevent a buttload of warnings + * + * @var array $deprecationWarnings + */ + protected static $deprecationWarnings = []; + + /** + * Enabled the debugger and load resource module. + * This is called by Setup.php when $wgDebugToolbar is true. + * + * @since 1.19 + */ + public static function init() { + self::$enabled = true; + } + + /** + * Disable the debugger. + * + * @since 1.28 + */ + public static function deinit() { + self::$enabled = false; + } + + /** + * Add ResourceLoader modules to the OutputPage object if debugging is + * enabled. + * + * @since 1.19 + * @param OutputPage $out + */ + public static function addModules( OutputPage $out ) { + if ( self::$enabled ) { + $out->addModules( 'mediawiki.debug' ); + } + } + + /** + * Adds a line to the log + * + * @since 1.19 + * @param mixed $str + */ + public static function log( $str ) { + if ( !self::$enabled ) { + return; + } + if ( !is_string( $str ) ) { + $str = print_r( $str, true ); + } + self::$log[] = [ + 'msg' => htmlspecialchars( $str ), + 'type' => 'log', + 'caller' => wfGetCaller(), + ]; + } + + /** + * Returns internal log array + * @since 1.19 + * @return array + */ + public static function getLog() { + return self::$log; + } + + /** + * Clears internal log array and deprecation tracking + * @since 1.19 + */ + public static function clearLog() { + self::$log = []; + self::$deprecationWarnings = []; + } + + /** + * Adds a warning entry to the log + * + * @since 1.19 + * @param string $msg + * @param int $callerOffset + * @param int $level A PHP error level. See sendMessage() + * @param string $log 'production' will always trigger a php error, 'auto' + * will trigger an error if $wgDevelopmentWarnings is true, and 'debug' + * will only write to the debug log(s). + */ + public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) { + global $wgDevelopmentWarnings; + + if ( $log === 'auto' && !$wgDevelopmentWarnings ) { + $log = 'debug'; + } + + if ( $log === 'debug' ) { + $level = false; + } + + $callerDescription = self::getCallerDescription( $callerOffset ); + + self::sendMessage( $msg, $callerDescription, 'warning', $level ); + + if ( self::$enabled ) { + self::$log[] = [ + 'msg' => htmlspecialchars( $msg ), + 'type' => 'warn', + 'caller' => $callerDescription['func'], + ]; + } + } + + /** + * Show a warning that $function is deprecated. + * This will send it to the following locations: + * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar + * is set to true. + * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings + * is set to true. + * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false. + * + * @since 1.19 + * @param string $function Function that is deprecated. + * @param string|bool $version Version in which the function was deprecated. + * @param string|bool $component Component to which the function belongs. + * If false, it is assumbed the function is in MediaWiki core. + * @param int $callerOffset How far up the callstack is the original + * caller. 2 = function that called the function that called + * MWDebug::deprecated() (Added in 1.20). + */ + public static function deprecated( $function, $version = false, + $component = false, $callerOffset = 2 + ) { + $callerDescription = self::getCallerDescription( $callerOffset ); + $callerFunc = $callerDescription['func']; + + $sendToLog = true; + + // Check to see if there already was a warning about this function + if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) { + return; + } elseif ( isset( self::$deprecationWarnings[$function] ) ) { + if ( self::$enabled ) { + $sendToLog = false; + } else { + return; + } + } + + self::$deprecationWarnings[$function][$callerFunc] = true; + + if ( $version ) { + global $wgDeprecationReleaseLimit; + if ( $wgDeprecationReleaseLimit && $component === false ) { + # Strip -* off the end of $version so that branches can use the + # format #.##-branchname to avoid issues if the branch is merged into + # a version of MediaWiki later than what it was branched from + $comparableVersion = preg_replace( '/-.*$/', '', $version ); + + # If the comparableVersion is larger than our release limit then + # skip the warning message for the deprecation + if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) { + $sendToLog = false; + } + } + + $component = $component === false ? 'MediaWiki' : $component; + $msg = "Use of $function was deprecated in $component $version."; + } else { + $msg = "Use of $function is deprecated."; + } + + if ( $sendToLog ) { + global $wgDevelopmentWarnings; // we could have a more specific $wgDeprecationWarnings setting. + self::sendMessage( + $msg, + $callerDescription, + 'deprecated', + $wgDevelopmentWarnings ? E_USER_DEPRECATED : false + ); + } + + if ( self::$enabled ) { + $logMsg = htmlspecialchars( $msg ) . + Html::rawElement( 'div', [ 'class' => 'mw-debug-backtrace' ], + Html::element( 'span', [], 'Backtrace:' ) . wfBacktrace() + ); + + self::$log[] = [ + 'msg' => $logMsg, + 'type' => 'deprecated', + 'caller' => $callerFunc, + ]; + } + } + + /** + * Get an array describing the calling function at a specified offset. + * + * @param int $callerOffset How far up the callstack is the original + * caller. 0 = function that called getCallerDescription() + * @return array Array with two keys: 'file' and 'func' + */ + private static function getCallerDescription( $callerOffset ) { + $callers = wfDebugBacktrace(); + + if ( isset( $callers[$callerOffset] ) ) { + $callerfile = $callers[$callerOffset]; + if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { + $file = $callerfile['file'] . ' at line ' . $callerfile['line']; + } else { + $file = '(internal function)'; + } + } else { + $file = '(unknown location)'; + } + + if ( isset( $callers[$callerOffset + 1] ) ) { + $callerfunc = $callers[$callerOffset + 1]; + $func = ''; + if ( isset( $callerfunc['class'] ) ) { + $func .= $callerfunc['class'] . '::'; + } + if ( isset( $callerfunc['function'] ) ) { + $func .= $callerfunc['function']; + } + } else { + $func = 'unknown'; + } + + return [ 'file' => $file, 'func' => $func ]; + } + + /** + * Send a message to the debug log and optionally also trigger a PHP + * error, depending on the $level argument. + * + * @param string $msg Message to send + * @param array $caller Caller description get from getCallerDescription() + * @param string $group Log group on which to send the message + * @param int|bool $level Error level to use; set to false to not trigger an error + */ + private static function sendMessage( $msg, $caller, $group, $level ) { + $msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']'; + + if ( $level !== false ) { + trigger_error( $msg, $level ); + } + + wfDebugLog( $group, $msg, 'private' ); + } + + /** + * This is a method to pass messages from wfDebug to the pretty debugger. + * Do NOT use this method, use MWDebug::log or wfDebug() + * + * @since 1.19 + * @param string $str + * @param array $context + */ + public static function debugMsg( $str, $context = [] ) { + global $wgDebugComments, $wgShowDebug; + + if ( self::$enabled || $wgDebugComments || $wgShowDebug ) { + 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 = LegacyLogger::interpolate( $str, $context ); + $str = $prefix . $str; + } + self::$debug[] = rtrim( UtfNormal\Validator::cleanUp( $str ) ); + } + } + + /** + * Begins profiling on a database query + * + * @since 1.19 + * @param string $sql + * @param string $function + * @param bool $isMaster + * @param float $runTime Query run time + * @return int ID number of the query to pass to queryTime or -1 if the + * debugger is disabled + */ + public static function query( $sql, $function, $isMaster, $runTime ) { + if ( !self::$enabled ) { + return -1; + } + + // Replace invalid UTF-8 chars with a square UTF-8 character + // This prevents json_encode from erroring out due to binary SQL data + $sql = preg_replace( + '/( + [\xC0-\xC1] # Invalid UTF-8 Bytes + | [\xF5-\xFF] # Invalid UTF-8 Bytes + | \xE0[\x80-\x9F] # Overlong encoding of prior code point + | \xF0[\x80-\x8F] # Overlong encoding of prior code point + | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start + | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start + | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start + | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle + | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4] + | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence + | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence + | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence + | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2) + )/x', + '■', + $sql + ); + + // last check for invalid utf8 + $sql = UtfNormal\Validator::cleanUp( $sql ); + + self::$query[] = [ + 'sql' => $sql, + 'function' => $function, + 'master' => (bool)$isMaster, + 'time' => $runTime, + ]; + + return count( self::$query ) - 1; + } + + /** + * Returns a list of files included, along with their size + * + * @param IContextSource $context + * @return array + */ + protected static function getFilesIncluded( IContextSource $context ) { + $files = get_included_files(); + $fileList = []; + foreach ( $files as $file ) { + $size = filesize( $file ); + $fileList[] = [ + 'name' => $file, + 'size' => $context->getLanguage()->formatSize( $size ), + ]; + } + + return $fileList; + } + + /** + * Returns the HTML to add to the page for the toolbar + * + * @since 1.19 + * @param IContextSource $context + * @return string + */ + public static function getDebugHTML( IContextSource $context ) { + global $wgDebugComments; + + $html = ''; + + if ( self::$enabled ) { + self::log( 'MWDebug output complete' ); + $debugInfo = self::getDebugInfo( $context ); + + // Cannot use OutputPage::addJsConfigVars because those are already outputted + // by the time this method is called. + $html = ResourceLoader::makeInlineScript( + ResourceLoader::makeConfigSetScript( [ 'debugInfo' => $debugInfo ] ) + ); + } + + if ( $wgDebugComments ) { + $html .= "<!-- Debug output:\n" . + htmlspecialchars( implode( "\n", self::$debug ), ENT_NOQUOTES ) . + "\n\n-->"; + } + + return $html; + } + + /** + * Generate debug log in HTML for displaying at the bottom of the main + * content area. + * If $wgShowDebug is false, an empty string is always returned. + * + * @since 1.20 + * @return string HTML fragment + */ + public static function getHTMLDebugLog() { + global $wgShowDebug; + + if ( !$wgShowDebug ) { + return ''; + } + + $ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n"; + + foreach ( self::$debug as $line ) { + $display = nl2br( htmlspecialchars( trim( $line ) ) ); + + $ret .= "<li><code>$display</code></li>\n"; + } + + $ret .= '</ul>' . "\n"; + + return $ret; + } + + /** + * Append the debug info to given ApiResult + * + * @param IContextSource $context + * @param ApiResult $result + */ + public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) { + if ( !self::$enabled ) { + return; + } + + // output errors as debug info, when display_errors is on + // this is necessary for all non html output of the api, because that clears all errors first + $obContents = ob_get_contents(); + if ( $obContents ) { + $obContentArray = explode( '<br />', $obContents ); + foreach ( $obContentArray as $obContent ) { + if ( trim( $obContent ) ) { + self::debugMsg( Sanitizer::stripAllTags( $obContent ) ); + } + } + } + + self::log( 'MWDebug output complete' ); + $debugInfo = self::getDebugInfo( $context ); + + 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 ); + } + + /** + * Returns the HTML to add to the page for the toolbar + * + * @param IContextSource $context + * @return array + */ + public static function getDebugInfo( IContextSource $context ) { + if ( !self::$enabled ) { + return []; + } + + global $wgVersion; + $request = $context->getRequest(); + + // HHVM's reported memory usage from memory_get_peak_usage() + // is not useful when passing false, but we continue passing + // false for consistency of historical data in zend. + // see: https://github.com/facebook/hhvm/issues/2257#issuecomment-39362246 + $realMemoryUsage = wfIsHHVM(); + + $branch = GitInfo::currentBranch(); + if ( GitInfo::isSHA1( $branch ) ) { + // If it's a detached HEAD, the SHA1 will already be + // included in the MW version, so don't show it. + $branch = false; + } + + return [ + 'mwVersion' => $wgVersion, + 'phpEngine' => wfIsHHVM() ? 'HHVM' : 'PHP', + 'phpVersion' => wfIsHHVM() ? HHVM_VERSION : PHP_VERSION, + 'gitRevision' => GitInfo::headSHA1(), + 'gitBranch' => $branch, + 'gitViewUrl' => GitInfo::headViewUrl(), + 'time' => $request->getElapsedTime(), + 'log' => self::$log, + 'debugLog' => self::$debug, + 'queries' => self::$query, + 'request' => [ + 'method' => $request->getMethod(), + 'url' => $request->getRequestURL(), + 'headers' => $request->getAllHeaders(), + 'params' => $request->getValues(), + ], + 'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ), + 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ), + 'includes' => self::getFilesIncluded( $context ), + ]; + } +} diff --git a/www/wiki/includes/debug/logger/ConsoleLogger.php b/www/wiki/includes/debug/logger/ConsoleLogger.php new file mode 100644 index 00000000..5a5e5071 --- /dev/null +++ b/www/wiki/includes/debug/logger/ConsoleLogger.php @@ -0,0 +1,21 @@ +<?php + +namespace MediaWiki\Logger; + +use Psr\Log\AbstractLogger; + +/** + * A logger which writes to the terminal. The output is supposed to be + * human-readable, and should be changed as necessary to better achieve that + * goal. + */ +class ConsoleLogger extends AbstractLogger { + public function __construct( $channel ) { + $this->channel = $channel; + } + + public function log( $level, $message, array $context = [] ) { + fwrite( STDERR, "[$level] " . + LegacyLogger::format( $this->channel, $message, $context ) ); + } +} diff --git a/www/wiki/includes/debug/logger/ConsoleSpi.php b/www/wiki/includes/debug/logger/ConsoleSpi.php new file mode 100644 index 00000000..e29b98d3 --- /dev/null +++ b/www/wiki/includes/debug/logger/ConsoleSpi.php @@ -0,0 +1,11 @@ +<?php +namespace MediaWiki\Logger; + +class ConsoleSpi implements Spi { + public function __construct( $config = [] ) { + } + + public function getLogger( $channel ) { + return new ConsoleLogger( $channel ); + } +} diff --git a/www/wiki/includes/debug/logger/LegacyLogger.php b/www/wiki/includes/debug/logger/LegacyLogger.php new file mode 100644 index 00000000..7bd505d0 --- /dev/null +++ b/www/wiki/includes/debug/logger/LegacyLogger.php @@ -0,0 +1,482 @@ +<?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 Exception; +use MWDebug; +use MWExceptionHandler; +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 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +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 $levelMapping + */ + protected static $levelMapping = [ + LogLevel::DEBUG => 100, + LogLevel::INFO => 200, + LogLevel::NOTICE => 250, + LogLevel::WARNING => 300, + LogLevel::ERROR => 400, + LogLevel::CRITICAL => 500, + LogLevel::ALERT => 550, + LogLevel::EMERGENCY => 600, + ]; + + /** + * @var array + */ + protected static $dbChannels = [ + 'DBQuery' => true, + 'DBConnection' => true + ]; + + /** + * @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 + * @return null + */ + public function log( $level, $message, array $context = [] ) { + if ( is_string( $level ) ) { + $level = self::$levelMapping[$level]; + } + if ( $this->channel === 'DBQuery' && isset( $context['method'] ) + && isset( $context['master'] ) && isset( $context['runtime'] ) + ) { + MWDebug::query( $message, $context['method'], $context['master'], $context['runtime'] ); + return; // only send profiling data to MWDebug profiling + } + + if ( isset( self::$dbChannels[$this->channel] ) + && $level >= self::$levelMapping[LogLevel::ERROR] + ) { + // Format and write DB errors to the legacy locations + $effectiveChannel = 'wfLogDBError'; + } else { + $effectiveChannel = $this->channel; + } + + if ( self::shouldEmit( $effectiveChannel, $message, $level, $context ) ) { + $text = self::format( $effectiveChannel, $message, $context ); + $destination = self::destination( $effectiveChannel, $message, $context ); + self::emit( $text, $destination ); + } + if ( !isset( $context['private'] ) || !$context['private'] ) { + // Add to debug toolbar if not marked as "private" + MWDebug::debugMsg( $message, [ '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 Monolog 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 ( is_string( $level ) ) { + $level = self::$levelMapping[$level]; + } + + 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 ( $channel === 'wfDebug' ) { + // wfDebug messages are emitted if a catch all logging file has + // been specified. Checked explicitly so that 'private' flagged + // messages are not discarded by unset $wgDebugLogGroups channel + // handling below. + $shouldEmit = $wgDebugLogFile != ''; + + } 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'] ) ) { + $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, $wgLogExceptionBacktrace; + + 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 ); + } + + // Append stacktrace of exception if available + if ( $wgLogExceptionBacktrace && isset( $context['exception'] ) ) { + $e = $context['exception']; + $backtrace = false; + + if ( $e instanceof Exception ) { + $backtrace = MWExceptionHandler::getRedactedTrace( $e ); + + } elseif ( is_array( $e ) && isset( $e['trace'] ) ) { + // Exception has already been unpacked as structured data + $backtrace = $e['trace']; + } + + if ( $backtrace ) { + $text .= MWExceptionHandler::prettyPrintTrace( $backtrace ) . + "\n"; + } + } + + 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 ( !$cachedTimezone ) { + $cachedTimezone = new DateTimeZone( $wgDBerrorLogTZ ); + } + + $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 + * @return string + */ + 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 = []; + foreach ( $context as $key => $val ) { + $replace['{' . $key . '}'] = self::flatten( $val ); + } + $message = strtr( $message, $replace ); + } + return $message; + } + + /** + * Convert a logging context element to a string suitable for + * interpolation. + * + * @param mixed $item + * @return string + */ + protected static function flatten( $item ) { + if ( null === $item ) { + return '[Null]'; + } + + if ( is_bool( $item ) ) { + return $item ? 'true' : 'false'; + } + + if ( is_float( $item ) ) { + if ( is_infinite( $item ) ) { + return ( $item > 0 ? '' : '-' ) . 'INF'; + } + if ( is_nan( $item ) ) { + return 'NaN'; + } + return (string)$item; + } + + if ( is_scalar( $item ) ) { + return (string)$item; + } + + if ( is_array( $item ) ) { + return '[Array(' . count( $item ) . ')]'; + } + + if ( $item instanceof \DateTime ) { + return $item->format( 'c' ); + } + + if ( $item instanceof Exception ) { + return '[Exception ' . get_class( $item ) . '( ' . + $item->getFile() . ':' . $item->getLine() . ') ' . + $item->getMessage() . ']'; + } + + if ( is_object( $item ) ) { + if ( method_exists( $item, '__toString' ) ) { + return (string)$item; + } + + return '[Object ' . get_class( $item ) . ']'; + } + + if ( is_resource( $item ) ) { + return '[Resource ' . get_resource_type( $item ) . ']'; + } + + return '[Unknown ' . gettype( $item ) . ']'; + } + + /** + * 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 + */ + public static function emit( $text, $file ) { + if ( substr( $file, 0, 4 ) == 'udp:' ) { + $transport = UDPTransport::newFromString( $file ); + $transport->emit( $text ); + } else { + \Wikimedia\suppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || + ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) + ) { + file_put_contents( $file, $text, FILE_APPEND ); + } + \Wikimedia\restoreWarnings(); + } + } + +} diff --git a/www/wiki/includes/debug/logger/LegacySpi.php b/www/wiki/includes/debug/logger/LegacySpi.php new file mode 100644 index 00000000..cb0e066c --- /dev/null +++ b/www/wiki/includes/debug/logger/LegacySpi.php @@ -0,0 +1,57 @@ +<?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 = [ + * 'class' => \MediaWiki\Logger\LegacySpi::class, + * ]; + * @endcode + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +class LegacySpi implements Spi { + + /** + * @var array $singletons + */ + protected $singletons = []; + + /** + * 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/www/wiki/includes/debug/logger/LoggerFactory.php b/www/wiki/includes/debug/logger/LoggerFactory.php new file mode 100644 index 00000000..d6931942 --- /dev/null +++ b/www/wiki/includes/debug/logger/LoggerFactory.php @@ -0,0 +1,102 @@ +<?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 Wikimedia\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 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +class LoggerFactory { + + /** + * Service provider. + * @var \MediaWiki\Logger\Spi $spi + */ + private static $spi; + + /** + * Register a service provider to create new \Psr\Log\LoggerInterface + * instances. + * + * @param \MediaWiki\Logger\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 \MediaWiki\Logger\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 ) { + return self::getProvider()->getLogger( $channel ); + } + + /** + * Construction of utility class is not allowed. + */ + private function __construct() { + // no-op + } +} diff --git a/www/wiki/includes/debug/logger/MonologSpi.php b/www/wiki/includes/debug/logger/MonologSpi.php new file mode 100644 index 00000000..ec27ad1c --- /dev/null +++ b/www/wiki/includes/debug/logger/MonologSpi.php @@ -0,0 +1,271 @@ +<?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 MediaWiki\Logger\Monolog\BufferHandler; +use Monolog\Logger; +use Wikimedia\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 = [ + * 'class' => \MediaWiki\Logger\MonologSpi::class, + * 'args' => [ [ + * 'loggers' => [ + * '@default' => [ + * 'processors' => [ 'wiki', 'psr', 'pid', 'uid', 'web' ], + * 'handlers' => [ 'stream' ], + * ], + * 'runJobs' => [ + * 'processors' => [ 'wiki', 'psr', 'pid' ], + * 'handlers' => [ 'stream' ], + * ] + * ], + * 'processors' => [ + * 'wiki' => [ + * 'class' => \MediaWiki\Logger\Monolog\WikiProcessor::class, + * ], + * 'psr' => [ + * 'class' => \Monolog\Processor\PsrLogMessageProcessor::class, + * ], + * 'pid' => [ + * 'class' => \Monolog\Processor\ProcessIdProcessor::class, + * ], + * 'uid' => [ + * 'class' => \Monolog\Processor\UidProcessor::class, + * ], + * 'web' => [ + * 'class' => \Monolog\Processor\WebProcessor::class, + * ], + * ], + * 'handlers' => [ + * 'stream' => [ + * 'class' => \Monolog\Handler\StreamHandler::class, + * 'args' => [ 'path/to/your.log' ], + * 'formatter' => 'line', + * ], + * 'redis' => [ + * 'class' => \Monolog\Handler\RedisHandler::class, + * 'args' => [ function() { + * $redis = new Redis(); + * $redis->connect( '127.0.0.1', 6379 ); + * return $redis; + * }, + * 'logstash' + * ], + * 'formatter' => 'logstash', + * 'buffer' => true, + * ], + * 'udp2log' => [ + * 'class' => \MediaWiki\Logger\Monolog\LegacyHandler::class, + * 'args' => [ + * 'udp://127.0.0.1:8420/mediawiki + * ], + * 'formatter' => 'line', + * ], + * ], + * 'formatters' => [ + * 'line' => [ + * 'class' => \Monolog\Formatter\LineFormatter::class, + * ], + * 'logstash' => [ + * 'class' => \Monolog\Formatter\LogstashFormatter::class, + * 'args' => [ 'mediawiki', php_uname( 'n' ), null, '', 1 ], + * ], + * ], + * ] ], + * ]; + * @endcode + * + * @see https://github.com/Seldaek/monolog + * @since 1.25 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +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 = []; + $this->mergeConfig( $config ); + } + + /** + * Merge additional configuration data into the configuration. + * + * @since 1.26 + * @param array $config Configuration data. + */ + public function mergeConfig( array $config ) { + foreach ( $config as $key => $value ) { + if ( isset( $this->config[$key] ) ) { + $this->config[$key] = array_merge( $this->config[$key], $value ); + } else { + $this->config[$key] = $value; + } + } + $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 = [ + 'loggers' => [], + 'handlers' => [], + 'formatters' => [], + 'processors' => [], + ]; + } + + /** + * 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['calls'] ) ) { + foreach ( $spec['calls'] as $method => $margs ) { + call_user_func_array( [ $obj, $method ], $margs ); + } + } + + 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'] ) + ); + } + if ( isset( $spec['buffer'] ) && $spec['buffer'] ) { + $handler = new BufferHandler( $handler ); + } + $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/www/wiki/includes/debug/logger/NullSpi.php b/www/wiki/includes/debug/logger/NullSpi.php new file mode 100644 index 00000000..d65c1851 --- /dev/null +++ b/www/wiki/includes/debug/logger/NullSpi.php @@ -0,0 +1,60 @@ +<?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: + * + * $wgMWLoggerDefaultSpi = [ + * 'class' => \MediaWiki\Logger\NullSpi::class, + * ]; + * + * @see \MediaWiki\Logger\LoggerFactory + * @since 1.25 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +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/www/wiki/includes/debug/logger/Spi.php b/www/wiki/includes/debug/logger/Spi.php new file mode 100644 index 00000000..8e0875f2 --- /dev/null +++ b/www/wiki/includes/debug/logger/Spi.php @@ -0,0 +1,46 @@ +<?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 + * @copyright © 2014 Wikimedia Foundation and contributors + */ +interface Spi { + + /** + * Get a logger instance. + * + * @param string $channel Logging channel + * @return \Psr\Log\LoggerInterface Logger instance + */ + public function getLogger( $channel ); + +} diff --git a/www/wiki/includes/debug/logger/monolog/AvroFormatter.php b/www/wiki/includes/debug/logger/monolog/AvroFormatter.php new file mode 100644 index 00000000..a395e0d0 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/AvroFormatter.php @@ -0,0 +1,171 @@ +<?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 AvroIODatumWriter; +use AvroIOBinaryEncoder; +use AvroIOTypeException; +use AvroSchema; +use AvroStringIO; +use AvroValidator; +use Monolog\Formatter\FormatterInterface; + +/** + * Log message formatter that uses the apache Avro format. + * + * @since 1.26 + * @author Erik Bernhardson <ebernhardson@wikimedia.org> + * @copyright © 2015 Erik Bernhardson and Wikimedia Foundation. + */ +class AvroFormatter implements FormatterInterface { + /** + * @var Magic byte to encode schema revision id. + */ + const MAGIC = 0x0; + /** + * @var array Map from schema name to schema definition + */ + protected $schemas; + + /** + * @var AvroStringIO + */ + protected $io; + + /** + * @var AvroIOBinaryEncoder + */ + protected $encoder; + + /** + * @var AvroIODatumWriter + */ + protected $writer; + + /** + * @param array $schemas Map from Monolog channel to Avro schema. + * Each schema can be either the JSON string or decoded into PHP + * arrays. + */ + public function __construct( array $schemas ) { + $this->schemas = $schemas; + $this->io = new AvroStringIO( '' ); + $this->encoder = new AvroIOBinaryEncoder( $this->io ); + $this->writer = new AvroIODatumWriter(); + } + + /** + * Formats the record context into a binary string per the + * schema configured for the records channel. + * + * @param array $record + * @return string|null The serialized record, or null if + * the record is not valid for the selected schema. + */ + public function format( array $record ) { + $this->io->truncate(); + $schema = $this->getSchema( $record['channel'] ); + $revId = $this->getSchemaRevisionId( $record['channel'] ); + if ( $schema === null || $revId === null ) { + trigger_error( "The schema for channel '{$record['channel']}' is not available" ); + return null; + } + try { + $this->writer->write_data( $schema, $record['context'], $this->encoder ); + } catch ( AvroIOTypeException $e ) { + $errors = AvroValidator::getErrors( $schema, $record['context'] ); + $json = json_encode( $errors ); + trigger_error( "Avro failed to serialize record for {$record['channel']} : {$json}" ); + return null; + } + return chr( self::MAGIC ) . $this->encodeLong( $revId ) . $this->io->string(); + } + + /** + * Format a set of records into a list of binary strings + * conforming to the configured schema. + * + * @param array $records + * @return string[] + */ + public function formatBatch( array $records ) { + $result = []; + foreach ( $records as $record ) { + $message = $this->format( $record ); + if ( $message !== null ) { + $result[] = $message; + } + } + return $result; + } + + /** + * Get the writer for the named channel + * + * @param string $channel Name of the schema to fetch + * @return \AvroSchema|null + */ + protected function getSchema( $channel ) { + if ( !isset( $this->schemas[$channel] ) ) { + return null; + } + if ( !isset( $this->schemas[$channel]['revision'], $this->schemas[$channel]['schema'] ) ) { + return null; + } + + if ( !$this->schemas[$channel]['schema'] instanceof AvroSchema ) { + $schema = $this->schemas[$channel]['schema']; + if ( is_string( $schema ) ) { + $this->schemas[$channel]['schema'] = AvroSchema::parse( $schema ); + } else { + $this->schemas[$channel]['schema'] = AvroSchema::real_parse( + $schema + ); + } + } + return $this->schemas[$channel]['schema']; + } + + /** + * Get the writer for the named channel + * + * @param string $channel Name of the schema + * @return int|null + */ + public function getSchemaRevisionId( $channel ) { + if ( isset( $this->schemas[$channel]['revision'] ) ) { + return (int)$this->schemas[$channel]['revision']; + } + return null; + } + + /** + * convert an integer to a 64bits big endian long (Java compatible) + * NOTE: certainly only compatible with PHP 64bits + * @param int $id + * @return string the binary representation of $id + */ + private function encodeLong( $id ) { + $high = ( $id & 0xffffffff00000000 ) >> 32; + $low = $id & 0x00000000ffffffff; + return pack( 'NN', $high, $low ); + } +} diff --git a/www/wiki/includes/debug/logger/monolog/BufferHandler.php b/www/wiki/includes/debug/logger/monolog/BufferHandler.php new file mode 100644 index 00000000..650d0127 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/BufferHandler.php @@ -0,0 +1,46 @@ +<?php +/** + * Helper class for the index.php entry point. + * + * 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 DeferredUpdates; +use Monolog\Handler\BufferHandler as BaseBufferHandler; + +/** + * Updates \Monolog\Handler\BufferHandler to use DeferredUpdates rather + * than register_shutdown_function. On supported platforms this will + * use register_postsend_function or fastcgi_finish_request() to delay + * until after the request has shutdown and we are no longer delaying + * the web request. + */ +class BufferHandler extends BaseBufferHandler { + /** + * @inheritDoc + */ + public function handle( array $record ) { + if ( !$this->initialized ) { + DeferredUpdates::addCallableUpdate( [ $this, 'close' ] ); + $this->initialized = true; + } + return parent::handle( $record ); + } +} diff --git a/www/wiki/includes/debug/logger/monolog/KafkaHandler.php b/www/wiki/includes/debug/logger/monolog/KafkaHandler.php new file mode 100644 index 00000000..8e711316 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/KafkaHandler.php @@ -0,0 +1,279 @@ +<?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 Kafka\MetaDataFromKafka; +use Kafka\Produce; +use Kafka\Protocol\Decoder; +use MediaWiki\Logger\LoggerFactory; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Psr\Log\LoggerInterface; + +/** + * Log handler sends log events to a kafka server. + * + * Constructor options array arguments: + * * alias: map from monolog channel to kafka topic name. When no + * alias exists the topic "monolog_$channel" will be used. + * * swallowExceptions: Swallow exceptions that occur while talking to + * kafka. Defaults to false. + * * logExceptions: Log exceptions talking to kafka here. Either null, + * the name of a channel to log to, or an object implementing + * FormatterInterface. Defaults to null. + * + * Requires the nmred/kafka-php library, version >= 1.3.0 + * + * @since 1.26 + * @author Erik Bernhardson <ebernhardson@wikimedia.org> + * @copyright © 2015 Erik Bernhardson and Wikimedia Foundation. + */ +class KafkaHandler extends AbstractProcessingHandler { + /** + * @var Produce Sends requests to kafka + */ + protected $produce; + + /** + * @var array Optional handler configuration + */ + protected $options; + + /** + * @var array Map from topic name to partition this request produces to + */ + protected $partitions = []; + + /** + * @var array defaults for constructor options + */ + private static $defaultOptions = [ + 'alias' => [], // map from monolog channel to kafka topic + 'swallowExceptions' => false, // swallow exceptions sending records + 'logExceptions' => null, // A PSR3 logger to inform about errors + 'requireAck' => 0, + ]; + + /** + * @param Produce $produce Kafka instance to produce through + * @param array $options optional handler configuration + * @param int $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( + Produce $produce, array $options, $level = Logger::DEBUG, $bubble = true + ) { + parent::__construct( $level, $bubble ); + $this->produce = $produce; + $this->options = array_merge( self::$defaultOptions, $options ); + } + + /** + * Constructs the necessary support objects and returns a KafkaHandler + * instance. + * + * @param string[] $kafkaServers + * @param array $options + * @param int $level The minimum logging level at which this handle will be triggered + * @param bool $bubble Whether the messages that are handled can bubble the stack or not + * @return KafkaHandler + */ + public static function factory( + $kafkaServers, array $options = [], $level = Logger::DEBUG, $bubble = true + ) { + $metadata = new MetaDataFromKafka( $kafkaServers ); + $produce = new Produce( $metadata ); + + if ( isset( $options['sendTimeout'] ) ) { + $timeOut = $options['sendTimeout']; + $produce->getClient()->setStreamOption( 'SendTimeoutSec', 0 ); + $produce->getClient()->setStreamOption( 'SendTimeoutUSec', + intval( $timeOut * 1000000 ) + ); + } + if ( isset( $options['recvTimeout'] ) ) { + $timeOut = $options['recvTimeout']; + $produce->getClient()->setStreamOption( 'RecvTimeoutSec', 0 ); + $produce->getClient()->setStreamOption( 'RecvTimeoutUSec', + intval( $timeOut * 1000000 ) + ); + } + if ( isset( $options['logExceptions'] ) && is_string( $options['logExceptions'] ) ) { + $options['logExceptions'] = LoggerFactory::getInstance( $options['logExceptions'] ); + } + + if ( isset( $options['requireAck'] ) ) { + $produce->setRequireAck( $options['requireAck'] ); + } + + return new self( $produce, $options, $level, $bubble ); + } + + /** + * @inheritDoc + */ + protected function write( array $record ) { + if ( $record['formatted'] !== null ) { + $this->addMessages( $record['channel'], [ $record['formatted'] ] ); + $this->send(); + } + } + + /** + * @inheritDoc + */ + public function handleBatch( array $batch ) { + $channels = []; + foreach ( $batch as $record ) { + if ( $record['level'] < $this->level ) { + continue; + } + $channels[$record['channel']][] = $this->processRecord( $record ); + } + + $formatter = $this->getFormatter(); + foreach ( $channels as $channel => $records ) { + $messages = []; + foreach ( $records as $idx => $record ) { + $message = $formatter->format( $record ); + if ( $message !== null ) { + $messages[] = $message; + } + } + if ( $messages ) { + $this->addMessages( $channel, $messages ); + } + } + + $this->send(); + } + + /** + * Send any records in the kafka client internal queue. + */ + protected function send() { + try { + $response = $this->produce->send(); + } catch ( \Kafka\Exception $e ) { + $ignore = $this->warning( + 'Error sending records to kafka: {exception}', + [ 'exception' => $e ] ); + if ( !$ignore ) { + throw $e; + } else { + return; + } + } + + if ( is_bool( $response ) ) { + return; + } + + $errors = []; + foreach ( $response as $topicName => $partitionResponse ) { + foreach ( $partitionResponse as $partition => $info ) { + if ( $info['errCode'] === 0 ) { + // no error + continue; + } + $errors[] = sprintf( + 'Error producing to %s (errno %d): %s', + $topicName, + $info['errCode'], + Decoder::getError( $info['errCode'] ) + ); + } + } + + if ( $errors ) { + $error = implode( "\n", $errors ); + if ( !$this->warning( $error ) ) { + throw new \RuntimeException( $error ); + } + } + } + + /** + * @param string $topic Name of topic to get partition for + * @return int|null The random partition to produce to for this request, + * or null if a partition could not be determined. + */ + protected function getRandomPartition( $topic ) { + if ( !array_key_exists( $topic, $this->partitions ) ) { + try { + $partitions = $this->produce->getAvailablePartitions( $topic ); + } catch ( \Kafka\Exception $e ) { + $ignore = $this->warning( + 'Error getting metadata for kafka topic {topic}: {exception}', + [ 'topic' => $topic, 'exception' => $e ] ); + if ( $ignore ) { + return null; + } + throw $e; + } + if ( $partitions ) { + $key = array_rand( $partitions ); + $this->partitions[$topic] = $partitions[$key]; + } else { + $details = $this->produce->getClient()->getTopicDetail( $topic ); + $ignore = $this->warning( + 'No partitions available for kafka topic {topic}', + [ 'topic' => $topic, 'kafka' => $details ] + ); + if ( !$ignore ) { + throw new \RuntimeException( "No partitions available for kafka topic $topic" ); + } + $this->partitions[$topic] = null; + } + } + return $this->partitions[$topic]; + } + + /** + * Adds records for a channel to the Kafka client internal queue. + * + * @param string $channel Name of Monolog channel records belong to + * @param array $records List of records to append + */ + protected function addMessages( $channel, array $records ) { + if ( isset( $this->options['alias'][$channel] ) ) { + $topic = $this->options['alias'][$channel]; + } else { + $topic = "monolog_$channel"; + } + $partition = $this->getRandomPartition( $topic ); + if ( $partition !== null ) { + $this->produce->setMessages( $topic, $partition, $records ); + } + } + + /** + * @param string $message PSR3 compatible message string + * @param array $context PSR3 compatible log context + * @return bool true if caller should ignore warning + */ + protected function warning( $message, array $context = [] ) { + if ( $this->options['logExceptions'] instanceof LoggerInterface ) { + $this->options['logExceptions']->warning( $message, $context ); + } + return $this->options['swallowExceptions']; + } +} diff --git a/www/wiki/includes/debug/logger/monolog/LegacyFormatter.php b/www/wiki/includes/debug/logger/monolog/LegacyFormatter.php new file mode 100644 index 00000000..92624a0b --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/LegacyFormatter.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; + +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 + * @copyright © 2013 Wikimedia Foundation and contributors + * @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/www/wiki/includes/debug/logger/monolog/LegacyHandler.php b/www/wiki/includes/debug/logger/monolog/LegacyHandler.php new file mode 100644 index 00000000..dbeb1369 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/LegacyHandler.php @@ -0,0 +1,236 @@ +<?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\Handler\StreamHandler. + * + * @since 1.25 + * @copyright © 2013 Wikimedia Foundation and contributors + */ +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( [ $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/www/wiki/includes/debug/logger/monolog/LineFormatter.php b/www/wiki/includes/debug/logger/monolog/LineFormatter.php new file mode 100644 index 00000000..cdc4da3a --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/LineFormatter.php @@ -0,0 +1,172 @@ +<?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 Exception; +use Monolog\Formatter\LineFormatter as MonologLineFormatter; +use MWExceptionHandler; + +/** + * Formats incoming records into a one-line string. + * + * An 'exeception' in the log record's context will be treated specially. + * It will be output for an '%exception%' placeholder in the format and + * excluded from '%context%' output if the '%exception%' placeholder is + * present. + * + * Exceptions that are logged with this formatter will optional have their + * stack traces appended. If that is done, MWExceptionHandler::redactedTrace() + * will be used to redact the trace information. + * + * @since 1.26 + * @copyright © 2015 Wikimedia Foundation and contributors + */ +class LineFormatter extends MonologLineFormatter { + + /** + * @param string $format The format of the message + * @param string $dateFormat The format of the timestamp: one supported by DateTime::format + * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries + * @param bool $ignoreEmptyContextAndExtra + * @param bool $includeStacktraces + */ + public function __construct( + $format = null, $dateFormat = null, $allowInlineLineBreaks = false, + $ignoreEmptyContextAndExtra = false, $includeStacktraces = false + ) { + parent::__construct( + $format, $dateFormat, $allowInlineLineBreaks, + $ignoreEmptyContextAndExtra + ); + $this->includeStacktraces( $includeStacktraces ); + } + + /** + * @inheritDoc + */ + public function format( array $record ) { + // Drop the 'private' flag from the context + unset( $record['context']['private'] ); + + // Handle exceptions specially: pretty format and remove from context + // Will be output for a '%exception%' placeholder in format + $prettyException = ''; + if ( isset( $record['context']['exception'] ) && + strpos( $this->format, '%exception%' ) !== false + ) { + $e = $record['context']['exception']; + unset( $record['context']['exception'] ); + + if ( $e instanceof Exception ) { + $prettyException = $this->normalizeException( $e ); + } elseif ( is_array( $e ) ) { + $prettyException = $this->normalizeExceptionArray( $e ); + } else { + $prettyException = $this->stringify( $e ); + } + } + + $output = parent::format( $record ); + + if ( strpos( $output, '%exception%' ) !== false ) { + $output = str_replace( '%exception%', $prettyException, $output ); + } + return $output; + } + + /** + * Convert an Exception to a string. + * + * @param Exception $e + * @return string + */ + protected function normalizeException( $e ) { + return $this->normalizeExceptionArray( $this->exceptionAsArray( $e ) ); + } + + /** + * Convert an exception to an array of structured data. + * + * @param Exception $e + * @return array + */ + protected function exceptionAsArray( Exception $e ) { + $out = [ + 'class' => get_class( $e ), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => MWExceptionHandler::redactTrace( $e->getTrace() ), + ]; + + $prev = $e->getPrevious(); + if ( $prev ) { + $out['previous'] = $this->exceptionAsArray( $prev ); + } + + return $out; + } + + /** + * Convert an array of Exception data to a string. + * + * @param array $e + * @return string + */ + protected function normalizeExceptionArray( array $e ) { + $defaults = [ + 'class' => 'Unknown', + 'file' => 'unknown', + 'line' => null, + 'message' => 'unknown', + 'trace' => [], + ]; + $e = array_merge( $defaults, $e ); + + $str = "\n[Exception {$e['class']}] (" . + "{$e['file']}:{$e['line']}) {$e['message']}"; + + if ( $this->includeStacktraces && $e['trace'] ) { + $str .= "\n" . + MWExceptionHandler::prettyPrintTrace( $e['trace'], ' ' ); + } + + if ( isset( $e['previous'] ) ) { + $prev = $e['previous']; + while ( $prev ) { + $prev = array_merge( $defaults, $prev ); + $str .= "\nCaused by: [Exception {$prev['class']}] (" . + "{$prev['file']}:{$prev['line']}) {$prev['message']}"; + + if ( $this->includeStacktraces && $prev['trace'] ) { + $str .= "\n" . + MWExceptionHandler::prettyPrintTrace( + $prev['trace'], ' ' + ); + } + + $prev = isset( $prev['previous'] ) ? $prev['previous'] : null; + } + } + return $str; + } +} diff --git a/www/wiki/includes/debug/logger/monolog/LogstashFormatter.php b/www/wiki/includes/debug/logger/monolog/LogstashFormatter.php new file mode 100644 index 00000000..09ed7555 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/LogstashFormatter.php @@ -0,0 +1,111 @@ +<?php + +namespace MediaWiki\Logger\Monolog; + +/** + * LogstashFormatter squashes the base message array and the context and extras subarrays into one. + * This can result in unfortunately named context fields overwriting other data (T145133). + * This class modifies the standard LogstashFormatter to rename such fields and flag the message. + * Also changes exception JSON-ification which is done poorly by the standard class. + * + * Compatible with Monolog 1.x only. + * + * @since 1.29 + */ +class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter { + /** @var array Keys which should not be used in log context */ + protected $reservedKeys = [ + // from LogstashFormatter + 'message', 'channel', 'level', 'type', + // from WebProcessor + 'url', 'ip', 'http_method', 'server', 'referrer', + // from WikiProcessor + 'host', 'wiki', 'reqId', 'mwversion', + // from config magic + 'normalized_message', + ]; + + /** + * Prevent key conflicts + * @param array $record + * @return array + */ + protected function formatV0( array $record ) { + if ( $this->contextPrefix ) { + return parent::formatV0( $record ); + } + + $context = !empty( $record['context'] ) ? $record['context'] : []; + $record['context'] = []; + $formatted = parent::formatV0( $record ); + + $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context ); + return $formatted; + } + + /** + * Prevent key conflicts + * @param array $record + * @return array + */ + protected function formatV1( array $record ) { + if ( $this->contextPrefix ) { + return parent::formatV1( $record ); + } + + $context = !empty( $record['context'] ) ? $record['context'] : []; + $record['context'] = []; + $formatted = parent::formatV1( $record ); + + $formatted = $this->fixKeyConflicts( $formatted, $context ); + return $formatted; + } + + /** + * Check whether some context field would overwrite another message key. If so, rename + * and flag. + * @param array $fields Fields to be sent to logstash + * @param array $context Copy of the original $record['context'] + * @return array Updated version of $fields + */ + protected function fixKeyConflicts( array $fields, array $context ) { + foreach ( $context as $key => $val ) { + if ( + in_array( $key, $this->reservedKeys, true ) && + isset( $fields[$key] ) && $fields[$key] !== $val + ) { + $fields['logstash_formatter_key_conflict'][] = $key; + $key = 'c_' . $key; + } + $fields[$key] = $val; + } + return $fields; + } + + /** + * Use a more user-friendly trace format than NormalizerFormatter + * @param \Exception|\Throwable $e + * @return array + */ + protected function normalizeException( $e ) { + if ( !$e instanceof \Exception && !$e instanceof \Throwable ) { + throw new \InvalidArgumentException( 'Exception/Throwable expected, got ' + . gettype( $e ) . ' / ' . get_class( $e ) ); + } + + $data = [ + 'class' => get_class( $e ), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile() . ':' . $e->getLine(), + 'trace' => \MWExceptionHandler::getRedactedTraceAsString( $e ), + ]; + + $previous = $e->getPrevious(); + if ( $previous ) { + $data['previous'] = $this->normalizeException( $previous ); + } + + return $data; + } +} diff --git a/www/wiki/includes/debug/logger/monolog/SyslogHandler.php b/www/wiki/includes/debug/logger/monolog/SyslogHandler.php new file mode 100644 index 00000000..780ea94d --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/SyslogHandler.php @@ -0,0 +1,94 @@ +<?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 + * @copyright © 2015 Wikimedia Foundation and contributors + */ +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/www/wiki/includes/debug/logger/monolog/WikiProcessor.php b/www/wiki/includes/debug/logger/monolog/WikiProcessor.php new file mode 100644 index 00000000..db5b9bf6 --- /dev/null +++ b/www/wiki/includes/debug/logger/monolog/WikiProcessor.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; + +/** + * Annotate log records with request-global metadata, such as the hostname, + * wiki / request ID, and MediaWiki version. + * + * @since 1.25 + * @copyright © 2013 Wikimedia Foundation and contributors + */ +class WikiProcessor { + + /** + * @param array $record + * @return array + */ + public function __invoke( array $record ) { + global $wgVersion; + $record['extra']['host'] = wfHostname(); + $record['extra']['wiki'] = wfWikiID(); + $record['extra']['mwversion'] = $wgVersion; + $record['extra']['reqId'] = \WebRequest::getRequestId(); + if ( wfIsCLI() && isset( $_SERVER['argv'] ) ) { + $record['extra']['cli_argv'] = implode( ' ', $_SERVER['argv'] ); + } + return $record; + } + +} |