Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | n/a |
0 / 0 |
n/a |
0 / 0 |
CRAP | n/a |
0 / 0 |
|||
LoggerFactory | n/a |
0 / 0 |
n/a |
0 / 0 |
45 | n/a |
0 / 0 |
|||
addDbWriters | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
addEmailWriters | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
addFileWriters | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
addSlackWriters | n/a |
0 / 0 |
n/a |
0 / 0 |
4 | |||||
addOffice365Writers | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
hasDynamicDebug | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
configureLogger | n/a |
0 / 0 |
n/a |
0 / 0 |
14 | |||||
addDebugWriter | n/a |
0 / 0 |
n/a |
0 / 0 |
4 | |||||
addWriters | n/a |
0 / 0 |
n/a |
0 / 0 |
10 | |||||
getProxyClassName | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
__invoke | n/a |
0 / 0 |
n/a |
0 / 0 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Factory for instantiating Logger |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2017. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Error_Logging |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFind\Log; |
31 | |
32 | use Laminas\Config\Config; |
33 | use Laminas\Log\Writer\WriterInterface; |
34 | use Laminas\ServiceManager\Exception\ServiceNotCreatedException; |
35 | use Laminas\ServiceManager\Exception\ServiceNotFoundException; |
36 | use Laminas\ServiceManager\Factory\FactoryInterface; |
37 | use Psr\Container\ContainerExceptionInterface as ContainerException; |
38 | use Psr\Container\ContainerInterface; |
39 | |
40 | use function count; |
41 | use function is_array; |
42 | use function is_int; |
43 | |
44 | /** |
45 | * Factory for instantiating Logger |
46 | * |
47 | * @category VuFind |
48 | * @package Error_Logging |
49 | * @author Demian Katz <demian.katz@villanova.edu> |
50 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
51 | * @link https://vufind.org/wiki/development Wiki |
52 | * |
53 | * @codeCoverageIgnore |
54 | */ |
55 | class LoggerFactory implements FactoryInterface |
56 | { |
57 | /** |
58 | * Configure database writers. |
59 | * |
60 | * @param Logger $logger Logger object |
61 | * @param ContainerInterface $container Service manager |
62 | * @param string $config Configuration |
63 | * |
64 | * @return void |
65 | */ |
66 | protected function addDbWriters( |
67 | Logger $logger, |
68 | ContainerInterface $container, |
69 | $config |
70 | ) { |
71 | $parts = explode(':', $config); |
72 | $table_name = $parts[0]; |
73 | $error_types = $parts[1] ?? ''; |
74 | |
75 | $columnMapping = [ |
76 | 'priority' => 'priority', |
77 | 'message' => 'message', |
78 | 'logtime' => 'timestamp', |
79 | 'ident' => 'ident', |
80 | ]; |
81 | |
82 | // Make Writers |
83 | $filters = explode(',', $error_types); |
84 | $writer = new Writer\Db( |
85 | $container->get(\Laminas\Db\Adapter\Adapter::class), |
86 | $table_name, |
87 | $columnMapping |
88 | ); |
89 | $this->addWriters($logger, $writer, $filters); |
90 | } |
91 | |
92 | /** |
93 | * Configure email writers. |
94 | * |
95 | * @param Logger $logger Logger object |
96 | * @param ContainerInterface $container Service manager |
97 | * @param Config $config Configuration |
98 | * |
99 | * @return void |
100 | */ |
101 | protected function addEmailWriters( |
102 | Logger $logger, |
103 | ContainerInterface $container, |
104 | Config $config |
105 | ) { |
106 | // Set up the logger's mailer to behave consistently with VuFind's |
107 | // general mailer: |
108 | $parts = explode(':', $config->Logging->email); |
109 | $email = $parts[0]; |
110 | $error_types = $parts[1] ?? ''; |
111 | |
112 | // use smtp |
113 | $mailer = $container->get(\VuFind\Mailer\Mailer::class); |
114 | $msg = $mailer->getNewMessage() |
115 | ->addFrom($config->Site->email) |
116 | ->addTo($email) |
117 | ->setSubject('VuFind Log Message'); |
118 | |
119 | // Make Writers |
120 | $filters = explode(',', $error_types); |
121 | $writer = new Writer\Mail($msg, $mailer->getTransport()); |
122 | $this->addWriters($logger, $writer, $filters); |
123 | } |
124 | |
125 | /** |
126 | * Configure File writers. |
127 | * |
128 | * @param Logger $logger Logger object |
129 | * @param string $config Configuration |
130 | * |
131 | * @return void |
132 | */ |
133 | protected function addFileWriters(Logger $logger, $config) |
134 | { |
135 | // Make sure to use only the last ':' after second character to avoid trouble |
136 | // with Windows drive letters (e.g. "c:\something\logfile:error-5") |
137 | $pos = strrpos($config, ':', 2); |
138 | if ($pos > 0) { |
139 | $file = substr($config, 0, $pos); |
140 | $error_types = substr($config, $pos + 1); |
141 | } else { |
142 | $file = $config; |
143 | $error_types = ''; |
144 | } |
145 | |
146 | // Make Writers |
147 | $filters = explode(',', $error_types); |
148 | $writer = new Writer\Stream($file); |
149 | $this->addWriters($logger, $writer, $filters); |
150 | } |
151 | |
152 | /** |
153 | * Configure Slack writers. |
154 | * |
155 | * @param Logger $logger Logger object |
156 | * @param ContainerInterface $container Service manager |
157 | * @param Config $config Configuration |
158 | * |
159 | * @return void |
160 | */ |
161 | protected function addSlackWriters( |
162 | Logger $logger, |
163 | ContainerInterface $container, |
164 | Config $config |
165 | ) { |
166 | $options = []; |
167 | // Get config |
168 | [$channel, $error_types] = explode(':', $config->Logging->slack); |
169 | if ($error_types == null) { |
170 | $error_types = $channel; |
171 | $channel = null; |
172 | } |
173 | if ($channel) { |
174 | $options['channel'] = $channel; |
175 | } |
176 | if (isset($config->Logging->slackname)) { |
177 | $options['name'] = $config->Logging->slackname; |
178 | } |
179 | $filters = explode(',', $error_types); |
180 | // Make Writers |
181 | $writer = new Writer\Slack( |
182 | $config->Logging->slackurl, |
183 | $container->get(\VuFindHttp\HttpService::class)->createClient(), |
184 | $options |
185 | ); |
186 | $writer->setContentType('application/json'); |
187 | $formatter = new \Laminas\Log\Formatter\Simple( |
188 | '*%priorityName%*: %message%' |
189 | ); |
190 | $writer->setFormatter($formatter); |
191 | $this->addWriters($logger, $writer, $filters); |
192 | } |
193 | |
194 | /** |
195 | * Configure Office365 writers. |
196 | * |
197 | * @param Logger $logger Logger object |
198 | * @param ContainerInterface $container Service manager |
199 | * @param Config $config Configuration |
200 | * |
201 | * @return void |
202 | */ |
203 | protected function addOffice365Writers( |
204 | Logger $logger, |
205 | ContainerInterface $container, |
206 | Config $config |
207 | ) { |
208 | $options = []; |
209 | // Get config |
210 | $error_types = $config->Logging->office365; |
211 | if (isset($config->Logging->office365_title)) { |
212 | $options['title'] = $config->Logging->office365_title; |
213 | } |
214 | $filters = explode(',', $error_types); |
215 | // Make Writers |
216 | $writer = new Writer\Office365( |
217 | $config->Logging->office365_url, |
218 | $container->get(\VuFindHttp\HttpService::class)->createClient(), |
219 | $options |
220 | ); |
221 | $writer->setContentType('application/json'); |
222 | $formatter = new \Laminas\Log\Formatter\Simple( |
223 | '*%priorityName%*: %message%' |
224 | ); |
225 | $writer->setFormatter($formatter); |
226 | $this->addWriters($logger, $writer, $filters); |
227 | } |
228 | |
229 | /** |
230 | * Is dynamic debug mode enabled? |
231 | * |
232 | * @param ContainerInterface $container Service manager |
233 | * |
234 | * @return bool |
235 | */ |
236 | protected function hasDynamicDebug(ContainerInterface $container): bool |
237 | { |
238 | // Query parameters do not apply in console mode; if we do have a debug |
239 | // query parameter, and the appropriate permission is set, activate dynamic |
240 | // debug: |
241 | if ( |
242 | PHP_SAPI !== 'cli' |
243 | && $container->get('Request')->getQuery()->get('debug') |
244 | ) { |
245 | return $container->get(\LmcRbacMvc\Service\AuthorizationService::class) |
246 | ->isGranted('access.DebugMode'); |
247 | } |
248 | return false; |
249 | } |
250 | |
251 | /** |
252 | * Set configuration |
253 | * |
254 | * @param ContainerInterface $container Service manager |
255 | * @param Logger $logger Logger to configure |
256 | * |
257 | * @return void |
258 | */ |
259 | protected function configureLogger(ContainerInterface $container, Logger $logger) |
260 | { |
261 | $config = $container->get(\VuFind\Config\PluginManager::class) |
262 | ->get('config'); |
263 | |
264 | // Add a no-op writer so fatal errors are not triggered if log messages are |
265 | // sent during the initialization process. |
266 | $noOpWriter = new \Laminas\Log\Writer\Noop(); |
267 | $logger->addWriter($noOpWriter); |
268 | |
269 | // DEBUGGER |
270 | if (!$config->System->debug == false || $this->hasDynamicDebug($container)) { |
271 | $this->addDebugWriter($logger, $config->System->debug); |
272 | } |
273 | |
274 | // Activate database logging, if applicable: |
275 | if (isset($config->Logging->database)) { |
276 | $this->addDbWriters($logger, $container, $config->Logging->database); |
277 | } |
278 | |
279 | // Activate file logging, if applicable: |
280 | if (isset($config->Logging->file)) { |
281 | $this->addFileWriters($logger, $config->Logging->file); |
282 | } |
283 | |
284 | // Activate email logging, if applicable: |
285 | if (isset($config->Logging->email)) { |
286 | $this->addEmailWriters($logger, $container, $config); |
287 | } |
288 | |
289 | // Activate Office365 logging, if applicable: |
290 | if ( |
291 | isset($config->Logging->office365) |
292 | && isset($config->Logging->office365_url) |
293 | ) { |
294 | $this->addOffice365Writers($logger, $container, $config); |
295 | } |
296 | |
297 | // Activate slack logging, if applicable: |
298 | if (isset($config->Logging->slack) && isset($config->Logging->slackurl)) { |
299 | $this->addSlackWriters($logger, $container, $config); |
300 | } |
301 | |
302 | // We're done now -- clean out the no-op writer if any other writers |
303 | // are found. |
304 | if (count($logger->getWriters()) > 1) { |
305 | $logger->removeWriter($noOpWriter); |
306 | } |
307 | |
308 | // Add ReferenceId processor, if applicable: |
309 | if ($referenceId = $config->Logging->reference_id ?? false) { |
310 | if ('username' === $referenceId) { |
311 | $authManager = $container->get(\VuFind\Auth\Manager::class); |
312 | if ($user = $authManager->getUserObject()) { |
313 | $processor = new \Laminas\Log\Processor\ReferenceId(); |
314 | $processor->setReferenceId($user->username); |
315 | $logger->addProcessor($processor); |
316 | } |
317 | } |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * Add the standard debug stream writer. |
323 | * |
324 | * @param Logger $logger Logger object |
325 | * @param bool|int $debug Debug mode/level |
326 | * |
327 | * @return void |
328 | */ |
329 | protected function addDebugWriter(Logger $logger, $debug) |
330 | { |
331 | // Only add debug writer ONCE! |
332 | static $hasDebugWriter = false; |
333 | if ($hasDebugWriter) { |
334 | return; |
335 | } |
336 | |
337 | $hasDebugWriter = true; |
338 | $writer = new Writer\Stream('php://output'); |
339 | $formatter = new \Laminas\Log\Formatter\Simple( |
340 | PHP_SAPI === 'cli' |
341 | ? '%timestamp% %priorityName%: %message%' |
342 | : '<pre>%timestamp% %priorityName%: %message%</pre>' . PHP_EOL |
343 | ); |
344 | $writer->setFormatter($formatter); |
345 | $level = (is_int($debug) ? $debug : '5'); |
346 | $this->addWriters( |
347 | $logger, |
348 | $writer, |
349 | "debug-$level,notice-$level,error-$level,alert-$level" |
350 | ); |
351 | } |
352 | |
353 | /** |
354 | * Applies an array of filters to a writer |
355 | * |
356 | * Filter keys: alert, error, notice, debug |
357 | * |
358 | * @param Logger $logger Logger object |
359 | * @param WriterInterface $writer The writer to apply the |
360 | * filters to |
361 | * @param string|array $filters An array or comma-separated |
362 | * string of |
363 | * logging levels |
364 | * |
365 | * @return void |
366 | */ |
367 | protected function addWriters(Logger $logger, WriterInterface $writer, $filters) |
368 | { |
369 | if (!is_array($filters)) { |
370 | $filters = explode(',', $filters); |
371 | } |
372 | |
373 | foreach ($filters as $filter) { |
374 | $parts = explode('-', $filter); |
375 | $priority = $parts[0]; |
376 | $verbosity = $parts[1] ?? false; |
377 | |
378 | // VuFind's configuration provides four priority options, each |
379 | // combining two of the standard Laminas levels. |
380 | switch (trim($priority)) { |
381 | case 'debug': |
382 | // Set static flag indicating that debug is turned on: |
383 | $logger->debugNeeded(true); |
384 | |
385 | $max = Logger::INFO; // Informational: informational messages |
386 | $min = Logger::DEBUG; // Debug: debug messages |
387 | break; |
388 | case 'notice': |
389 | $max = Logger::WARN; // Warning: warning conditions |
390 | $min = Logger::NOTICE;// Notice: normal but significant condition |
391 | break; |
392 | case 'error': |
393 | $max = Logger::CRIT; // Critical: critical conditions |
394 | $min = Logger::ERR; // Error: error conditions |
395 | break; |
396 | case 'alert': |
397 | $max = Logger::EMERG; // Emergency: system is unusable |
398 | $min = Logger::ALERT; // Alert: action must be taken immediately |
399 | break; |
400 | default: |
401 | // INVALID FILTER, so skip it. We must continue 2 levels, so we |
402 | // continue the foreach loop instead of just breaking the switch. |
403 | continue 2; |
404 | } |
405 | |
406 | // Clone the submitted writer since we'll need a separate instance of the |
407 | // writer for each selected priority level. |
408 | $newWriter = clone $writer; |
409 | |
410 | // verbosity |
411 | if ($verbosity) { |
412 | if (method_exists($newWriter, 'setVerbosity')) { |
413 | $newWriter->setVerbosity($verbosity); |
414 | } else { |
415 | throw new \Exception( |
416 | $newWriter::class . ' does not support verbosity.' |
417 | ); |
418 | } |
419 | } |
420 | |
421 | // filtering -- only log messages between the min and max priority levels |
422 | $filter1 = new \Laminas\Log\Filter\Priority($min, '<='); |
423 | $filter2 = new \Laminas\Log\Filter\Priority($max, '>='); |
424 | $newWriter->addFilter($filter1); |
425 | $newWriter->addFilter($filter2); |
426 | |
427 | // add the writer |
428 | $logger->addWriter($newWriter); |
429 | } |
430 | } |
431 | |
432 | /** |
433 | * Get proxy class to instantiate from the requested class name |
434 | * |
435 | * @param string $requestedName Service being created |
436 | * |
437 | * @return string |
438 | */ |
439 | protected function getProxyClassName(string $requestedName): string |
440 | { |
441 | $className = $requestedName . 'Proxy'; |
442 | // Fall back to default if the class doesn't exist: |
443 | if (!class_exists($className)) { |
444 | return LoggerProxy::class; |
445 | } |
446 | return $className; |
447 | } |
448 | |
449 | /** |
450 | * Create an object |
451 | * |
452 | * @param ContainerInterface $container Service manager |
453 | * @param string $requestedName Service being created |
454 | * @param null|array $options Extra options (optional) |
455 | * |
456 | * @return object |
457 | * |
458 | * @throws ServiceNotFoundException if unable to resolve the service. |
459 | * @throws ServiceNotCreatedException if an exception is raised when |
460 | * creating a service. |
461 | * @throws ContainerException&\Throwable if any other error occurs |
462 | */ |
463 | public function __invoke( |
464 | ContainerInterface $container, |
465 | $requestedName, |
466 | array $options = null |
467 | ) { |
468 | if (!empty($options)) { |
469 | throw new \Exception('Unexpected options passed to factory.'); |
470 | } |
471 | |
472 | // Construct the logger as a lazy loading proxy so that the object is not |
473 | // instantiated until it is called. This helps break potential circular |
474 | // dependencies with other services. |
475 | $callback = function (&$wrapped, $proxy) use ($container, $requestedName) { |
476 | // Now build the actual service: |
477 | $wrapped = new $requestedName( |
478 | $container->get(\VuFind\Net\UserIpReader::class) |
479 | ); |
480 | $this->configureLogger($container, $wrapped); |
481 | }; |
482 | |
483 | $proxyClass = $this->getProxyClassName($requestedName); |
484 | return new $proxyClass($callback); |
485 | } |
486 | } |