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