summaryrefslogtreecommitdiff
path: root/includes/debug/logger
diff options
context:
space:
mode:
Diffstat (limited to 'includes/debug/logger')
-rw-r--r--includes/debug/logger/LegacyLogger.php101
-rw-r--r--includes/debug/logger/LegacySpi.php4
-rw-r--r--includes/debug/logger/LoggerFactory.php16
-rw-r--r--includes/debug/logger/MonologSpi.php35
-rw-r--r--includes/debug/logger/NullSpi.php8
-rw-r--r--includes/debug/logger/Spi.php8
-rw-r--r--includes/debug/logger/monolog/AvroFormatter.php139
-rw-r--r--includes/debug/logger/monolog/BufferHandler.php47
-rw-r--r--includes/debug/logger/monolog/KafkaHandler.php224
-rw-r--r--includes/debug/logger/monolog/LegacyFormatter.php4
-rw-r--r--includes/debug/logger/monolog/LineFormatter.php177
11 files changed, 727 insertions, 36 deletions
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;
+ }
+}