TLogRoute   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 509
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 182
dl 0
loc 509
ccs 0
cts 58
cp 0
rs 2.96
c 1
b 0
f 0
wmc 68

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getTime() 0 5 2
A getDisplaySubSeconds() 0 3 1
B getLogPrefix() 0 33 10
C filterLogs() 0 55 12
A setProcessInterval() 0 5 1
A getLevels() 0 3 1
A getEnabled() 0 6 2
A setCategories() 0 14 4
A setPrefixCallback() 0 8 2
B collectLogs() 0 23 7
A getProcessInterval() 0 3 1
A setDisplaySubSeconds() 0 5 1
A setEnabled() 0 8 2
A getLevelName() 0 3 1
A getCategories() 0 3 1
A getLogColor() 0 6 1
A init() 0 2 1
A getLogCount() 0 3 1
A getLevelValue() 0 3 1
A getPrefixCallback() 0 3 1
B formatLogMessage() 0 23 9
A setLevels() 0 24 6

How to fix   Complexity   

Complex Class

Complex classes like TLogRoute often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TLogRoute, and based on these observations, apply Extract Interface, too.

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

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

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

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