Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.69% covered (warning)
79.69%
51 / 64
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
MonologSpi
79.69% covered (warning)
79.69%
51 / 64
62.50% covered (warning)
62.50%
5 / 8
34.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeConfig
46.15% covered (danger)
46.15%
6 / 13
0.00% covered (danger)
0.00%
0 / 1
6.50
 reset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getLogger
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 createLogger
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
12.54
 getProcessor
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getHandler
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 getFormatter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Logger;
8
9use DateTimeZone;
10use MediaWiki\Logger\Monolog\BufferHandler;
11use Monolog\Formatter\FormatterInterface;
12use Monolog\Handler\FormattableHandlerInterface;
13use Monolog\Handler\HandlerInterface;
14use Monolog\Handler\PsrHandler;
15use Monolog\Handler\StreamHandler;
16use Monolog\Level;
17use Monolog\Logger;
18use Psr\Log\LoggerInterface;
19use 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 */
110class 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}