Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.19% |
43 / 64 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
MonologSpi | |
67.19% |
43 / 64 |
|
50.00% |
4 / 8 |
55.70 | |
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 | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
7.33 | |||
getFormatter | |
0.00% |
0 / 5 |
|
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 | |
21 | namespace MediaWiki\Logger; |
22 | |
23 | use DateTimeZone; |
24 | use MediaWiki\Logger\Monolog\BufferHandler; |
25 | use Monolog\Formatter\FormatterInterface; |
26 | use Monolog\Handler\FormattableHandlerInterface; |
27 | use Monolog\Handler\HandlerInterface; |
28 | use Monolog\Handler\PsrHandler; |
29 | use Monolog\Handler\StreamHandler; |
30 | use Monolog\Logger; |
31 | use Psr\Log\LoggerInterface; |
32 | use 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 | */ |
123 | class 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 | } |