Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.66% |
147 / 149 |
|
90.48% |
19 / 21 |
CRAP | |
0.00% |
0 / 1 |
FileLogger | |
98.66% |
147 / 149 |
|
90.48% |
19 / 21 |
68 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
createFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getInstance | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
__destruct | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
__clone | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
startTimeLog | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
endTimeLog | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
interpolate | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
5 | |||
write | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
emergency | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
alert | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
critical | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
error | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
warning | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
notice | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
info | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
debug | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
log | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
countLogs | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
getHighestPerpetrator | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
get | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
8 | |||
getByLine | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
8 | |||
console | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\Log |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Log; |
16 | |
17 | use phpOMS\Stdlib\Base\Exception\InvalidEnumValue; |
18 | use phpOMS\System\File\Local\File; |
19 | |
20 | /** |
21 | * Logging class. |
22 | * |
23 | * @package phpOMS\Log |
24 | * @license OMS License 2.0 |
25 | * @link https://jingga.app |
26 | * @since 1.0.0 |
27 | * |
28 | * @SuppressWarnings(PHPMD.Superglobals) |
29 | */ |
30 | final class FileLogger implements LoggerInterface |
31 | { |
32 | public const MSG_BACKTRACE = '{datetime}; {level}; {ip}; {message}; {backtrace}'; |
33 | |
34 | public const MSG_FULL = '{datetime}; {level}; {ip}; {line}; {version}; {os}; {path}; {message}; {file}; {backtrace}'; |
35 | |
36 | public const MSG_SIMPLE = '{datetime}; {level}; {ip}; {message};'; |
37 | |
38 | /** |
39 | * Timing array. |
40 | * |
41 | * Potential values are null or an array filled with log timings. |
42 | * This is used in order to profile code sections by ID. |
43 | * |
44 | * @var array<string, array{start:float, end:float, time:float}> |
45 | * @since 1.0.0 |
46 | */ |
47 | private static array $timings = []; |
48 | |
49 | /** |
50 | * Instance. |
51 | * |
52 | * @var FileLogger |
53 | * @since 1.0.0 |
54 | */ |
55 | protected static FileLogger $instance; |
56 | |
57 | /** |
58 | * Verbose. |
59 | * |
60 | * @var bool |
61 | * @since 1.0.0 |
62 | */ |
63 | protected bool $verbose = false; |
64 | |
65 | /** |
66 | * The file pointer for the logging. |
67 | * |
68 | * Potential values are null or a valid file pointer |
69 | * |
70 | * @var false|resource |
71 | * @since 1.0.0 |
72 | */ |
73 | private $fp = false; |
74 | |
75 | /** |
76 | * Logging path |
77 | * |
78 | * @var string |
79 | * @since 1.0.0 |
80 | */ |
81 | private string $path; |
82 | |
83 | /** |
84 | * Is the logging file created |
85 | * |
86 | * @var bool |
87 | * @since 1.0.0 |
88 | */ |
89 | private bool $created = false; |
90 | |
91 | /** |
92 | * Object constructor. |
93 | * |
94 | * Creates the logging object and overwrites all default values. |
95 | * |
96 | * @param string $lpath Path for logging |
97 | * @param bool $verbose Verbose logging |
98 | * |
99 | * @since 1.0.0 |
100 | */ |
101 | public function __construct(string $lpath = '', bool $verbose = false) |
102 | { |
103 | $path = \realpath(empty($lpath) ? __DIR__ . '/../../Logs/' : $lpath); |
104 | $this->verbose = $verbose; |
105 | |
106 | $this->path = \is_dir($lpath) || \strpos($lpath, '.') === false |
107 | ? \rtrim($path !== false ? $path : $lpath, '/') . '/' . \date('Y-m-d') . '.log' |
108 | : $lpath; |
109 | } |
110 | |
111 | /** |
112 | * Create logging file. |
113 | * |
114 | * @return void |
115 | * |
116 | * @since 1.0.0 |
117 | */ |
118 | private function createFile() : void |
119 | { |
120 | if (!$this->created && !\is_file($this->path)) { |
121 | File::create($this->path); |
122 | $this->created = true; |
123 | } |
124 | } |
125 | |
126 | /** |
127 | * Returns instance. |
128 | * |
129 | * @param string $lpath Logging path |
130 | * @param bool $verbose Verbose logging |
131 | * |
132 | * @return FileLogger |
133 | * |
134 | * @since 1.0.0 |
135 | */ |
136 | public static function getInstance(string $lpath = '', bool $verbose = false) : self |
137 | { |
138 | if (!isset(self::$instance)) { |
139 | self::$instance = new self($lpath, $verbose); |
140 | } |
141 | |
142 | return self::$instance; |
143 | } |
144 | |
145 | /** |
146 | * Object destructor. |
147 | * |
148 | * Closes the logging file |
149 | * |
150 | * @since 1.0.0 |
151 | * @codeCoverageIgnore |
152 | */ |
153 | public function __destruct() |
154 | { |
155 | if (\is_resource($this->fp)) { |
156 | \fclose($this->fp); |
157 | } |
158 | } |
159 | |
160 | /** |
161 | * Protect instance from getting copied from outside. |
162 | * |
163 | * @return void |
164 | * |
165 | * @since 1.0.0 |
166 | * @codeCoverageIgnore |
167 | */ |
168 | private function __clone() |
169 | { |
170 | } |
171 | |
172 | /** |
173 | * Starts the time measurement. |
174 | * |
175 | * @param string $id the ID by which this time measurement gets identified |
176 | * |
177 | * @return bool |
178 | * |
179 | * @since 1.0.0 |
180 | */ |
181 | public static function startTimeLog(string $id = '') : bool |
182 | { |
183 | self::$timings[$id] = ['start' => \microtime(true), 'end' => 0.0, 'time' => 0.0]; |
184 | |
185 | return true; |
186 | } |
187 | |
188 | /** |
189 | * Ends the time measurement. |
190 | * |
191 | * @param string $id the ID by which this time measurement gets identified |
192 | * |
193 | * @return float The time measurement in seconds |
194 | * |
195 | * @since 1.0.0 |
196 | */ |
197 | public static function endTimeLog(string $id = '') : float |
198 | { |
199 | $mtime = \microtime(true); |
200 | |
201 | self::$timings[$id]['end'] = $mtime; |
202 | self::$timings[$id]['time'] = $mtime - self::$timings[$id]['start']; |
203 | |
204 | return self::$timings[$id]['time']; |
205 | } |
206 | |
207 | /** |
208 | * Interpolate context |
209 | * |
210 | * @param string $message Log schema |
211 | * @param array<string, null|int|bool|float|string> $context Context to log |
212 | * @param string $level Log level |
213 | * |
214 | * @return string |
215 | * |
216 | * @since 1.0.0 |
217 | */ |
218 | private function interpolate(string $message, array $context = [], string $level = LogLevel::DEBUG) : string |
219 | { |
220 | $replace = []; |
221 | foreach ($context as $key => $val) { |
222 | $replace['{' . $key . '}'] = \str_replace(["\r\n", "\r", "\n"], ' ', (string) $val); |
223 | } |
224 | |
225 | $backtrace = \debug_backtrace(); |
226 | |
227 | // Removing sensitive config data from logging |
228 | foreach ($backtrace as $key => $value) { |
229 | if (isset($value['args'])) { |
230 | unset($backtrace[$key]['args']); |
231 | } |
232 | } |
233 | |
234 | $encodedBacktrace = \json_encode($backtrace); |
235 | if (!\is_string($encodedBacktrace)) { |
236 | $encodedBacktrace = ''; |
237 | } |
238 | |
239 | $backtrace = \str_replace(["\r\n", "\r", "\n"], ' ', $encodedBacktrace); |
240 | |
241 | $replace['{backtrace}'] = $backtrace; |
242 | $replace['{datetime}'] = \sprintf('%--19s', (new \DateTimeImmutable('NOW'))->format('Y-m-d H:i:s')); |
243 | $replace['{level}'] = \sprintf('%--12s', $level); |
244 | $replace['{path}'] = $_SERVER['REQUEST_URI'] ?? 'REQUEST_URI'; |
245 | $replace['{ip}'] = \sprintf('%--15s', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); |
246 | $replace['{version}'] = \sprintf('%--15s', \PHP_VERSION); |
247 | $replace['{os}'] = \sprintf('%--15s', \PHP_OS); |
248 | $replace['{line}'] = \sprintf('%--15s', $context['line'] ?? '?'); |
249 | |
250 | return \strtr($message, $replace); |
251 | } |
252 | |
253 | /** |
254 | * Write to file. |
255 | * |
256 | * @param string $message Log message |
257 | * |
258 | * @return void |
259 | * |
260 | * @since 1.0.0 |
261 | */ |
262 | private function write(string $message) : void |
263 | { |
264 | if ($this->verbose) { |
265 | echo $message, "\n"; |
266 | } |
267 | |
268 | $this->createFile(); |
269 | if (!\is_writable($this->path)) { |
270 | return; // @codeCoverageIgnore |
271 | } |
272 | |
273 | $this->fp = \fopen($this->path, 'a'); |
274 | |
275 | if ($this->fp !== false && \flock($this->fp, \LOCK_EX)) { |
276 | \fwrite($this->fp, $message . "\n"); |
277 | \fflush($this->fp); |
278 | \flock($this->fp, \LOCK_UN); |
279 | \fclose($this->fp); |
280 | $this->fp = false; |
281 | } |
282 | } |
283 | |
284 | /** |
285 | * {@inheritdoc} |
286 | */ |
287 | public function emergency(string $message, array $context = []) : void |
288 | { |
289 | $message = $this->interpolate($message, $context, LogLevel::EMERGENCY); |
290 | $this->write($message); |
291 | } |
292 | |
293 | /** |
294 | * {@inheritdoc} |
295 | */ |
296 | public function alert(string $message, array $context = []) : void |
297 | { |
298 | $message = $this->interpolate($message, $context, LogLevel::ALERT); |
299 | $this->write($message); |
300 | } |
301 | |
302 | /** |
303 | * {@inheritdoc} |
304 | */ |
305 | public function critical(string $message, array $context = []) : void |
306 | { |
307 | $message = $this->interpolate($message, $context, LogLevel::CRITICAL); |
308 | $this->write($message); |
309 | } |
310 | |
311 | /** |
312 | * {@inheritdoc} |
313 | */ |
314 | public function error(string $message, array $context = []) : void |
315 | { |
316 | $message = $this->interpolate($message, $context, LogLevel::ERROR); |
317 | $this->write($message); |
318 | } |
319 | |
320 | /** |
321 | * {@inheritdoc} |
322 | */ |
323 | public function warning(string $message, array $context = []) : void |
324 | { |
325 | $message = $this->interpolate($message, $context, LogLevel::WARNING); |
326 | $this->write($message); |
327 | } |
328 | |
329 | /** |
330 | * {@inheritdoc} |
331 | */ |
332 | public function notice(string $message, array $context = []) : void |
333 | { |
334 | $message = $this->interpolate($message, $context, LogLevel::NOTICE); |
335 | $this->write($message); |
336 | } |
337 | |
338 | /** |
339 | * {@inheritdoc} |
340 | */ |
341 | public function info(string $message, array $context = []) : void |
342 | { |
343 | $message = $this->interpolate($message, $context, LogLevel::INFO); |
344 | $this->write($message); |
345 | } |
346 | |
347 | /** |
348 | * {@inheritdoc} |
349 | */ |
350 | public function debug(string $message, array $context = []) : void |
351 | { |
352 | $message = $this->interpolate($message, $context, LogLevel::DEBUG); |
353 | $this->write($message); |
354 | } |
355 | |
356 | /** |
357 | * {@inheritdoc} |
358 | * |
359 | * @throws InvalidEnumValue |
360 | */ |
361 | public function log(string $level, string $message, array $context = []) : void |
362 | { |
363 | if (!LogLevel::isValidValue($level)) { |
364 | throw new InvalidEnumValue($level); |
365 | } |
366 | |
367 | $message = $this->interpolate($message, $context, $level); |
368 | $this->write($message); |
369 | } |
370 | |
371 | /** |
372 | * Analyse logging file. |
373 | * |
374 | * @return array |
375 | * |
376 | * @since 1.0.0 |
377 | */ |
378 | public function countLogs() : array |
379 | { |
380 | $levels = []; |
381 | |
382 | if (!\is_file($this->path)) { |
383 | return $levels; |
384 | } |
385 | |
386 | $this->fp = \fopen($this->path, 'r'); |
387 | |
388 | if ($this->fp === false) { |
389 | return $levels; // @codeCoverageIgnore |
390 | } |
391 | |
392 | \fseek($this->fp, 0); |
393 | $line = \fgetcsv($this->fp, 0, ';'); |
394 | |
395 | while ($line !== false && $line !== null) { |
396 | if (\count($line) < 2) { |
397 | continue; // @codeCoverageIgnore |
398 | } |
399 | |
400 | $line[1] = \trim($line[1]); |
401 | |
402 | if (!isset($levels[$line[1]])) { |
403 | $levels[$line[1]] = 0; |
404 | } |
405 | |
406 | ++$levels[$line[1]]; |
407 | $line = \fgetcsv($this->fp, 0, ';'); |
408 | } |
409 | |
410 | \fseek($this->fp, 0, \SEEK_END); |
411 | \fclose($this->fp); |
412 | |
413 | return $levels; |
414 | } |
415 | |
416 | /** |
417 | * Find cricitcal connections. |
418 | * |
419 | * @param int $limit Amout of perpetrators |
420 | * |
421 | * @return array |
422 | * |
423 | * @since 1.0.0 |
424 | */ |
425 | public function getHighestPerpetrator(int $limit = 10) : array |
426 | { |
427 | $connection = []; |
428 | |
429 | if (!\is_file($this->path)) { |
430 | return $connection; |
431 | } |
432 | |
433 | $this->fp = \fopen($this->path, 'r'); |
434 | |
435 | if ($this->fp === false) { |
436 | return $connection; // @codeCoverageIgnore |
437 | } |
438 | |
439 | \fseek($this->fp, 0); |
440 | $line = \fgetcsv($this->fp, 0, ';'); |
441 | |
442 | while ($line !== false && $line !== null) { |
443 | if (\count($line) < 3) { |
444 | continue; // @codeCoverageIgnore |
445 | } |
446 | |
447 | $line[2] = \trim($line[2]); |
448 | |
449 | if (!isset($connection[$line[2]])) { |
450 | $connection[$line[2]] = 0; |
451 | } |
452 | |
453 | ++$connection[$line[2]]; |
454 | $line = \fgetcsv($this->fp, 0, ';'); |
455 | } |
456 | |
457 | \fseek($this->fp, 0, \SEEK_END); |
458 | \fclose($this->fp); |
459 | \asort($connection); |
460 | |
461 | return \array_slice($connection, 0, $limit); |
462 | } |
463 | |
464 | /** |
465 | * Get logging messages from file. |
466 | * |
467 | * @param int $limit Amout of logs |
468 | * @param int $offset Offset |
469 | * |
470 | * @return array |
471 | * |
472 | * @since 1.0.0 |
473 | */ |
474 | public function get(int $limit = 25, int $offset = 0) : array |
475 | { |
476 | $logs = []; |
477 | $id = 0; |
478 | |
479 | if (!\is_file($this->path)) { |
480 | return $logs; |
481 | } |
482 | |
483 | $this->fp = \fopen($this->path, 'r'); |
484 | |
485 | if ($this->fp === false) { |
486 | return $logs; // @codeCoverageIgnore |
487 | } |
488 | |
489 | \fseek($this->fp, 0); |
490 | |
491 | $line = \fgetcsv($this->fp, 0, ';'); |
492 | while ($line !== false && $line !== null) { |
493 | if ($limit < 1) { |
494 | break; |
495 | } |
496 | |
497 | ++$id; |
498 | |
499 | if ($offset > 0) { |
500 | $line = \fgetcsv($this->fp, 0, ';'); |
501 | |
502 | --$offset; |
503 | continue; |
504 | } |
505 | |
506 | foreach ($line as &$value) { |
507 | $value = \trim($value); |
508 | } |
509 | |
510 | $logs[$id] = $line; |
511 | |
512 | --$limit; |
513 | |
514 | $line = \fgetcsv($this->fp, 0, ';'); |
515 | } |
516 | |
517 | \fseek($this->fp, 0, \SEEK_END); |
518 | \fclose($this->fp); |
519 | |
520 | return $logs; |
521 | } |
522 | |
523 | /** |
524 | * Get single logging message from file. |
525 | * |
526 | * @param int $id Id/Line number of the logging message |
527 | * |
528 | * @return array |
529 | * |
530 | * @since 1.0.0 |
531 | */ |
532 | public function getByLine(int $id = 1) : array |
533 | { |
534 | $log = []; |
535 | $current = 0; |
536 | |
537 | if (!\is_file($this->path)) { |
538 | return $log; |
539 | } |
540 | |
541 | $this->fp = \fopen($this->path, 'r'); |
542 | |
543 | if ($this->fp === false) { |
544 | return $log; // @codeCoverageIgnore |
545 | } |
546 | |
547 | \fseek($this->fp, 0); |
548 | |
549 | while (($line = \fgetcsv($this->fp, 0, ';')) !== false && $current <= $id) { |
550 | ++$current; |
551 | |
552 | if ($current < $id || $line === null) { |
553 | continue; |
554 | } |
555 | |
556 | foreach ($line as $value) { |
557 | $log[] = \trim($value); |
558 | } |
559 | |
560 | break; |
561 | } |
562 | |
563 | \fseek($this->fp, 0, \SEEK_END); |
564 | \fclose($this->fp); |
565 | |
566 | return $log; |
567 | } |
568 | |
569 | /** |
570 | * Create console log. |
571 | * |
572 | * @param string $message Log message |
573 | * @param bool $verbose Is verbose |
574 | * @param array<string, string> $context Context |
575 | * |
576 | * @return void |
577 | * |
578 | * @since 1.0.0 |
579 | */ |
580 | public function console(string $message, bool $verbose = true, array $context = []) : void |
581 | { |
582 | if (empty($context)) { |
583 | $message = \date('[Y-m-d H:i:s] ') . $message . "\r\n"; |
584 | } |
585 | |
586 | if ($verbose) { |
587 | echo $this->interpolate($message, $context); |
588 | } else { |
589 | $this->info($message, $context); |
590 | } |
591 | } |
592 | } |