Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.66% covered (success)
98.66%
147 / 149
90.48% covered (success)
90.48%
19 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileLogger
98.66% covered (success)
98.66%
147 / 149
90.48% covered (success)
90.48%
19 / 21
68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 createFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getInstance
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
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% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 endTimeLog
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 interpolate
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
 write
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 emergency
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 alert
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 critical
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 error
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 warning
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 notice
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 info
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 debug
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 log
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 countLogs
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 getHighestPerpetrator
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 get
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 getByLine
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 console
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
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 */
13declare(strict_types=1);
14
15namespace phpOMS\Log;
16
17use phpOMS\Stdlib\Base\Exception\InvalidEnumValue;
18use 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 */
30final 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}