Passed
Push — master ( d979ac...b22592 )
by Fabio
05:10
created

TLogRoute::filterLogs()   C

Complexity

Conditions 12
Paths 82

Size

Total Lines 55
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
cc 12
eloc 42
nc 82
nop 1
dl 0
loc 55
ccs 0
cts 0
cp 0
crap 156
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * TLogRouter, TLogRoute, TFileLogRoute, TEmailLogRoute class file
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Util;
11
12
use Prado\Exceptions\TConfigurationException;
13
use Prado\Prado;
14
use Prado\TPropertyValue;
15
16
/**
17
 * TLogRoute class.
18
 *
19
 * TLogRoute is the base class for all log route classes.
20
 * A log route object retrieves log messages from a logger and sends it
21
 * somewhere, such as files, emails.
22
 * The messages being retrieved may be filtered first before being sent
23
 * to the destination. The filters include log level filter and log category filter.
24
 *
25
 * To specify level filter, set {@link setLevels Levels} property,
26
 * which takes a string of comma-separated desired level names (e.g. 'Error, Debug').
27
 * To specify category filter, set {@link setCategories Categories} property,
28
 * which takes a string of comma-separated desired category names (e.g. 'Prado\Web, Prado\IO').
29
 *
30
 * The categories filter can use '!' or '~', e.g. '!Prado\Web\UI' or '~Prado\Web\UI',
31
 * to exclude categories.  Added 4.2.3.
32
 *
33
 * Level filter and category filter are combinational, i.e., only messages
34
 * satisfying both filter conditions will they be returned.
35
 *
36
 * @author Qiang Xue <[email protected]>
37
 * @author Brad Anderson <[email protected]>
38
 * @since 3.0
39
 */
