Passed
Push — master ( 2e496e...4c080f )
by Alexander
03:20 queued 01:17
created

Target::getContextMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log;
6
7
use DateTime;
8
use Psr\Log\InvalidArgumentException;
9
use Psr\Log\LogLevel;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\VarDumper\VarDumper;
12
13
use function gettype;
14
use function implode;
15
use function in_array;
16
use function is_array;
17
use function is_bool;
18
use function is_callable;
19
use function is_string;
20
use function microtime;
21
use function sprintf;
22
use function strpos;
23
24
/**
25
 * Target is the base class for all log target classes.
26
 *
27
 * A log target object will filter the messages logged by {@see \Yiisoft\Log\Logger} according
28
 * to its {@see Target::setLevels()} and {@see Target::setCategories()}. It may also export
29
 * the filtered messages to specific destination defined by the target, such as emails, files.
30
 *
31
 * Level filter and category filter are combinatorial, i.e., only messages
32
 * satisfying both filter conditions will be handled. Additionally, you may
33
 * specify {@see Target::setExcept()} to exclude messages of certain categories.
34
 */
35
abstract class Target
36
{
37
    private MessageCategoryFilter $categories;
38
    private MessageCollection $messages;
39
40
    /**
41
     * @var string[] list of the PHP predefined variables that should be logged in a message.
42
     *
43
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged.
44
     *
45
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`.
46
     *
47
     * Each element can also be specified as one of the following:
48
     *
49
     * - `var` - `var` will be logged.
50
     * - `var.key` - only `var[key]` key will be logged.
51
     * - `!var.key` - `var[key]` key will be excluded.
52
     *
53
     * Note that if you need $_SESSION to logged regardless if session
54
     * was used you have to open it right at he start of your request.
55
     *
56
     * @see \Yiisoft\Arrays\ArrayHelper::filter()
57
     */
58
    private array $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'];
59
60
    /**
61
     * @var callable|null PHP callable that returns a string to be prefixed to every exported message.
62
     *
63
     * If not set, {@see Target::getMessagePrefix()} will be used, which prefixes
64
     * the message with context information such as user IP, user ID and session ID.
65
     *
66
     * The signature of the callable should be `function ($message)`.
67
     */
68
    private $prefix;
69
70
    /**
71
     * @var int How many log messages should be accumulated before they are exported.
72
     *
73
     * Defaults to 1000. Note that messages will always be exported when the application terminates.
74
     * Set this property to be 0 if you don't want to export messages until the application terminates.
75
     */
76
    private int $exportInterval = 1000;
77
78
    /**
79
     * @var string The date format for the log timestamp. Defaults to `Y-m-d H:i:s.u`.
80
     */
81
    private string $timestampFormat = 'Y-m-d H:i:s.u';
82
83
    /**
84
     * @var bool|callable Enables or disables the current target to export.
85
     */
86
    private $enabled = true;
87
88
    /**
89
     * Exports log messages to a specific destination.
90
     * Child classes must implement this method.
91
     */
92
    abstract protected function export(): void;
93
94
    /**
95
     * When defining a constructor in child classes, you must call `parent::__construct()`.
96
     */
97 128
    public function __construct()
98
    {
99 128
        $this->categories = new MessageCategoryFilter();
100 128
        $this->messages = new MessageCollection();
101 128
    }
102
103
    /**
104
     * Processes the given log messages.
105
     *
106
     * This method will filter the given messages with levels and categories.
107
     * The message structure follows that in {@see MessageCollection::$messages}.
108
     * And if requested, it will also export the filtering result to specific medium (e.g. email).
109
     *
110
     * @param array $messages Log messages to be processed.
111
     * @param bool $final Whether this method is called at the end of the current application.
112
     */
113 27
    public function collect(array $messages, bool $final): void
114
    {
115 27
        $this->messages->addMultiple($this->filterMessages($messages));
116 23
        $count = $this->messages->count();
117
118 23
        if ($count > 0 && ($final || ($this->exportInterval > 0 && $count >= $this->exportInterval))) {
119 1
            if (($contextMessage = $this->getContextMessage()) !== '') {
120 1
                $this->messages->add(LogLevel::INFO, $contextMessage, [
121 1
                    'category' => MessageCategoryFilter::DEFAULT,
122 1
                    'time' => microtime(true),
123
                ]);
124
            }
125
            // set exportInterval to 0 to avoid triggering export again while exporting
126 1
            $oldExportInterval = $this->exportInterval;
127 1
            $this->exportInterval = 0;
128 1
            $this->export();
129 1
            $this->exportInterval = $oldExportInterval;
130 1
            $this->messages->clear();
131
        }
132 23
    }
133
134
    /**
135
     * Sets a list of log message categories that this target is interested in.
136
     *
137
     * @param array $categories The list of log message categories.
138
     * @return self
139
     * @throws InvalidArgumentException for invalid log message categories structure.
140
     * @see MessageCategoryFilter::$include
141
     */
142 19
    public function setCategories(array $categories): self
143
    {
144 19
        $this->categories->include($categories);
145 13
        return $this;
146
    }
147
148
    /**
149
     * Gets a list of log message categories that this target is interested in.
150
     *
151
     * @return string[] The list of log message categories.
152
     * @see MessageCategoryFilter::$include
153
     */
154 1
    public function getCategories(): array
155
    {
156 1
        return $this->categories->getIncluded();
157
    }
158
159
    /**
160
     * Sets a list of log message categories that this target is NOT interested in.
161
     *
162
     * @param array $except The list of log message categories.
163
     * @return self
164
     * @throws InvalidArgumentException for invalid log message categories structure.
165
     * @see MessageCategoryFilter::$exclude
166
     */
167 10
    public function setExcept(array $except): self
168
    {
169 10
        $this->categories->exclude($except);
170 4
        return $this;
171
    }
172
173
    /**
174
     * Gets a list of log message categories that this target is NOT interested in.
175
     *
176
     * @return string[] The list of excluded categories of log messages.
177
     * @see MessageCategoryFilter::$exclude
178
     */
179 1
    public function getExcept(): array
180
    {
181 1
        return $this->categories->getExcluded();
182
    }
183
184
    /**
185
     * Sets a list of log messages that are retrieved from the logger so far by this log target.
186
     *
187
     * @param array[] $messages The list of log messages.
188
     * @return self
189
     * @throws InvalidArgumentException for invalid message structure.
190
     * @see MessageCollection::$messages
191
     */
192 8
    public function setMessages(array $messages): self
193
    {
194 8
        $this->messages->addMultiple($messages);
195 8
        return $this;
196
    }
197
198
    /**
199
     * Gets a list of log messages that are retrieved from the logger so far by this log target.
200
     *
201
     * @return array[] The list of log messages.
202
     * @see MessageCollection::$messages
203
     */
204 29
    public function getMessages(): array
205
    {
206 29
        return $this->messages->all();
207
    }
208
209
    /**
210
     * Sets a list of log message levels that current target is interested in.
211
     *
212
     * @param array $levels The list of log message levels.
213
     * @return self
214
     * @throws InvalidArgumentException for invalid log message level.
215
     * @see MessageCollection::$levels
216
     */
217 16
    public function setLevels(array $levels): self
218
    {
219 16
        $this->messages->setLevels($levels);
220 10
        return $this;
221
    }
222
223
    /**
224
     * Gets a list of log message levels that current target is interested in.
225
     *
226
     * @return string[] The list of log message levels.
227
     * @see MessageCollection::$levels
228
     */
229 1
    public function getLevels(): array
230
    {
231 1
        return $this->messages->getLevels();
232
    }
233
234
    /**
235
     * Sets a list of the PHP predefined variables that should be logged in a message.
236
     *
237
     * @param array $logVars The list of PHP predefined variables.
238
     * @return self
239
     * @throws InvalidArgumentException for non-string values.
240
     * @see Target::$logVars
241
     */
242 28
    public function setLogVars(array $logVars): self
243
    {
244 28
        foreach ($logVars as $logVar) {
245 8
            if (!is_string($logVar)) {
246 6
                throw new InvalidArgumentException(sprintf(
247 6
                    'The PHP predefined variable must be a string, %s received.',
248 6
                    gettype($logVar)
249
                ));
250
            }
251
        }
252
253 22
        $this->logVars = $logVars;
254 22
        return $this;
255
    }
256
257
    /**
258
     * Gets a list of the PHP predefined variables that should be logged in a message.
259
     *
260
     * @return string[] The list of the PHP predefined variables.
261
     * @see Target::$logVars
262
     */
263 1
    public function getLogVars(): array
264
    {
265 1
        return $this->logVars;
266
    }
267
268
    /**
269
     * Sets a PHP callable that returns a string to be prefixed to every exported message.
270
     *
271
     * @param callable $prefix The PHP callable to get a string value from.
272
     * @return self
273
     * @see Target::$prefix
274
     */
275 12
    public function setPrefix(callable $prefix): self
276
    {
277 12
        $this->prefix = $prefix;
278 12
        return $this;
279
    }
280
281
    /**
282
     * Gets a PHP callable that returns a string to be prefixed to every exported message.
283
     *
284
     * @return callable|null The PHP callable to get a string value from or null.
285
     * @see Target::$prefix
286
     */
287 1
    public function getPrefix(): ?callable
288
    {
289 1
        return $this->prefix;
290
    }
291
292
    /**
293
     * Sets how many messages should be accumulated before they are exported.
294
     *
295
     * @param int $exportInterval The number of log messages to accumulate before exporting.
296
     * @return self
297
     * @see Target::$exportInterval
298
     */
299 1
    public function setExportInterval(int $exportInterval): self
300
    {
301 1
        $this->exportInterval = $exportInterval;
302 1
        return $this;
303
    }
304
305
    /**
306
     * Gets how many messages should be accumulated before they are exported.
307
     *
308
     * @return int The number of messages to accumulate before exporting.
309
     * @see Target::$exportInterval
310
     */
311 1
    public function getExportInterval(): int
312
    {
313 1
        return $this->exportInterval;
314
    }
315
316
    /**
317
     * Sets a date format for the log timestamp.
318
     *
319
     * @param string $format The date format for the log timestamp.
320
     * @return self
321
     * @see Target::$timestampFormat
322
     */
323 4
    public function setTimestampFormat(string $format): self
324
    {
325 4
        $this->timestampFormat = $format;
326 4
        return $this;
327
    }
328
329
    /**
330
     * Gets a date format for the log timestamp.
331
     *
332
     * @return string The date format for the log timestamp.
333
     * @see Target::$timestampFormat
334
     */
335 1
    public function getTimestampFormat(): string
336
    {
337 1
        return $this->timestampFormat;
338
    }
339
340
    /**
341
     * Sets a value indicating whether this log target is enabled.
342
     *
343
     * A callable may be used to determine whether the log target should be enabled in a dynamic way.
344
     *
345
     * @param bool|callable $value The boolean value or a callable to get a boolean value from.
346
     * @return self
347
     * @throws InvalidArgumentException for non-boolean or non-callable value.
348
     * @see Target::$enabled
349
     */
350 12
    public function setEnabled($value): self
351
    {
352 12
        if (!is_bool($value) && !is_callable($value)) {
353 5
            throw new InvalidArgumentException(sprintf(
354 5
                'The value indicating whether this log target is enabled must be a boolean or callable, %s received.',
355 5
                gettype($value)
356
            ));
357
        }
358
359 7
        $this->enabled = $value;
360 7
        return $this;
361
    }
362
363
    /**
364
     * Enables the log target.
365
     *
366
     * @return self
367
     * @see Target::setEnabled()
368
     */
369 1
    public function enable(): self
370
    {
371 1
        return $this->setEnabled(true);
372
    }
373
374
    /**
375
     * Disables the log target.
376
     *
377
     * @return self
378
     * @see Target::setEnabled()
379
     */
380 1
    public function disable(): self
381
    {
382 1
        return $this->setEnabled(false);
383
    }
384
385
    /**
386
     * Check whether the log target is enabled.
387
     *
388
     * @return bool The value indicating whether this log target is enabled.
389
     * @throws LogRuntimeException for a callable "enabled" that does not return a boolean.
390
     * @see Target::$enabled
391
     */
392 30
    public function isEnabled(): bool
393
    {
394 30
        if (is_bool($this->enabled)) {
395 24
            return $this->enabled;
396
        }
397
398 7
        if (!is_bool($enabled = ($this->enabled)($this))) {
399 6
            throw new LogRuntimeException(sprintf(
400 6
                'The PHP callable "enabled" must returns a boolean, %s received.',
401 6
                gettype($enabled)
402
            ));
403
        }
404
405 1
        return $enabled;
406
    }
407
408
    /**
409
     * Generates the context information to be logged.
410
     *
411
     * The default implementation will dump user information, system variables, etc.
412
     *
413
     * @return string The context information. If an empty string, it means no context information.
414
     */
415 2
    protected function getContextMessage(): string
416
    {
417 2
        $result = [];
418
419 2
        foreach (ArrayHelper::filter($GLOBALS, $this->logVars) as $key => $value) {
420 2
            $result[] = "\${$key} = " . VarDumper::create($value)->asString();
421
        }
422
423 2
        return implode("\n\n", $result);
424
    }
425
426
    /**
427
     * Filters the given messages according to their categories and levels.
428
     *
429
     * The message structure follows that in {@see MessageCollection::$messages}.
430
     *
431
     * @param array[] $messages List log messages to be filtered.
432
     * @return array[] The filtered log messages.
433
     */
434 27
    protected function filterMessages(array $messages): array
435
    {
436 27
        foreach ($messages as $i => $message) {
437 27
            $levels = $this->messages->getLevels();
438
439 27
            if ((!empty($levels) && !in_array(($message[0] ?? ''), $levels, true))) {
440 7
                unset($messages[$i]);
441 7
                continue;
442
            }
443
444 27
            $category = (string) ($message[2]['category'] ?? '');
445
446 27
            if ($this->categories->isExcluded($category)) {
447 13
                unset($messages[$i]);
448
            }
449
        }
450
451 27
        return $messages;
452
    }
453
454
    /**
455
     * Formats a log message for display as a string.
456
     *
457
     * The message structure follows that in {@see MessageCollection::$messages}.
458
     *
459
     * @param array $message The log message to be formatted.
460
     * @return string The formatted log message.
461
     * @throws InvalidArgumentException for invalid message structure.
462
     */
463 7
    protected function formatMessage(array $message): string
464
    {
465 7
        $this->messages->checkStructure($message);
466 3
        [$level, $text, $context] = $message;
467
468 3
        $level = Logger::getLevelName($level);
469 3
        $timestamp = $context['time'] ?? microtime(true);
470 3
        $category = $context['category'] ?? MessageCategoryFilter::DEFAULT;
471
472 3
        $traces = [];
473 3
        if (isset($context['trace']) && is_array($context['trace'])) {
474 1
            foreach ($context['trace'] as $trace) {
475 1
                if (isset($trace['file'], $trace['line'])) {
476 1
                    $traces[] = "in {$trace['file']}:{$trace['line']}";
477
                }
478
            }
479
        }
480
481 3
        $prefix = $this->getMessagePrefix($message);
482
483 3
        return $this->getTime($timestamp) . " {$prefix}[$level][$category] $text"
484 3
            . (empty($traces) ? '' : "\n    " . implode("\n    ", $traces));
485
    }
486
487
    /**
488
     * Gets a string to be prefixed to the given message.
489
     *
490
     * If {@see Target::$prefix} is configured it will return the result of the callback.
491
     * The default implementation will return user IP, user ID and session ID as a prefix.
492
     * The message structure follows that in {@see MessageCollection::$messages}.
493
     *
494
     * @param array $message The log message being exported.
495
     * @return string The log  prefix string.
496
     * @throws LogRuntimeException for a callable "prefix" that does not return a string.
497
     */
498 14
    protected function getMessagePrefix(array $message): string
499
    {
500 14
        if ($this->prefix === null) {
501 3
            return '';
502
        }
503
504 11
        $this->messages->checkStructure($message);
505 7
        $prefix = ($this->prefix)($message);
506
507 7
        if (!is_string($prefix)) {
508 6
            throw new LogRuntimeException(sprintf(
509 6
                'The PHP callable "prefix" must returns a string, %s received.',
510 6
                gettype($prefix)
511
            ));
512
        }
513
514 1
        return $prefix;
515
    }
516
517
    /**
518
     * Gets formatted timestamp for message, according to {@see Target::$timestampFormat}.
519
     *
520
     * @param float|int $timestamp The timestamp to be formatted.
521
     * @return string Formatted timestamp for message.
522
     */
523 3
    protected function getTime($timestamp): string
524
    {
525 3
        $timestamp = (string) $timestamp;
526 3
        $format = strpos($timestamp, '.') === false ? 'U' : 'U.u';
527 3
        return DateTime::createFromFormat($format, $timestamp)->format($this->timestampFormat);
528
    }
529
}
530