Passed
Push — master ( a8cd64...c52ac9 )
by Alexander
02:12
created

Target::getMessagePrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 10
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 array_merge;
14
use function count;
15
use function implode;
16
use function in_array;
17
use function is_array;
18
use function is_callable;
19
use function is_string;
20
use function rtrim;
21
use function strpos;
22
use function substr_compare;
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::$levels} and {@see Target::$categories}. It may also export the filtered
29
 * 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
33
 * may specify {@see Target::$except} to exclude messages of certain categories.
34
 *
35
 * For more details and usage information on Target, see the
36
 * [guide article on logging & targets](guide:runtime-logging).
37
 */
38
abstract class Target
39
{
40
    /**
41
     * The default category will be used if the category was not passed in the message context {@see Target::$messages}.
42
     */
43
    public const DEFAULT_CATEGORY = 'application';
44
45
    /**
46
     * @var array list of message categories that this target is interested in.
47
     * Defaults to empty, meaning all categories.
48
     * You can use an asterisk at the end of a category so that the category may be used to
49
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
50
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
51
     */
52
    private array $categories = [];
53
54
    /**
55
     * @var array list of message categories that this target is NOT interested in.
56
     * Defaults to empty, meaning no uninteresting messages.
57
     * If this property is not empty, then any category listed here will be excluded from {@see Target::$categories}.
58
     * You can use an asterisk at the end of a category so that the category can be used to
59
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
60
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
61
     * @see categories
62
     */
63
    private array $except = [];
64
65
    /**
66
     * @var array the message levels that this target is interested in.
67
     *
68
     * The parameter should be an array of interested level names. See {@see LogLevel} constants for valid level names.
69
     *
70
     * For example:
71
     *
72
     * ```php
73
     * ['error', 'warning'],
74
     * // or
75
     * [LogLevel::ERROR, LogLevel::WARNING]
76
     * ```
77
     *
78
     * Defaults is empty array, meaning all available levels.
79
     */
80
    private array $levels = [];
81
82
    /**
83
     * @var array list of the PHP predefined variables that should be logged in a message.
84
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged.
85
     *
86
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`.
87
     *
88
     * Each element can also be specified as one of the following:
89
     *
90
     * - `var` - `var` will be logged.
91
     * - `var.key` - only `var[key]` key will be logged.
92
     * - `!var.key` - `var[key]` key will be excluded.
93
     *
94
     * Note that if you need $_SESSION to logged regardless if session was used you have to open it right at
95
     * the start of your request.
96
     *
97
     * @see \Yiisoft\Arrays\ArrayHelper::filter()
98
     */
99
    private array $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'];
100
101
    /**
102
     * @var callable|null a PHP callable that returns a string to be prefixed to every exported message.
103
     *
104
     * If not set, {@see Target::getMessagePrefix()} will be used, which prefixes the message with context information
105
     * such as user IP, user ID and session ID.
106
     *
107
     * The signature of the callable should be `function ($message)`.
108
     */
109
    private $prefix;
110
111
    /**
112
     * @var int how many messages should be accumulated before they are exported.
113
     * Defaults to 1000. Note that messages will always be exported when the application terminates.
114
     * Set this property to be 0 if you don't want to export messages until the application terminates.
115
     */
116
    private int $exportInterval = 1000;
117
118
    /**
119
     * @var array the messages that are retrieved from the logger so far by this log target.
120
     * Please refer to {@see Logger::$messages} for the details about the message structure.
121
     */
122
    private array $messages = [];
123
124
    /**
125
     * @var string The date format for the log timestamp.
126
     * Defaults to Y-m-d H:i:s.u
127
     */
128
    private string $timestampFormat = 'Y-m-d H:i:s.u';
129
130
131
    /**
132
     * @var bool|callable
133
     */
134
    private $enabled = true;
135
136
    /**
137
     * Exports log {@see Target::$messages} to a specific destination.
138
     * Child classes must implement this method.
139
     */
140
    abstract public function export(): void;
141
142
    /**
143
     * Processes the given log messages.
144
     * This method will filter the given messages with {@see Target::$levels} and {@see Target::$categories}.
145
     * And if requested, it will also export the filtering result to specific medium (e.g. email).
146
     * @param array $messages log messages to be processed. See {@see Logger::$messages} for the structure
147
     * of each message.
148
     * @param bool $final whether this method is called at the end of the current application
149
     */
150 25
    public function collect(array $messages, bool $final): void
151
    {
152 25
        $this->messages = array_merge(
153 25
            $this->messages,
154 25
            $this->filterMessages($messages, $this->levels, $this->categories, $this->except)
155
        );
156
157 20
        $count = count($this->messages);
158 20
        if ($count > 0 && ($final || ($this->exportInterval > 0 && $count >= $this->exportInterval))) {
159 19
            if (($context = $this->getContextMessage()) !== '') {
160
                $this->messages[] = [
161
                    LogLevel::INFO,
162
                    $context,
163
                    [
164
                        'category' => static::DEFAULT_CATEGORY,
165
                        'time' => $_SERVER['REQUEST_TIME_FLOAT'] ?? \microtime(true)
166
                    ]
167
                ];
168
            }
169
            // set exportInterval to 0 to avoid triggering export again while exporting
170 19
            $oldExportInterval = $this->exportInterval;
171 19
            $this->exportInterval = 0;
172 19
            $this->export();
173 19
            $this->exportInterval = $oldExportInterval;
174
175 19
            $this->messages = [];
176
        }
177 20
    }
178
179
    /**
180
     * Generates the context information to be logged.
181
     * The default implementation will dump user information, system variables, etc.
182
     * @return string the context information. If an empty string, it means no context information.
183
     */
184 20
    protected function getContextMessage(): string
185
    {
186 20
        $context = ArrayHelper::filter($GLOBALS, $this->logVars);
187 20
        $result = [];
188 20
        foreach ($context as $key => $value) {
189 1
            $result[] = "\${$key} = " . VarDumper::create($value)->asString();
190
        }
191
192 20
        return implode("\n\n", $result);
193
    }
194
195
    /**
196
     * Filters the given messages according to their categories and levels.
197
     * @param array $messages messages to be filtered. The message structure follows that in {@see Logger::$messages}.
198
     * @param array $levels the message levels to filter by. Empty value means allowing all levels.
199
     * @param array $categories the message categories to filter by. If empty, it means all categories are allowed.
200
     * @param array $except the message categories to exclude. If empty, it means all categories are allowed.
201
     * @return array the filtered messages.
202
     * @throws InvalidArgumentException for invalid message structure.
203
     */
204 25
    protected function filterMessages(
205
        array $messages,
206
        array $levels = [],
207
        array $categories = [],
208
        array $except = []
209
    ): array {
210 25
        foreach ($messages as $i => $message) {
211 25
            $this->checkMessageStructure($message);
212
213 20
            if (!empty($levels) && !in_array($message[0], $levels, true)) {
214 7
                unset($messages[$i]);
215 7
                continue;
216
            }
217
218 20
            $messageCategory = $message[2]['category'] ?? '';
219 20
            $matched = empty($categories);
220
221 20
            foreach ($categories as $category) {
222
                if (
223 12
                    ($messageCategory && $messageCategory === $category)
224
                    || (
225 12
                        !empty($category)
226 12
                        && substr_compare($category, '*', -1, 1) === 0
227 12
                        && strpos($messageCategory, rtrim($category, '*')) === 0
228
                    )
229
                ) {
230 11
                    $matched = true;
231 11
                    break;
232
                }
233
            }
234
235 20
            if ($matched) {
236 19
                foreach ($except as $category) {
237 3
                    $prefix = rtrim($category, '*');
238
                    if (
239 3
                        (($messageCategory && $messageCategory === $category) || $prefix !== $category)
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($messageCategory && $me...ategory, $prefix) === 0, Probably Intended Meaning: $messageCategory && $mes...tegory, $prefix) === 0)
Loading history...
240 3
                        && strpos($messageCategory, $prefix) === 0
241
                    ) {
242 3
                        $matched = false;
243 3
                        break;
244
                    }
245
                }
246
            }
247
248 20
            if (!$matched) {
249 13
                unset($messages[$i]);
250
            }
251
        }
