MediaWiki  master
LegacyLogger.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Logger;
22 
24 use Error;
26 use WikiMap;
27 use MWDebug;
33 
51 class LegacyLogger extends AbstractLogger {
52 
56  protected $channel;
57 
64  protected static $levelMapping = [
65  LogLevel::DEBUG => 100,
66  LogLevel::INFO => 200,
67  LogLevel::NOTICE => 250,
68  LogLevel::WARNING => 300,
69  LogLevel::ERROR => 400,
70  LogLevel::CRITICAL => 500,
71  LogLevel::ALERT => 550,
72  LogLevel::EMERGENCY => 600,
73  ];
74 
78  protected static $dbChannels = [
79  'DBQuery' => true,
80  'DBConnection' => true
81  ];
82 
86  public function __construct( $channel ) {
87  $this->channel = $channel;
88  }
89 
98  public function log( $level, $message, array $context = [] ) {
99  global $wgDBerrorLog;
100 
101  if ( is_string( $level ) ) {
102  $level = self::$levelMapping[$level];
103  }
104  if ( $this->channel === 'DBQuery'
105  && isset( $context['method'] )
106  && isset( $context['master'] )
107  && isset( $context['runtime'] )
108  ) {
109  // Also give the query information to the MWDebug tools
110  $enabled = MWDebug::query(
111  $message,
112  $context['method'],
113  $context['master'],
114  $context['runtime']
115  );
116  if ( $enabled ) {
117  // If we the toolbar was enabled, return early so that we don't
118  // also log the query to the main debug output.
119  return;
120  }
121  }
122 
123  // If this is a DB-related error, and the site has $wgDBerrorLog
124  // configured, rewrite the channel as wfLogDBError instead.
125  // Likewise, if the site does not use $wgDBerrorLog, it should
126  // configurable like any other channel via $wgDebugLogGroups
127  // or $wgMWLoggerDefaultSpi.
128  if ( isset( self::$dbChannels[$this->channel] )
129  && $level >= self::$levelMapping[LogLevel::ERROR]
130  && $wgDBerrorLog
131  ) {
132  // Format and write DB errors to the legacy locations
133  $effectiveChannel = 'wfLogDBError';
134  } else {
135  $effectiveChannel = $this->channel;
136  }
137 
138  if ( self::shouldEmit( $effectiveChannel, $message, $level, $context ) ) {
139  $text = self::format( $effectiveChannel, $message, $context );
140  $destination = self::destination( $effectiveChannel, $message, $context );
141  self::emit( $text, $destination );
142  }
143  if ( !isset( $context['private'] ) || !$context['private'] ) {
144  // Add to debug toolbar if not marked as "private"
145  MWDebug::debugMsg( $message, [ 'channel' => $this->channel ] + $context );
146  }
147  }
148 
159  public static function shouldEmit( $channel, $message, $level, $context ) {
161 
162  if ( is_string( $level ) ) {
163  $level = self::$levelMapping[$level];
164  }
165 
166  if ( $channel === 'wfLogDBError' ) {
167  // wfLogDBError messages are emitted if a database log location is
168  // specfied.
169  $shouldEmit = (bool)$wgDBerrorLog;
170 
171  } elseif ( $channel === 'wfDebug' ) {
172  // wfDebug messages are emitted if a catch all logging file has
173  // been specified. Checked explicitly so that 'private' flagged
174  // messages are not discarded by unset $wgDebugLogGroups channel
175  // handling below.
176  $shouldEmit = $wgDebugLogFile != '';
177 
178  } elseif ( isset( $wgDebugLogGroups[$channel] ) ) {
179  $logConfig = $wgDebugLogGroups[$channel];
180 
181  if ( is_array( $logConfig ) ) {
182  $shouldEmit = true;
183  if ( isset( $logConfig['sample'] ) ) {
184  // Emit randomly with a 1 in 'sample' chance for each message.
185  $shouldEmit = mt_rand( 1, $logConfig['sample'] ) === 1;
186  }
187 
188  if ( isset( $logConfig['level'] ) ) {
189  $shouldEmit = $level >= self::$levelMapping[$logConfig['level']];
190  }
191  } else {
192  // Emit unless the config value is explictly false.
193  $shouldEmit = $logConfig !== false;
194  }
195 
196  } elseif ( isset( $context['private'] ) && $context['private'] ) {
197  // Don't emit if the message didn't match previous checks based on
198  // the channel and the event is marked as private. This check
199  // discards messages sent via wfDebugLog() with dest == 'private'
200  // and no explicit wgDebugLogGroups configuration.
201  $shouldEmit = false;
202  } else {
203  // Default return value is the same as the historic wfDebug
204  // method: emit if $wgDebugLogFile has been set.
205  $shouldEmit = $wgDebugLogFile != '';
206  }
207 
208  return $shouldEmit;
209  }
210 
223  public static function format( $channel, $message, $context ) {
225 
226  if ( $channel === 'wfDebug' ) {
227  $text = self::formatAsWfDebug( $channel, $message, $context );
228 
229  } elseif ( $channel === 'wfLogDBError' ) {
230  $text = self::formatAsWfLogDBError( $channel, $message, $context );
231 
232  } elseif ( $channel === 'profileoutput' ) {
233  // Legacy wfLogProfilingData formatitng
234  $forward = '';
235  if ( isset( $context['forwarded_for'] ) ) {
236  $forward = " forwarded for {$context['forwarded_for']}";
237  }
238  if ( isset( $context['client_ip'] ) ) {
239  $forward .= " client IP {$context['client_ip']}";
240  }
241  if ( isset( $context['from'] ) ) {
242  $forward .= " from {$context['from']}";
243  }
244  if ( $forward ) {
245  $forward = "\t(proxied via {$context['proxy']}{$forward})";
246  }
247  if ( $context['anon'] ) {
248  $forward .= ' anon';
249  }
250  if ( !isset( $context['url'] ) ) {
251  $context['url'] = 'n/a';
252  }
253 
254  $log = sprintf( "%s\t%04.3f\t%s%s\n",
255  gmdate( 'YmdHis' ), $context['elapsed'], $context['url'], $forward );
256 
257  $text = self::formatAsWfDebugLog(
258  $channel, $log . $context['output'], $context );
259 
260  } elseif ( !isset( $wgDebugLogGroups[$channel] ) ) {
261  $text = self::formatAsWfDebug(
262  $channel, "[{$channel}] {$message}", $context );
263 
264  } else {
265  // Default formatting is wfDebugLog's historic style
266  $text = self::formatAsWfDebugLog( $channel, $message, $context );
267  }
268 
269  // Append stacktrace of exception if available
270  if ( $wgLogExceptionBacktrace && isset( $context['exception'] ) ) {
271  $e = $context['exception'];
272  $backtrace = false;
273 
274  if ( $e instanceof Throwable || $e instanceof Exception ) {
276 
277  } elseif ( is_array( $e ) && isset( $e['trace'] ) ) {
278  // Exception has already been unpacked as structured data
279  $backtrace = $e['trace'];
280  }
281 
282  if ( $backtrace ) {
283  $text .= MWExceptionHandler::prettyPrintTrace( $backtrace ) .
284  "\n";
285  }
286  }
287 
288  return self::interpolate( $text, $context );
289  }
290 
299  protected static function formatAsWfDebug( $channel, $message, $context ) {
300  $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $message );
301  if ( isset( $context['seconds_elapsed'] ) ) {
302  // Prepend elapsed request time and real memory usage with two
303  // trailing spaces.
304  $text = "{$context['seconds_elapsed']} {$context['memory_used']} {$text}";
305  }
306  if ( isset( $context['prefix'] ) ) {
307  $text = "{$context['prefix']}{$text}";
308  }
309  return "{$text}\n";
310  }
311 
320  protected static function formatAsWfLogDBError( $channel, $message, $context ) {
321  global $wgDBerrorLogTZ;
322  static $cachedTimezone = null;
323 
324  if ( !$cachedTimezone ) {
325  $cachedTimezone = new DateTimeZone( $wgDBerrorLogTZ );
326  }
327 
328  $d = date_create( 'now', $cachedTimezone );
329  $date = $d->format( 'D M j G:i:s T Y' );
330 
331  $host = wfHostname();
333 
334  $text = "{$date}\t{$host}\t{$wiki}\t{$message}\n";
335  return $text;
336  }
337 
346  protected static function formatAsWfDebugLog( $channel, $message, $context ) {
347  $time = wfTimestamp( TS_DB );
349  $host = wfHostname();
350  $text = "{$time} {$host} {$wiki}: {$message}\n";
351  return $text;
352  }
353 
361  public static function interpolate( $message, array $context ) {
362  if ( strpos( $message, '{' ) !== false ) {
363  $replace = [];
364  foreach ( $context as $key => $val ) {
365  $replace['{' . $key . '}'] = self::flatten( $val );
366  }
367  $message = strtr( $message, $replace );
368  }
369  return $message;
370  }
371 
379  protected static function flatten( $item ) {
380  if ( $item === null ) {
381  return '[Null]';
382  }
383 
384  if ( is_bool( $item ) ) {
385  return $item ? 'true' : 'false';
386  }
387 
388  if ( is_float( $item ) ) {
389  if ( is_infinite( $item ) ) {
390  return ( $item > 0 ? '' : '-' ) . 'INF';
391  }
392  if ( is_nan( $item ) ) {
393  return 'NaN';
394  }
395  return (string)$item;
396  }
397 
398  if ( is_scalar( $item ) ) {
399  return (string)$item;
400  }
401 
402  if ( is_array( $item ) ) {
403  return '[Array(' . count( $item ) . ')]';
404  }
405 
406  if ( $item instanceof \DateTime ) {
407  return $item->format( 'c' );
408  }
409 
410  if ( $item instanceof Throwable || $item instanceof Exception ) {
411  $which = $item instanceof Error ? 'Error' : 'Exception';
412  return '[' . $which . ' ' . get_class( $item ) . '( ' .
413  $item->getFile() . ':' . $item->getLine() . ') ' .
414  $item->getMessage() . ']';
415  }
416 
417  if ( is_object( $item ) ) {
418  if ( method_exists( $item, '__toString' ) ) {
419  return (string)$item;
420  }
421 
422  return '[Object ' . get_class( $item ) . ']';
423  }
424 
425  if ( is_resource( $item ) ) {
426  return '[Resource ' . get_resource_type( $item ) . ']';
427  }
428 
429  return '[Unknown ' . gettype( $item ) . ']';
430  }
431 
442  protected static function destination( $channel, $message, $context ) {
444 
445  // Default destination is the debug log file as historically used by
446  // the wfDebug function.
447  $destination = $wgDebugLogFile;
448 
449  if ( isset( $context['destination'] ) ) {
450  // Use destination explicitly provided in context
451  $destination = $context['destination'];
452 
453  } elseif ( $channel === 'wfDebug' ) {
454  $destination = $wgDebugLogFile;
455 
456  } elseif ( $channel === 'wfLogDBError' ) {
457  $destination = $wgDBerrorLog;
458 
459  } elseif ( isset( $wgDebugLogGroups[$channel] ) ) {
460  $logConfig = $wgDebugLogGroups[$channel];
461 
462  if ( is_array( $logConfig ) ) {
463  $destination = $logConfig['destination'];
464  } else {
465  $destination = strval( $logConfig );
466  }
467  }
468 
469  return $destination;
470  }
471 
481  public static function emit( $text, $file ) {
482  if ( substr( $file, 0, 4 ) == 'udp:' ) {
483  $transport = UDPTransport::newFromString( $file );
484  $transport->emit( $text );
485  } else {
486  \Wikimedia\suppressWarnings();
487  $exists = file_exists( $file );
488  $size = $exists ? filesize( $file ) : false;
489  if ( !$exists ||
490  ( $size !== false && $size + strlen( $text ) < 0x7fffffff )
491  ) {
492  file_put_contents( $file, $text, FILE_APPEND );
493  }
494  \Wikimedia\restoreWarnings();
495  }
496  }
497 
498 }
$wgLogExceptionBacktrace
If true, send the exception backtrace to the error log.
static newFromString( $info)
$wgDBerrorLogTZ
Timezone to use in the error log.
PSR-3 logger that mimics the historic implementation of MediaWiki&#39;s former wfErrorLog logging impleme...
$wgDebugLogGroups
Map of string log group names to log destinations.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
static getRedactedTrace( $e)
Return a copy of an exception&#39;s backtrace as an array.
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2147
$wgDBerrorLog
File to log database errors to.
static array __construct( $channel)
wfHostname()
Fetch server name for use in error reporting etc.
static debugMsg( $str, $context=[])
This is a method to pass messages from wfDebug to the pretty debugger.
Definition: MWDebug.php:323
static getWikiIdFromDbDomain( $domain)
Get the wiki ID of a database domain.
Definition: WikiMap.php:269
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1781
$wgDebugLogFile
Filename for debug logging.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1972
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static interpolate( $message, array $context)
Interpolate placeholders in logging message.
IContextSource $context
Definition: MediaWiki.php:38
log( $level, $message, array $context=[])
Logs with an arbitrary level.
static formatAsWfDebugLog( $channel, $message, $context)
Format a message as `wfDebugLog() would have formatted it.
static destination( $channel, $message, $context)
Select the appropriate log output destination for the given log event.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static emit( $text, $file)
Log to a file without getting "file size exceeded" signals.
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static format( $channel, $message, $context)
Format a message.
static query( $sql, $function, $isMaster, $runTime)
Begins profiling on a database query.
Definition: MWDebug.php:354
static prettyPrintTrace(array $trace, $pad='')
Generate a string representation of a stacktrace.
static formatAsWfDebug( $channel, $message, $context)
Format a message as wfDebug() would have formatted it.
static formatAsWfLogDBError( $channel, $message, $context)
Format a message as wfLogDBError() would have formatted it.
static $levelMapping
Convert \Psr\Log\LogLevel constants into int for sane comparisons These are the same values that Monl...
static flatten( $item)
Convert a logging context element to a string suitable for interpolation.
static shouldEmit( $channel, $message, $level, $context)
Determine if the given message should be emitted or not.
if the prop value should be in the metadata multi language array format
Definition: hooks.txt:1623