diff options
Diffstat (limited to 'includes/debug/logger/LegacyLogger.php')
-rw-r--r-- | includes/debug/logger/LegacyLogger.php | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/includes/debug/logger/LegacyLogger.php b/includes/debug/logger/LegacyLogger.php new file mode 100644 index 00000000..edaef4a7 --- /dev/null +++ b/includes/debug/logger/LegacyLogger.php @@ -0,0 +1,380 @@ +<?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 MWDebug; +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 + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +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 + */ + protected static $levelMapping = array( + LogLevel::DEBUG => 100, + LogLevel::INFO => 200, + LogLevel::NOTICE => 250, + LogLevel::WARNING => 300, + LogLevel::ERROR => 400, + LogLevel::CRITICAL => 500, + LogLevel::ALERT => 550, + LogLevel::EMERGENCY => 600, + ); + + + /** + * @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 + */ + public function log( $level, $message, array $context = array() ) { + if ( self::shouldEmit( $this->channel, $message, $level, $context ) ) { + $text = self::format( $this->channel, $message, $context ); + $destination = self::destination( $this->channel, $message, $context ); + self::emit( $text, $destination ); + } + // Add to debug toolbar + MWDebug::debugMsg( $message, array( '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 Monlog 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 ( $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 ( 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'] ) ) { + if ( is_string( $level ) ) { + $level = self::$levelMapping[$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; + + 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 ); + } + + 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 ( $wgDBerrorLogTZ && !$cachedTimezone ) { + $cachedTimezone = new DateTimeZone( $wgDBerrorLogTZ ); + } + + // Workaround for https://bugs.php.net/bug.php?id=52063 + // Can be removed when min PHP > 5.3.6 + if ( $cachedTimezone === null ) { + $d = date_create( 'now' ); + } else { + $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 + */ + 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 = array(); + foreach ( $context as $key => $val ) { + $replace['{' . $key . '}'] = $val; + } + $message = strtr( $message, $replace ); + } + return $message; + } + + + /** + * 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 + * @throws MWException + */ + public static function emit( $text, $file ) { + if ( substr( $file, 0, 4 ) == 'udp:' ) { + $transport = UDPTransport::newFromString( $file ); + $transport->emit( $text ); + } else { + wfSuppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || + ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) + ) { + file_put_contents( $file, $text, FILE_APPEND ); + } + wfRestoreWarnings(); + } + } + +} |