40
abstract class TLogRoute extends \Prado\TApplicationComponent
41
{
42
	/**
43
	 * @var array lookup table for level names
44
	 */
45
	protected static $_levelNames = [
46
		TLogger::DEBUG => 'Debug',
47
		TLogger::INFO => 'Info',
48
		TLogger::NOTICE => 'Notice',
49
		TLogger::WARNING => 'Warning',
50
		TLogger::ERROR => 'Error',
51
		TLogger::ALERT => 'Alert',
52
		TLogger::FATAL => 'Fatal',
53
		// @ since 4.2.3:
54
		TLogger::PROFILE => 'Profile',
55
		TLogger::PROFILE_BEGIN => 'Profile Begin',
56
		TLogger::PROFILE_END => 'Profile End',
57
	];
58
	/**
59
	 * @var array lookup table for level values
60
	 */
61
	protected static $_levelValues = [
62
		'debug' => TLogger::DEBUG,
63
		'info' => TLogger::INFO,
64
		'notice' => TLogger::NOTICE,
65
		'warning' => TLogger::WARNING,
66
		'error' => TLogger::ERROR,
67
		'alert' => TLogger::ALERT,
68
		'fatal' => TLogger::FATAL,
69
		// @ since 4.2.3:
70
		'profile' => TLogger::PROFILE,
71
		'profile begin' => TLogger::PROFILE_BEGIN_SELECT,
72
		'profile end' => TLogger::PROFILE_END_SELECT,
73
	];
74
	/**
75
	 * @var array of stored route logs
76
	 */
77
	protected array $_logs = [];
78
	/**
79
	 * @var int the number of logs to save before processing, default 1000
80
	 * @since 4.2.3
81
	 */
82
	private ?int $_processInterval = 1000;
83
	/**
84
	 * @var int log level filter (bits), default null.
85
	 */
86
	private ?int $_levels = null;
87
	/**
88
	 * @var ?array log category filter
89
	 */
90
	private ?array $_categories = [];
91
	/**
92
	 * @var bool|callable Whether the route is enabled, default true
93
	 * @since 4.2.3
94
	 */
95
	private mixed $_enabled = true;
96
	/**
97
	 * @var ?callable The prefix callable
98
	 * @since 4.2.3
99
	 */
100
	private mixed $_prefix = null;
101
	/**
102
	 * @var bool display the time with subseconds, default false.
103
	 * @since 4.2.3
104
	 */
105
	private bool $_displaySubSeconds = false;
106
	/**
107
	 * @var float the maximum delta for the log items, default 0.
108
	 * @since 4.2.3
109
	 */
110
	private float $_maxDelta = 0;
111
	/**
112
	 * @var float The computed total time of the logs, default 0.
113
	 * @since 4.2.3
114
	 */
115
	private float $_totalTime = 0;
116
117
	/**
118
	 * Initializes the route.
119
	 * @param \Prado\Xml\TXmlElement $config configurations specified in {@link TLogRouter}.
120
	 */
121
	public function init($config)
0 ignored issues
show
Unused Code introduced by
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

121
	public function init(/** @scrutinizer ignore-unused */ $config)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
122
	{
123
	}
124
125
	/**
126
	 *
127
	 * @return int the stored logs for the route.
128
	 */
129
	public function getLogCount(): int
130
	{
131
		return count($this->_logs);
132
	}
133
134
	/**
135
	 * @return ?int log level filter
136
	 */
137
	public function getLevels(): ?int
138
	{
139
		return $this->_levels;
140
	}
141
142
	/**
143
	 * @param int|string $levels integer log level filter (in bits). If the value is
144
	 * a string, it is assumed to be comma-separated level names. Valid level names
145
	 * include 'Debug', 'Info', 'Notice', 'Warning', 'Error', 'Alert', 'Fatal', 'Profile,
146
	 * 'Profile Begin', and 'Profile End'.
147
	 * @return static The current object.
148
	 */
149
	public function setLevels($levels): static
150
	{
151
		if (is_int($levels)) {
152
			$invalidLevels = ~array_reduce(static::$_levelValues, function ($levels, $level) {
153
				return $levels | $level;
154
			}, 0);
155
			if ($invalidLevels & $levels) {
156
				throw new TConfigurationException('logroute_bad_level', '0x' . dechex($levels));
157
			}
158
			$this->_levels = $levels;
159
		} else {
160
			$this->_levels = null;
161
			if (!is_array($levels)) {
0 ignored issues
show
introduced by
The condition is_array($levels) is always false.
Loading history...
162
				$levels = preg_split('/[|,]/', $levels);
163
			}
164
			foreach ($levels as $level) {
165
				$level = str_replace('_', ' ', strtolower(trim($level)));
166
				if (isset(static::$_levelValues[$level])) {
167
					$this->_levels |= static::$_levelValues[$level];
168
				}
169
			}
170
		}
171
172
		return $this;
173
	}
174
175
	/**
176
	 * @return array list of categories to be looked for
177
	 */
178
	public function getCategories()
179
	{
180
		return $this->_categories;
181
	}
182
183
	/**
184
	 * @param array|string $categories list of categories to be looked for. If the value is a string,
185
	 * it is assumed to be comma-separated category names.
186
	 * @return static The current object.
187
	 */
188
	public function setCategories($categories): static
189
	{
190
		if (is_array($categories)) {
191
			$this->_categories = $categories;
192
		} else {
193
			$this->_categories = null;
194
			foreach (explode(',', $categories) as $category) {
195
				if (($category = trim($category)) !== '') {
196
					$this->_categories[] = $category;
197
				}
198
			}
199
		}
200
201
		return $this;
202
	}
203
204
	/**
205
	 * @param int $level level value
206
	 * @return string level name
207
	 */
208
	protected function getLevelName($level)
209
	{
210
		return self::$_levelNames[$level] ?? 'Unknown';
211
	}
212
213
	/**
214
	 * @param string $level level name
215
	 * @return int level value
216
	 */
217
	protected function getLevelValue($level)
218
	{
219
		return static::$_levelValues[$level] ?? 0;
220
	}
221
222
	/**
223
	 * @return bool Is the log enabled. Defaults is true.
224
	 * @since 4.2.3
225
	 */
226
	public function getEnabled(): bool
227
	{
228
		if (is_callable($this->_enabled)) {
229
			return call_user_func($this->_enabled, $this);
230
		}
231
		return $this->_enabled;
232
	}
233
234
	/**
235
	 * This can be a boolean or a callable in the format:
236
	 * ```php
237
	 *		$route->setEnabled(
238
	 *				function(TLogRoute $route):bool {
239
	 * 					return !Prado::getUser()?->getIsGuest()
240
	 *				}
241
	 *			);
242
	 * ```
243
	 *
244
	 * @param bool|callable $value Whether the route is enabled.
245
	 * @return static $this
246
	 * @since 4.2.3
247
	 */
248
	public function setEnabled($value): static
249
	{
250
		if (!is_callable($value)) {
251
			$value = TPropertyValue::ensureBoolean($value);
252
		}
253
		$this->_enabled = $value;
254
255
		return $this;
256
	}
257
258
	/**
259
	 * @return int The number of logs before they are processed by the route.
260
	 * @since 4.2.3
261
	 */
262
	public function getProcessInterval(): int
263
	{
264
		return $this->_processInterval;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_processInterval could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
265
	}
266
267
	/**
268
	 * @param int $value The number of logs before they are processed by the route.
269
	 * @since 4.2.3
270
	 */
271
	public function setProcessInterval($value): static
272
	{
273
		$this->_processInterval = TPropertyValue::ensureInteger($value);
274
275
		return $this;
276
	}
277
278
	/**
279
	 * @return callable Changes the prefix.
280
	 * @since 4.2.3
281
	 */
282
	public function getPrefixCallback(): mixed
283
	{
284
		return $this->_prefix;
285
	}
286
287
	/**
288
	 * This is the application callback for changing the prefix in the format of:
289
	 * ```php
290
	 *		$route->setPrefixCallback(function(array $log, string $prefix) {
291
	 * 				return $prefix . '[my data]';
292
	 *			});
293
	 * ```
294
	 * @param callable $value Changes the prefix.
295
	 * @return static The current object.
296
	 * @since 4.2.3
297
	 */
298
	public function setPrefixCallback(mixed $value): static
299
	{
300
		if (!is_callable($value)) {
301
			throw new TConfigurationException('logroute_bad_prefix_callback');
302
		}
303
		$this->_prefix = $value;
304
305
		return $this;
306
	}
307
308
	/**
309
	 * @return bool display the subseconds with the time during logging.
310
	 * @since 4.2.3
311
	 */
312
	public function getDisplaySubSeconds(): bool
313
	{
314
		return $this->_displaySubSeconds;
315
	}
316
317
	/**
318
	 * @param bool|string $value display the subseconds with the time during logging.
319
	 * @since 4.2.3
320
	 */
321
	public function setDisplaySubSeconds($value): static
322
	{
323
		$this->_displaySubSeconds = TPropertyValue::ensureBoolean($value);
324
325
		return $this;
326
	}
327
328
	/**
329
	 * Formats a log message given different fields.
330
	 * @param array $log The log to format
331
	 * @return string formatted message
332
	 */
333
	public function formatLogMessage(array $log): string
334
	{
335
		$prefix = $this->getLogPrefix($log);
336
		$traces = [];
337
		if (!is_string($log[TLogger::LOG_MESSAGE])) {
338
			if ($log[TLogger::LOG_MESSAGE] instanceof \Exception || $log[TLogger::LOG_MESSAGE] instanceof \Throwable) {
339
				$log[TLogger::LOG_MESSAGE] = (string) $log[TLogger::LOG_MESSAGE];
340
			} else {
341
				$log[TLogger::LOG_MESSAGE] = \Prado\Util\TVarDumper::dump($log[TLogger::LOG_MESSAGE]);
342
			}
343
		}
344
		if (!is_string($log[TLogger::LOG_MESSAGE])) {
345
			if ($log[TLogger::LOG_MESSAGE] instanceof \Exception || $log[TLogger::LOG_MESSAGE] instanceof \Throwable) {
346
				$log[TLogger::LOG_MESSAGE] = (string) $log[TLogger::LOG_MESSAGE];
347
			} else {
348
				$log[TLogger::LOG_MESSAGE] = \Prado\Util\TVarDumper::dump($log[TLogger::LOG_MESSAGE]);
349
			}
350
		}
351
		if (isset($log[TLogger::LOG_TRACES])) {
352
			$traces = array_map(fn ($trace) => "in {$trace['file']}:{$trace['line']}", $log[TLogger::LOG_TRACES]);
353
		}
354
		return $this->getTime($log[TLogger::LOG_TIME]) . ' ' . $prefix . '[' . $this->getLevelName($log[TLogger::LOG_LEVEL]) . '] [' . $log[TLogger::LOG_CATEGORY] . '] ' . $log[TLogger::LOG_MESSAGE]
355
			. (empty($traces) ? '' : "\n    " . implode("\n    ", $traces));
356
	}
357
358
	/**
359
	 * @param array $log
360
	 * @return string The prefix for the message
361
	 * @since 4.2.3
362
	 */
363
	public function getLogPrefix(array $log): string
364
	{
365
		if (($app = Prado::getApplication()) === null) {
366
			if ($ip = $_SERVER['REMOTE_ADDR'] ?? null) {
367
				$result = '[' . $ip . ']';
368
				if ($this->_prefix !== null) {
369
					return call_user_func($this->_prefix, $log, $result);
370
				}
371
				return $result;
372
			}
373
			return '';
374
		}
375
376
		$ip = $app->getRequest()->getUserHostAddress() ?? '-';
377
378
		$user = $app->getUser();
379
		if ($user && ($name = $user->getName())) {
380
			$userName = $name;
381
		} else {
382
			$userName = '-';
383
		}
384
385
		$session = $app->getSession();
386
		$sessionID = ($session && $session->getIsStarted()) ? $session->getSessionID() : '-';
387
388
		$result = "[{$ip}][{$userName}][{$sessionID}]";
389
		if ($log[TLogger::LOG_PID] !== getmypid()) {
390
			$result .= '[pid:' . $log[TLogger::LOG_PID] . ']';
391
		}
392
		if ($this->_prefix !== null) {
393
			return call_user_func($this->_prefix, $log, $result);
394
		}
395
		return $result;
396
	}
397
398
	/**
399
	 * @param float $timestamp The timestamp to format
400
	 * @return string The formatted time
401
	 * @since 4.2.3
402
	 */
403
	protected function getTime(float $timestamp): string
404
	{
405
		$parts = explode('.', sprintf('%F', $timestamp));
406
407
		return date('Y-m-d H:i:s', $parts[0]) . ($this->_displaySubSeconds ? ('.' . $parts[1]) : '');
0 ignored issues
show
Bug introduced by
$parts[0] of type string is incompatible with the type integer|null expected by parameter $timestamp of date(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

407
		return date('Y-m-d H:i:s', /** @scrutinizer ignore-type */ $parts[0]) . ($this->_displaySubSeconds ? ('.' . $parts[1]) : '');
Loading history...
408
	}
409
410
	/**
411
	 * Given $normalizedTime between 0 and 1 will produce an associated color.
412
	 * Lowest values (~0) are black, then blue, green, yellow, orange, and red at 1.
413
	 * @since 4.2.3
414
	 * @param float $normalizedTime
415
	 * @return array [red, green, blue] values for the color of the log.
416
	 */
417
	public static function getLogColor(float $normalizedTime): array
418
	{
419
		return [
420
			255 * exp(-pow(2.5 * ($normalizedTime - 1), 4)),
421
			204 * exp(-pow(3.5 * ($normalizedTime - 0.5), 4)),
422
			255 * exp(-pow(10 * ($normalizedTime - 0.13), 4)),
423
		];
424
	}
425
426
	/**
427
	 * This filters the logs to calculate the delta and total times.  It also calculates
428
	 * the Profile times based upon the begin and end logged profile logs
429
	 * @param array &$logs The logs to filter.
430
	 * array(
431
	 *   [0] => message
432
	 *   [1] => level
433
	 *   [2] => category
434
	 *   [3] => timestamp (by microtime(true), float number)
435
	 *   [4] => memory in bytes
436
	 *   [5] => control client id
437
	 * 		@ since 4.2.3:
438
	 *   [6] => traces, when configured
439
	 *   [7] => process id)
440
	 * @since 4.2.3
441
	 */
442
	public function filterLogs(&$logs)
443
	{
444
		$next = [];
445
		$nextNext = [];
446
447
		foreach(array_reverse(array_keys($logs)) as $key) {
448
			$next[$key] = $nextNext[$logs[$key][TLogger::LOG_PID]] ?? null;
449
			$nextNext[$logs[$key][TLogger::LOG_PID]] = $key;
450
		}
451
		unset($nextNext);
452
453
		$profile = [];
454
		$profileLast = [];
455
		$profileTotal = [];
456
		$startTime = $_SERVER["REQUEST_TIME_FLOAT"];
457
		foreach(array_keys($logs) as $key) {
458
			if (isset($next[$key])) {
459
				$logs[$key]['delta'] = $logs[$next[$key]][TLogger::LOG_TIME] - $logs[$key][TLogger::LOG_TIME];
460
				$total = $logs[$key]['total'] = $logs[$key][TLogger::LOG_TIME] - $startTime;
461
			} else {
462
				$logs[$key]['delta'] = '?';
463
				$total = $logs[$key]['total'] = $logs[$key][TLogger::LOG_TIME] - $startTime;
464
			}
465
			if ($total > $this->_totalTime) {
466
				$this->_totalTime = $total;
467
			}
468
			if(($logs[$key][TLogger::LOG_LEVEL] & TLogger::PROFILE_BEGIN) === TLogger::PROFILE_BEGIN) {
469
				$profileToken = $logs[$key][TLogger::LOG_MESSAGE] . $logs[$key][TLogger::LOG_PID];
470
				$profile[$profileToken] = $logs[$key];
471
				$profileLast[$profileToken] = false;
472
				$profileTotal[$profileToken] ??= 0;
473
				$logs[$key]['delta'] = 0;
474
				$logs[$key]['total'] = 0;
475
				$logs[$key][TLogger::LOG_MESSAGE] = 'Profile Begin: ' . $logs[$key][TLogger::LOG_MESSAGE];
476
477
			} elseif(($logs[$key][TLogger::LOG_LEVEL] & TLogger::PROFILE_END) === TLogger::PROFILE_END) {
478
				$profileToken = $logs[$key][TLogger::LOG_MESSAGE] . $logs[$key][TLogger::LOG_PID];
479
				if (isset($profile[$profileToken])) {
480
					if ($profileLast[$profileToken] !== false) {
481
						$delta = $logs[$key][TLogger::LOG_TIME] - $profileLast[$profileToken][TLogger::LOG_TIME];
482
					} else {
483
						$delta = $logs[$key][TLogger::LOG_TIME] - $profile[$profileToken][TLogger::LOG_TIME];
484
					}
485
					$profileTotal[$profileToken] += $delta;
486
					$logs[$key]['delta'] = $delta;
487
					$logs[$key]['total'] = $profileTotal[$profileToken];
488
				}
489
				$profileLast[$profileToken] = $logs[$key];
490
				$logs[$key][TLogger::LOG_MESSAGE] = 'Profile End: ' . $logs[$key][TLogger::LOG_MESSAGE];
491
			}
492
			if (is_numeric($logs[$key]['delta']) && ($this->_maxDelta === null || $logs[$key]['delta'] > $this->_maxDelta)) {
493
				$this->_maxDelta = $logs[$key]['delta'];
494
			}
495
		}
496
		$logs = array_values(array_filter($logs, fn ($l): bool => !($l[TLogger::LOG_LEVEL] & TLogger::LOGGED)));
497
	}
498
499
	/**
500
	 * Retrieves log messages from logger to log route specific destination.
501
	 * @param TLogger $logger logger instance
502
	 * @param bool $final is the final collection of logs
503
	 */
504
	public function collectLogs(null|bool|TLogger $logger = null, bool $final = false)
505
	{
506
		if (is_bool($logger)) {
507
			$final = $logger;
508
			$logger = null;
509
		}
510
		if ($logger) {
511
			$logs = $logger->getLogs($this->getLevels(), $this->getCategories());
512
			$this->filterLogs($logs);
513
			$this->_logs = array_merge($this->_logs, $logs);
514
		}
515
		$count = count($this->_logs);
516
		$final |= $this instanceof IOutputLogRoute;
517
		if ($count > 0 && ($final || $this->_processInterval > 0 && $count >= $this->_processInterval)) {
518
			$logs = $this->_logs;
519
			$meta = ['maxdelta' => $this->_maxDelta, 'total' => $this->_totalTime] ;
520
			$this->_logs = [];
521
			$this->_maxDelta = 0;
522
			$this->_totalTime = 0;
523
			$saved = $this->_processInterval;
524
			$this->_processInterval = 0;
525
			$this->processLogs($logs, $final, $meta);
0 ignored issues
show
Bug introduced by
$final of type integer is incompatible with the type boolean expected by parameter $final of Prado\Util\TLogRoute::processLogs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

525
			$this->processLogs($logs, /** @scrutinizer ignore-type */ $final, $meta);
Loading history...
526
			$this->_processInterval = $saved;
527
		}
528
	}
529
530
	/**
531
	 * Processes log messages and sends them to specific destination.
532
	 * Derived child classes must implement this method.
533
	 * @param array $logs list of messages.  Each array elements represents one message
534
	 * with the following structure:
535
	 * array(
536
	 *   [0] => message
537
	 *   [1] => level
538
	 *   [2] => category
539
	 *   [3] => timestamp (by microtime(time), float number)
540
	 *   [4] => memory in bytes
541
	 *   [5] => control client id
542
	 *     @ since 4.2.3:
543
	 *   [6] => traces, when configured
544
	 *   [7] => process id)
545
	 * @param bool $final
546
	 * @param array $meta
547
	 */
548
	abstract protected function processLogs(array $logs, bool $final, array $meta);
549
}
550