252
253 20
        return $messages;
254
    }
255
256
    /**
257
     * Checks message structure {@see Logger::$messages}.
258
     * @param mixed $message the log message to be checked.
259
     * @throws InvalidArgumentException for invalid message structure.
260
     */
261 32
    protected function checkMessageStructure($message): void
262
    {
263 32
        if (!isset($message[0], $message[1], $message[2]) || !is_string($message[0]) || !is_array($message[2])) {
264 10
            throw new InvalidArgumentException('The message structure is not valid.');
265
        }
266 22
    }
267
268
    /**
269
     * Formats a log message for display as a string.
270
     * @param array $message the log message to be formatted.
271
     * The message structure follows that in {@see Logger::$messages}.
272
     * @return string the formatted message.
273
     * @throws InvalidArgumentException for invalid message structure.
274
     */
275 7
    protected function formatMessage(array $message): string
276
    {
277 7
        $this->checkMessageStructure($message);
278 2
        [$level, $text, $context] = $message;
279
280 2
        $category = $context['category'] ?? static::DEFAULT_CATEGORY;
281 2
        $timestamp = $context['time'] ?? \microtime(true);
282 2
        $level = Logger::getLevelName($level);
283
284 2
        $traces = [];
285 2
        if (isset($context['trace'])) {
286
            foreach ($context['trace'] as $trace) {
287
                if (isset($trace['file'], $trace['line'])) {
288
                    $traces[] = "in {$trace['file']}:{$trace['line']}";
289
                }
290
            }
291
        }
292
293 2
        $prefix = $this->getMessagePrefix($message);
294
295 2
        return $this->getTime($timestamp) . " {$prefix}[$level][$category] $text"
296 2
            . (empty($traces) ? '' : "\n    " . implode("\n    ", $traces));
297
    }
