diff options
author | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-05-01 15:32:59 -0400 |
---|---|---|
committer | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-05-01 15:32:59 -0400 |
commit | 6dc1997577fab2c366781fd7048144935afa0012 (patch) | |
tree | 8918d28c7ab4342f0738985e37af1dfc42d0e93a /includes/debug | |
parent | 150f94f051128f367bc89f6b7e5f57eb2a69fc62 (diff) | |
parent | fa89acd685cb09cdbe1c64cbb721ec64975bbbc1 (diff) |
Merge commit 'fa89acd'
# Conflicts:
# .gitignore
# extensions/ArchInterWiki.sql
Diffstat (limited to 'includes/debug')
-rw-r--r-- | includes/debug/MWDebug.php | 6 | ||||
-rw-r--r-- | includes/debug/logger/LegacyLogger.php | 101 | ||||
-rw-r--r-- | includes/debug/logger/LegacySpi.php | 4 | ||||
-rw-r--r-- | includes/debug/logger/LoggerFactory.php | 16 | ||||
-rw-r--r-- | includes/debug/logger/MonologSpi.php | 35 | ||||
-rw-r--r-- | includes/debug/logger/NullSpi.php | 8 | ||||
-rw-r--r-- | includes/debug/logger/Spi.php | 8 | ||||
-rw-r--r-- | includes/debug/logger/monolog/AvroFormatter.php | 139 | ||||
-rw-r--r-- | includes/debug/logger/monolog/BufferHandler.php | 47 | ||||
-rw-r--r-- | includes/debug/logger/monolog/KafkaHandler.php | 224 | ||||
-rw-r--r-- | includes/debug/logger/monolog/LegacyFormatter.php | 4 | ||||
-rw-r--r-- | includes/debug/logger/monolog/LineFormatter.php | 177 |
12 files changed, 729 insertions, 40 deletions
diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index ae2d9954..1249ebae 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -432,10 +432,8 @@ class MWDebug { // Cannot use OutputPage::addJsConfigVars because those are already outputted // by the time this method is called. - $html = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) ) - ) + $html = ResourceLoader::makeInlineScript( + ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) ) ); } diff --git a/includes/debug/logger/LegacyLogger.php b/includes/debug/logger/LegacyLogger.php index edaef4a7..0f4c6484 100644 --- a/includes/debug/logger/LegacyLogger.php +++ b/includes/debug/logger/LegacyLogger.php @@ -21,7 +21,9 @@ namespace MediaWiki\Logger; use DateTimeZone; +use Exception; use MWDebug; +use MWExceptionHandler; use Psr\Log\AbstractLogger; use Psr\Log\LogLevel; use UDPTransport; @@ -39,7 +41,7 @@ use UDPTransport; * See documentation in DefaultSettings.php for detailed explanations of each * variable. * - * @see \MediaWiki\Logger\LoggerFactory + * @see \\MediaWiki\\Logger\\LoggerFactory * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2014 Bryan Davis and Wikimedia Foundation. @@ -52,10 +54,10 @@ class LegacyLogger extends AbstractLogger { protected $channel; /** - * Convert Psr\Log\LogLevel constants into int for sane comparisons + * Convert Psr\\Log\\LogLevel constants into int for sane comparisons * These are the same values that Monlog uses * - * @var array + * @var array $levelMapping */ protected static $levelMapping = array( LogLevel::DEBUG => 100, @@ -99,7 +101,7 @@ class LegacyLogger extends AbstractLogger { * * @param string $channel * @param string $message - * @param string|int $level Psr\Log\LogEvent constant or Monlog level int + * @param string|int $level Psr\\Log\\LogEvent constant or Monlog level int * @param array $context * @return bool True if message should be sent to disk/network, false * otherwise @@ -167,7 +169,7 @@ class LegacyLogger extends AbstractLogger { * @return string */ public static function format( $channel, $message, $context ) { - global $wgDebugLogGroups; + global $wgDebugLogGroups, $wgLogExceptionBacktrace; if ( $channel === 'wfDebug' ) { $text = self::formatAsWfDebug( $channel, $message, $context ); @@ -181,7 +183,7 @@ class LegacyLogger extends AbstractLogger { } elseif ( $channel === 'profileoutput' ) { // Legacy wfLogProfilingData formatitng $forward = ''; - if ( isset( $context['forwarded_for'] )) { + if ( isset( $context['forwarded_for'] ) ) { $forward = " forwarded for {$context['forwarded_for']}"; } if ( isset( $context['client_ip'] ) ) { @@ -215,6 +217,25 @@ class LegacyLogger extends AbstractLogger { $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 ); } @@ -253,7 +274,7 @@ class LegacyLogger extends AbstractLogger { global $wgDBerrorLogTZ; static $cachedTimezone = null; - if ( $wgDBerrorLogTZ && !$cachedTimezone ) { + if ( !$cachedTimezone ) { $cachedTimezone = new DateTimeZone( $wgDBerrorLogTZ ); } @@ -301,7 +322,7 @@ class LegacyLogger extends AbstractLogger { if ( strpos( $message, '{' ) !== false ) { $replace = array(); foreach ( $context as $key => $val ) { - $replace['{' . $key . '}'] = $val; + $replace['{' . $key . '}'] = self::flatten( $val ); } $message = strtr( $message, $replace ); } @@ -310,6 +331,66 @@ class LegacyLogger extends AbstractLogger { /** + * 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 $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' @@ -365,7 +446,7 @@ class LegacyLogger extends AbstractLogger { $transport = UDPTransport::newFromString( $file ); $transport->emit( $text ); } else { - wfSuppressWarnings(); + \MediaWiki\suppressWarnings(); $exists = file_exists( $file ); $size = $exists ? filesize( $file ) : false; if ( !$exists || @@ -373,7 +454,7 @@ class LegacyLogger extends AbstractLogger { ) { file_put_contents( $file, $text, FILE_APPEND ); } - wfRestoreWarnings(); + \MediaWiki\restoreWarnings(); } } diff --git a/includes/debug/logger/LegacySpi.php b/includes/debug/logger/LegacySpi.php index 1bf39e41..6a7f1d05 100644 --- a/includes/debug/logger/LegacySpi.php +++ b/includes/debug/logger/LegacySpi.php @@ -30,7 +30,7 @@ namespace MediaWiki\Logger; * ); * @endcode * - * @see \MediaWiki\Logger\LoggerFactory + * @see \\MediaWiki\\Logger\\LoggerFactory * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2014 Bryan Davis and Wikimedia Foundation. @@ -47,7 +47,7 @@ class LegacySpi implements Spi { * Get a logger instance. * * @param string $channel Logging channel - * @return \Psr\Log\LoggerInterface Logger instance + * @return \\Psr\\Log\\LoggerInterface Logger instance */ public function getLogger( $channel ) { if ( !isset( $this->singletons[$channel] ) ) { diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php index b3078b9a..1e44b708 100644 --- a/includes/debug/logger/LoggerFactory.php +++ b/includes/debug/logger/LoggerFactory.php @@ -25,7 +25,7 @@ use ObjectFactory; /** * PSR-3 logger instance factory. * - * Creation of \Psr\Log\LoggerInterface instances is managed via the + * Creation of \\Psr\\Log\\LoggerInterface instances is managed via the * LoggerFactory::getInstance() static method which in turn delegates to the * currently registered service provider. * @@ -38,7 +38,7 @@ use ObjectFactory; * $wgMWLoggerDefaultSpi is expected to be an array usable by * ObjectFactory::getObjectFromSpec() to create a class. * - * @see \MediaWiki\Logger\Spi + * @see \\MediaWiki\\Logger\\Spi * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2014 Bryan Davis and Wikimedia Foundation. @@ -47,16 +47,16 @@ class LoggerFactory { /** * Service provider. - * @var Spi $spi + * @var \MediaWiki\Logger\Spi $spi */ private static $spi; /** - * Register a service provider to create new \Psr\Log\LoggerInterface + * Register a service provider to create new \\Psr\\Log\\LoggerInterface * instances. * - * @param Spi $provider Provider to register + * @param \\MediaWiki\\Logger\\Spi $provider Provider to register */ public static function registerProvider( Spi $provider ) { self::$spi = $provider; @@ -71,7 +71,7 @@ class LoggerFactory { * Spi registration. $wgMWLoggerDefaultSpi is expected to be an * array usable by ObjectFactory::getObjectFromSpec() to create a class. * - * @return Spi + * @return \\MediaWiki\\Logger\\Spi * @see registerProvider() * @see ObjectFactory::getObjectFromSpec() */ @@ -91,10 +91,10 @@ class LoggerFactory { * Get a named logger instance from the currently configured logger factory. * * @param string $channel Logger channel (name) - * @return \Psr\Log\LoggerInterface + * @return \\Psr\\Log\\LoggerInterface */ public static function getInstance( $channel ) { - if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { $message = ( 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' . "library</a> to be present. This library is not embedded directly in MediaWiki's " . diff --git a/includes/debug/logger/MonologSpi.php b/includes/debug/logger/MonologSpi.php index a07fdc4a..274e18e1 100644 --- a/includes/debug/logger/MonologSpi.php +++ b/includes/debug/logger/MonologSpi.php @@ -20,6 +20,7 @@ namespace MediaWiki\Logger; +use MediaWiki\Logger\Monolog\BufferHandler; use Monolog\Logger; use ObjectFactory; @@ -30,7 +31,7 @@ use ObjectFactory; * 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 + * The ['loggers']['\@default'] configuration will be used to create loggers * for any channel that isn't explicitly named in the 'loggers' configuration * section. * @@ -84,6 +85,7 @@ use ObjectFactory; * 'logstash' * ), * 'formatter' => 'logstash', + * 'buffer' => true, * ), * 'udp2log' => array( * 'class' => '\\MediaWiki\\Logger\\Monolog\\LegacyHandler', @@ -129,7 +131,25 @@ class MonologSpi implements Spi { * @param array $config Configuration data. */ public function __construct( array $config ) { - $this->config = $config; + $this->config = array(); + $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(); } @@ -158,7 +178,7 @@ class MonologSpi implements Spi { * name will return the cached instance. * * @param string $channel Logging channel - * @return \Psr\Log\LoggerInterface Logger instance + * @return \\Psr\\Log\\LoggerInterface Logger instance */ public function getLogger( $channel ) { if ( !isset( $this->singletons['loggers'][$channel] ) ) { @@ -180,7 +200,7 @@ class MonologSpi implements Spi { * Create a logger. * @param string $channel Logger channel * @param array $spec Configuration - * @return \Monolog\Logger + * @return \\Monolog\\Logger */ protected function createLogger( $channel, $spec ) { $obj = new Logger( $channel ); @@ -218,7 +238,7 @@ class MonologSpi implements Spi { /** * Create or return cached handler. * @param string $name Processor name - * @return \Monolog\Handler\HandlerInterface + * @return \\Monolog\\Handler\\HandlerInterface */ public function getHandler( $name ) { if ( !isset( $this->singletons['handlers'][$name] ) ) { @@ -229,6 +249,9 @@ class MonologSpi implements Spi { $this->getFormatter( $spec['formatter'] ) ); } + if ( isset( $spec['buffer'] ) && $spec['buffer'] ) { + $handler = new BufferHandler( $handler ); + } $this->singletons['handlers'][$name] = $handler; } return $this->singletons['handlers'][$name]; @@ -238,7 +261,7 @@ class MonologSpi implements Spi { /** * Create or return cached formatter. * @param string $name Formatter name - * @return \Monolog\Formatter\FormatterInterface + * @return \\Monolog\\Formatter\\FormatterInterface */ public function getFormatter( $name ) { if ( !isset( $this->singletons['formatters'][$name] ) ) { diff --git a/includes/debug/logger/NullSpi.php b/includes/debug/logger/NullSpi.php index a82d2c4c..c9c74821 100644 --- a/includes/debug/logger/NullSpi.php +++ b/includes/debug/logger/NullSpi.php @@ -23,7 +23,7 @@ namespace MediaWiki\Logger; use Psr\Log\NullLogger; /** - * LoggerFactory service provider that creates \Psr\Log\NullLogger + * LoggerFactory service provider that creates \\Psr\\Log\\NullLogger * instances. A NullLogger silently discards all log events sent to it. * * Usage: @@ -33,7 +33,7 @@ use Psr\Log\NullLogger; * ); * @endcode * - * @see \MediaWiki\Logger\LoggerFactory + * @see \\MediaWiki\\Logger\\LoggerFactory * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2014 Bryan Davis and Wikimedia Foundation. @@ -41,7 +41,7 @@ use Psr\Log\NullLogger; class NullSpi implements Spi { /** - * @var \Psr\Log\NullLogger $singleton + * @var \\Psr\\Log\\NullLogger $singleton */ protected $singleton; @@ -55,7 +55,7 @@ class NullSpi implements Spi { * Get a logger instance. * * @param string $channel Logging channel - * @return \Psr\Log\NullLogger Logger instance + * @return \\Psr\\Log\\NullLogger Logger instance */ public function getLogger( $channel ) { return $this->singleton; diff --git a/includes/debug/logger/Spi.php b/includes/debug/logger/Spi.php index 044789f2..51818a38 100644 --- a/includes/debug/logger/Spi.php +++ b/includes/debug/logger/Spi.php @@ -21,15 +21,15 @@ namespace MediaWiki\Logger; /** - * Service provider interface for \Psr\Log\LoggerInterface implementation + * 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 + * 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 + * @see \\MediaWiki\\Logger\\LoggerFactory * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2014 Bryan Davis and Wikimedia Foundation. @@ -40,7 +40,7 @@ interface Spi { * Get a logger instance. * * @param string $channel Logging channel - * @return \Psr\Log\LoggerInterface Logger instance + * @return \\Psr\\Log\\LoggerInterface Logger instance */ public function getLogger( $channel ); diff --git a/includes/debug/logger/monolog/AvroFormatter.php b/includes/debug/logger/monolog/AvroFormatter.php new file mode 100644 index 00000000..b6adab4a --- /dev/null +++ b/includes/debug/logger/monolog/AvroFormatter.php @@ -0,0 +1,139 @@ +<?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 AvroNamedSchemata; +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 array Map from schema name to schema definition + */ + protected $schemas; + + /** + * @var AvroStringIO + */ + protected $io; + + /** + * @var AvroIOBinaryEncoder + */ + protected $encoder; + + /** + * @var AvroIODatumWriter + */ + protected $writer; + + /** + * @var 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'] ); + if ( $schema === 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 $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 = array(); + foreach ( $records as $record ) { + $message = $this->format( $record ); + if ( $message !== null ) { + $result[] = $message; + } + } + return $result; + } + + /** + * Get the writer for the named channel + * + * @var string $channel Name of the schema to fetch + * @return AvroSchema|null + */ + protected function getSchema( $channel ) { + if ( !isset( $this->schemas[$channel] ) ) { + return null; + } + if ( !$this->schemas[$channel] instanceof AvroSchema ) { + if ( is_string( $this->schemas[$channel] ) ) { + $this->schemas[$channel] = AvroSchema::parse( $this->schemas[$channel] ); + } else { + $this->schemas[$channel] = AvroSchema::real_parse( + $this->schemas[$channel], + null, + new AvroNamedSchemata() + ); + } + } + return $this->schemas[$channel]; + } +} diff --git a/includes/debug/logger/monolog/BufferHandler.php b/includes/debug/logger/monolog/BufferHandler.php new file mode 100644 index 00000000..3ebd0b1f --- /dev/null +++ b/includes/debug/logger/monolog/BufferHandler.php @@ -0,0 +1,47 @@ +<?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 the Monolog 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( array( $this, 'close' ) ); + $this->initialized = true; + } + return parent::handle( $record ); + } +} + diff --git a/includes/debug/logger/monolog/KafkaHandler.php b/includes/debug/logger/monolog/KafkaHandler.php new file mode 100644 index 00000000..59d7764a --- /dev/null +++ b/includes/debug/logger/monolog/KafkaHandler.php @@ -0,0 +1,224 @@ +<?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 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 = array(); + + /** + * @var array defaults for constructor options + */ + private static $defaultOptions = array( + 'alias' => array(), // map from monolog channel to kafka topic + 'swallowExceptions' => false, // swallow exceptions sending records + 'logExceptions' => null, // A PSR3 logger to inform about errors + ); + + /** + * @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 = array(), $level = Logger::DEBUG, $bubble = true ) { + $metadata = new MetaDataFromKafka( $kafkaServers ); + $produce = new Produce( $metadata ); + if ( isset( $options['logExceptions'] ) && is_string( $options['logExceptions'] ) ) { + $options['logExceptions'] = LoggerFactory::getInstance( $options['logExceptions'] ); + } + return new self( $produce, $options, $level, $bubble ); + } + + /** + * {@inheritDoc} + */ + protected function write( array $record ) { + if ( $record['formatted'] !== null ) { + $this->addMessages( $record['channel'], array( $record['formatted'] ) ); + $this->send(); + } + } + + /** + * {@inheritDoc} + */ + public function handleBatch( array $batch ) { + $channels = array(); + 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 = array(); + 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 { + $this->produce->send(); + } catch ( \Kafka\Exception $e ) { + $ignore = $this->warning( + 'Error sending records to kafka: {exception}', + array( 'exception' => $e ) ); + if ( !$ignore ) { + throw $e; + } + } + } + + /** + * @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}', + array( '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}', + array( '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 = array() ) { + if ( $this->options['logExceptions'] instanceof LoggerInterface ) { + $this->options['logExceptions']->warning( $message, $context ); + } + return $this->options['swallowExceptions']; + } +} diff --git a/includes/debug/logger/monolog/LegacyFormatter.php b/includes/debug/logger/monolog/LegacyFormatter.php index 9ec15cb8..42e7caba 100644 --- a/includes/debug/logger/monolog/LegacyFormatter.php +++ b/includes/debug/logger/monolog/LegacyFormatter.php @@ -26,12 +26,12 @@ 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. + * delegating the formatting to \\MediaWiki\\Logger\\LegacyLogger. * * @since 1.25 * @author Bryan Davis <bd808@wikimedia.org> * @copyright © 2013 Bryan Davis and Wikimedia Foundation. - * @see \MediaWiki\Logger\LegacyLogger + * @see \\MediaWiki\\Logger\\LegacyLogger */ class LegacyFormatter extends NormalizerFormatter { diff --git a/includes/debug/logger/monolog/LineFormatter.php b/includes/debug/logger/monolog/LineFormatter.php new file mode 100644 index 00000000..2ba7a53c --- /dev/null +++ b/includes/debug/logger/monolog/LineFormatter.php @@ -0,0 +1,177 @@ +<?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 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2015 Bryan Davis and Wikimedia Foundation. + */ +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( Exception $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 = array( + '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 = array( + 'class' => 'Unknown', + 'file' => 'unknown', + 'line' => null, + 'message' => 'unknown', + 'trace' => array(), + ); + $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; + } +} |