Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
79.69% |
51 / 64 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
| MonologSpi | |
79.69% |
51 / 64 |
|
62.50% |
5 / 8 |
34.57 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| mergeConfig | |
46.15% |
6 / 13 |
|
0.00% |
0 / 1 |
6.50 | |||
| reset | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| getLogger | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| createLogger | |
70.59% |
12 / 17 |
|
0.00% |
0 / 1 |
12.54 | |||
| getProcessor | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| getHandler | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
6.02 | |||
| getFormatter | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Logger; |
| 8 | |
| 9 | use DateTimeZone; |
| 10 | use MediaWiki\Logger\Monolog\BufferHandler; |
| 11 | use Monolog\Formatter\FormatterInterface; |
| 12 | use Monolog\Handler\FormattableHandlerInterface; |
| 13 | use Monolog\Handler\HandlerInterface; |
| 14 | use Monolog\Handler\PsrHandler; |
| 15 | use Monolog\Handler\StreamHandler; |
| 16 | use Monolog\Level; |
| 17 | use Monolog\Logger; |
| 18 | use Psr\Log\LoggerInterface; |
| 19 | use Wikimedia\ObjectFactory\ObjectFactory; |
| 20 | |
| 21 | /** |
| 22 | * LoggerFactory service provider that creates loggers implemented by |
| 23 | * Monolog. |
| 24 | * |
| 25 | * Configured using an array of configuration data with the keys 'loggers', |
| 26 | * 'processors', 'handlers' and 'formatters'. |
| 27 | * |
| 28 | * The ['loggers']['\@default'] configuration will be used to create loggers |
| 29 | * for any channel that isn't explicitly named in the 'loggers' configuration |
| 30 | * section. |
| 31 | * |
| 32 | * Configuration will most typically be provided in the $wgMWLoggerDefaultSpi |
| 33 | * global configuration variable used by LoggerFactory to construct its |
| 34 | * default SPI provider: |
| 35 | * @code |
| 36 | * $wgMWLoggerDefaultSpi = [ |
| 37 | * 'class' => \MediaWiki\Logger\MonologSpi::class, |
| 38 | * 'args' => [ [ |
| 39 | * 'loggers' => [ |
| 40 | * '@default' => [ |
| 41 | * 'processors' => [ 'wiki', 'psr', 'pid', 'uid', 'web' ], |
| 42 | * 'handlers' => [ 'stream' ], |
| 43 | * ], |
| 44 | * 'runJobs' => [ |
| 45 | * 'processors' => [ 'wiki', 'psr', 'pid' ], |
| 46 | * 'handlers' => [ 'stream' ], |
| 47 | * ] |
| 48 | * ], |
| 49 | * 'processors' => [ |
| 50 | * 'wiki' => [ |
| 51 | * 'class' => \MediaWiki\Logger\Monolog\WikiProcessor::class, |
| 52 | * ], |
| 53 | * 'psr' => [ |
| 54 | * 'class' => \Monolog\Processor\PsrLogMessageProcessor::class, |
| 55 | * ], |
| 56 | * 'pid' => [ |
| 57 | * 'class' => \Monolog\Processor\ProcessIdProcessor::class, |
| 58 | * ], |
| 59 | * 'uid' => [ |
| 60 | * 'class' => \Monolog\Processor\UidProcessor::class, |
| 61 | * ], |
| 62 | * 'web' => [ |
| 63 | * 'class' => \Monolog\Processor\WebProcessor::class, |
| 64 | * ], |
| 65 | * ], |
| 66 | * 'handlers' => [ |
| 67 | * 'stream' => [ |
| 68 | * 'class' => \Monolog\Handler\StreamHandler::class, |
| 69 | * 'args' => [ 'path/to/your.log' ], |
| 70 | * 'formatter' => 'line', |
| 71 | * ], |
| 72 | * 'redis' => [ |
| 73 | * 'class' => \Monolog\Handler\RedisHandler::class, |
| 74 | * 'args' => [ function() { |
| 75 | * $redis = new Redis(); |
| 76 | * $redis->connect( '127.0.0.1', 6379 ); |
| 77 | * return $redis; |
| 78 | * }, |
| 79 | * 'logstash' |
| 80 | * ], |
| 81 | * 'formatter' => 'logstash', |
| 82 | * 'buffer' => true, |
| 83 | * ], |
| 84 | * 'udp2log' => [ |
| 85 | * 'class' => \MediaWiki\Logger\Monolog\LegacyHandler::class, |
| 86 | * 'args' => [ |
| 87 | * 'udp://127.0.0.1:8420/mediawiki |
| 88 | * ], |
| 89 | * 'formatter' => 'line', |
| 90 | * ], |
| 91 | * ], |
| 92 | * 'formatters' => [ |
| 93 | * 'line' => [ |
| 94 | * 'class' => \Monolog\Formatter\LineFormatter::class, |
| 95 | * ], |
| 96 | * 'logstash' => [ |
| 97 | * 'class' => \Monolog\Formatter\LogstashFormatter::class, |
| 98 | * 'args' => [ 'mediawiki', php_uname( 'n' ), null, '', 1 ], |
| 99 | * ], |
| 100 | * ], |
| 101 | * ] ], |
| 102 | * ]; |
| 103 | * @endcode |
| 104 | * |
| 105 | * @see https://github.com/Seldaek/monolog |
| 106 | * @since 1.25 |
| 107 | * @ingroup Debug |
| 108 | * @copyright © 2014 Wikimedia Foundation and contributors |
| 109 | */ |
| 110 | class MonologSpi implements Spi { |
| 111 | |
| 112 | /** |
| 113 | * @var array{loggers:LoggerInterface[],handlers:HandlerInterface[],formatters:FormatterInterface[],processors:callable[]} |
| 114 | */ |
| 115 | protected $singletons; |
| 116 | |
| 117 | /** |
| 118 | * Configuration for creating new loggers. |
| 119 | * @var array<string,array<string,array>> |
| 120 | */ |
| 121 | protected array $config = []; |
| 122 | |
| 123 | /** |
| 124 | * @param array $config Configuration data. |
| 125 | */ |
| 126 | public function __construct( array $config ) { |
| 127 | $this->mergeConfig( $config ); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Merge additional configuration data into the configuration. |
| 132 | * |
| 133 | * @since 1.26 |
| 134 | * @param array $config Configuration data. |
| 135 | */ |
| 136 | public function mergeConfig( array $config ) { |
| 137 | foreach ( $config as $key => $value ) { |
| 138 | if ( isset( $this->config[$key] ) ) { |
| 139 | $this->config[$key] = array_merge( $this->config[$key], $value ); |
| 140 | } else { |
| 141 | $this->config[$key] = $value; |
| 142 | } |
| 143 | } |
| 144 | if ( !isset( $this->config['loggers']['@default'] ) ) { |
| 145 | $this->config['loggers']['@default'] = [ |
| 146 | 'handlers' => [ '@default' ], |
| 147 | ]; |
| 148 | $this->config['handlers']['@default'] ??= [ |
| 149 | 'class' => StreamHandler::class, |
| 150 | 'args' => [ 'php://stderr', Level::Error ], |
| 151 | ]; |
| 152 | } |
| 153 | $this->reset(); |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Reset internal caches. |
| 158 | * |
| 159 | * This is public for use in unit tests. Under normal operation there should |
| 160 | * be no need to flush the caches. |
| 161 | */ |
| 162 | public function reset() { |
| 163 | $this->singletons = [ |
| 164 | 'loggers' => [], |
| 165 | 'handlers' => [], |
| 166 | 'formatters' => [], |
| 167 | 'processors' => [], |
| 168 | ]; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Get a logger instance. |
| 173 | * |
| 174 | * Creates and caches a logger instance based on configuration found in the |
| 175 | * $wgMWLoggerMonologSpiConfig global. Subsequent request for the same channel |
| 176 | * name will return the cached instance. |
| 177 | * |
| 178 | * @param string $channel Logging channel |
| 179 | * @return LoggerInterface |
| 180 | */ |
| 181 | public function getLogger( $channel ) { |
| 182 | if ( !isset( $this->singletons['loggers'][$channel] ) ) { |
| 183 | // Fallback to using the '@default' configuration if an explicit |
| 184 | // configuration for the requested channel isn't found. |
| 185 | $spec = $this->config['loggers'][$channel] ?? $this->config['loggers']['@default']; |
| 186 | |
| 187 | $monolog = $this->createLogger( $channel, $spec ); |
| 188 | $this->singletons['loggers'][$channel] = $monolog; |
| 189 | } |
| 190 | |
| 191 | return $this->singletons['loggers'][$channel]; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Create a logger. |
| 196 | * @param string $channel Logger channel |
| 197 | * @param array $spec Configuration |
| 198 | * @return LoggerInterface |
| 199 | */ |
| 200 | protected function createLogger( $channel, $spec ): LoggerInterface { |
| 201 | global $wgShowDebug, $wgDebugToolbar; |
| 202 | |
| 203 | $handlers = []; |
| 204 | if ( isset( $spec['handlers'] ) && $spec['handlers'] ) { |
| 205 | foreach ( $spec['handlers'] as $handler ) { |
| 206 | $handlers[] = $this->getHandler( $handler ); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | $processors = []; |
| 211 | if ( isset( $spec['processors'] ) ) { |
| 212 | foreach ( $spec['processors'] as $processor ) { |
| 213 | $processors[] = $this->getProcessor( $processor ); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | // Use UTC for logs instead of Monolog's default, which asks the |
| 218 | // PHP runtime, which MediaWiki sets to $wgLocaltimezone (T99581) |
| 219 | $obj = new Logger( $channel, $handlers, $processors, new DateTimeZone( 'UTC' ) ); |
| 220 | |
| 221 | if ( $wgShowDebug || $wgDebugToolbar ) { |
| 222 | $legacyLogger = new LegacyLogger( $channel ); |
| 223 | $legacyPsrHandler = new PsrHandler( $legacyLogger ); |
| 224 | $obj->pushHandler( $legacyPsrHandler ); |
| 225 | } |
| 226 | |
| 227 | if ( isset( $spec['calls'] ) ) { |
| 228 | foreach ( $spec['calls'] as $method => $margs ) { |
| 229 | $obj->$method( ...$margs ); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | return $obj; |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Create or return cached processor. |
| 238 | * @param string $name Processor name |
| 239 | * @return callable |
| 240 | */ |
| 241 | public function getProcessor( $name ) { |
| 242 | if ( !isset( $this->singletons['processors'][$name] ) ) { |
| 243 | $spec = $this->config['processors'][$name]; |
| 244 | /** @var callable $processor */ |
| 245 | $processor = ObjectFactory::getObjectFromSpec( $spec ); |
| 246 | $this->singletons['processors'][$name] = $processor; |
| 247 | } |
| 248 | return $this->singletons['processors'][$name]; |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Create or return cached handler. |
| 253 | * @param string $name Processor name |
| 254 | * @return HandlerInterface |
| 255 | */ |
| 256 | public function getHandler( $name ) { |
| 257 | if ( !isset( $this->singletons['handlers'][$name] ) ) { |
| 258 | $spec = $this->config['handlers'][$name]; |
| 259 | /** @var HandlerInterface $handler */ |
| 260 | $handler = ObjectFactory::getObjectFromSpec( $spec ); |
| 261 | if ( |
| 262 | isset( $spec['formatter'] ) && |
| 263 | $handler instanceof FormattableHandlerInterface |
| 264 | ) { |
| 265 | $handler->setFormatter( |
| 266 | $this->getFormatter( $spec['formatter'] ) |
| 267 | ); |
| 268 | } |
| 269 | if ( isset( $spec['buffer'] ) && $spec['buffer'] ) { |
| 270 | // @phan-suppress-next-line PhanTypeMismatchArgument Gets confused by FormattableHandlerInterface |
| 271 | $handler = new BufferHandler( $handler ); |
| 272 | } |
| 273 | $this->singletons['handlers'][$name] = $handler; |
| 274 | } |
| 275 | return $this->singletons['handlers'][$name]; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Create or return cached formatter. |
| 280 | * @param string $name Formatter name |
| 281 | * @return FormatterInterface |
| 282 | */ |
| 283 | public function getFormatter( $name ) { |
| 284 | if ( !isset( $this->singletons['formatters'][$name] ) ) { |
| 285 | $spec = $this->config['formatters'][$name]; |
| 286 | /** @var FormatterInterface $formatter */ |
| 287 | $formatter = ObjectFactory::getObjectFromSpec( $spec ); |
| 288 | $this->singletons['formatters'][$name] = $formatter; |
| 289 | } |
| 290 | return $this->singletons['formatters'][$name]; |
| 291 | } |
| 292 | } |