298
299
    /**
300
     * Returns a string to be prefixed to the given message.
301
     * If {@see Target::$prefix} is configured it will return the result of the callback.
302
     * The default implementation will return user IP, user ID and session ID as a prefix.
303
     * @param array $message the message being exported.
304
     * The message structure follows that in {@see Logger::$messages}.
305
     * @return string the prefix string
306
     */
307 2
    protected function getMessagePrefix(array $message): string
308
    {
309 2
        if ($this->prefix !== null) {
310
            return ($this->prefix)($message);
311
        }
312
313 2
        return '';
314
    }
315
316
    /**
317
     * Returns formatted timestamp for message, according to {@see Target::$timestampFormat}
318
     * @param float|int $timestamp
319
     * @return string
320
     */
321 2
    protected function getTime($timestamp): string
322
    {
323 2
        $timestamp = (string) $timestamp;
324 2
        $format = strpos($timestamp, '.') === false ? 'U' : 'U.u';
325 2
        return DateTime::createFromFormat($format, $timestamp)->format($this->timestampFormat);
326
    }
327
328
    /**
329
     * Sets a value indicating whether this log target is enabled.
330
     * @param bool|callable $value a boolean value or a callable to obtain the value from.
331
     *
332
     * A callable may be used to determine whether the log target should be enabled in a dynamic way.
333
     * For example, to only enable a log if the current user is logged in you can configure the target
334
     * as follows:
335
     *
336
     * ```php
337
     * 'enabled' => function() {
338
     *     return !Yii::getApp()->user->isGuest;
339
     * }
340
     * ```
341
     * @return $this
342
     */
343 1
    public function setEnabled($value): self
344
    {
345 1
        $this->enabled = $value;
346
347 1
        return $this;
348
    }
349
350
    /**
351
     * Enables the log target
352
     *
353
     * @return $this
354
     */
355 1
    public function enable(): self
356
    {
357 1
        return $this->setEnabled(true);
358
    }
359
360
    /**
361
     * Disables the log target
362
     *
363
     * @return $this
364
     */
365 1
    public function disable(): self
366
    {
367 1
        return $this->setEnabled(false);
368
    }
369
370
    /**
371
     * Check whether the log target is enabled.
372
     * @return bool A value indicating whether this log target is enabled.
373
     */
374 21
    public function isEnabled(): bool
375
    {
376 21
        if (is_callable($this->enabled)) {
377 1
            return ($this->enabled)($this);
378
        }
379
380 21
        return $this->enabled;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->enabled could return the type callable which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
381
    }
382
383 21
    public function setLogVars(array $logVars): self
384
    {
385 21
        $this->logVars = $logVars;
386 21
        return $this;
387
    }
388
389
    public function getLogVars(): array
390
    {
391
        return $this->logVars;
392
    }
393
394 2
    public function setTimestampFormat(string $format): self
395
    {
396 2
        $this->timestampFormat = $format;
397 2
        return $this;
398
    }
399
400
    public function getCategories(): array
401
    {
402
        return $this->categories;
403
    }
404
405 12
    public function setCategories(array $categories): self
406
    {
407 12
        $this->categories = $categories;
408 12
        return $this;
409
    }
410
411
    public function getExcept(): array
412
    {
413
        return $this->except;
414
    }
415
416 3
    public function setExcept(array $except): self
417
    {
418 3
        $this->except = $except;
419 3
        return $this;
420
    }
421
422
    public function getLevels(): array
423
    {
424
        return $this->levels;
425
    }
426
427 9
    public function setLevels(array $levels): self
428
    {
429 9
        $this->levels = $levels;
430 9
        return $this;
431
    }
432
433
    public function getPrefix(): ?callable
434
    {
435
        return $this->prefix;
436
    }
437
438
    public function setPrefix(callable $prefix): self
439
    {
440
        $this->prefix = $prefix;
441
        return $this;
442
    }
443
444
    public function getExportInterval(): int
445
    {
446
        return $this->exportInterval;
447
    }
448
449 33
    public function setExportInterval(int $exportInterval): self
450
    {
451 33
        $this->exportInterval = $exportInterval;
452 33
        return $this;
453
    }
454
455 19
    public function getMessages(): array
456
    {
457 19
        return $this->messages;
458
    }
459
460 19
    public function setMessages(array $messages): self
461
    {
462 19
        $this->messages = $messages;
463 19
        return $this;
464
    }
465
}
466