summaryrefslogtreecommitdiff
path: root/www/wiki/includes/debug
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/debug
first commit
Diffstat (limited to 'www/wiki/includes/debug')
-rw-r--r--www/wiki/includes/debug/MWDebug.php558
-rw-r--r--www/wiki/includes/debug/logger/ConsoleLogger.php21
-rw-r--r--www/wiki/includes/debug/logger/ConsoleSpi.php11
-rw-r--r--www/wiki/includes/debug/logger/LegacyLogger.php482
-rw-r--r--www/wiki/includes/debug/logger/LegacySpi.php57
-rw-r--r--www/wiki/includes/debug/logger/LoggerFactory.php102
-rw-r--r--www/wiki/includes/debug/logger/MonologSpi.php271
-rw-r--r--www/wiki/includes/debug/logger/NullSpi.php60
-rw-r--r--www/wiki/includes/debug/logger/Spi.php46
-rw-r--r--www/wiki/includes/debug/logger/monolog/AvroFormatter.php171
-rw-r--r--www/wiki/includes/debug/logger/monolog/BufferHandler.php46
-rw-r--r--www/wiki/includes/debug/logger/monolog/KafkaHandler.php279
-rw-r--r--www/wiki/includes/debug/logger/monolog/LegacyFormatter.php47
-rw-r--r--www/wiki/includes/debug/logger/monolog/LegacyHandler.php236
-rw-r--r--www/wiki/includes/debug/logger/monolog/LineFormatter.php172
-rw-r--r--www/wiki/includes/debug/logger/monolog/LogstashFormatter.php111
-rw-r--r--www/wiki/includes/debug/logger/monolog/SyslogHandler.php94
-rw-r--r--www/wiki/includes/debug/logger/monolog/WikiProcessor.php48
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;
+ }
+
+}