%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/includes/debug/logger/monolog/ |
| Current File : /www/varak.net/wiki.varak.net/includes/debug/logger/monolog/KafkaHandler.php |
<?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'];
}
}