Passed
Pull Request — master (#87)
by Sergei
07:59 queued 05:40
created

Logger::setTraceLevel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log;
6
7
use InvalidArgumentException;
8
use Psr\Log\LoggerInterface;
9
use Psr\Log\LoggerTrait;
10
use Psr\Log\LogLevel;
11
use Stringable;
12
use Throwable;
13
use Yiisoft\Log\Message\CategoryFilter;
14
15
use function array_filter;
16
use function count;
17
use function debug_backtrace;
18
use function gettype;
19
use function implode;
20
use function in_array;
21
use function is_string;
22
use function memory_get_usage;
23
use function microtime;
24
use function register_shutdown_function;
25
use function sprintf;
26
27
/**
28
 * Logger records logged messages in memory and sends them to different targets according to {@see Logger::$targets}.
29
 *
30
 * You can call the method {@see Logger::log()} to record a single log message.
31
 *
32
 * For more details and usage information on Logger,
33
 * see [PSR-3 specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
34
 *
35
 * When the application ends or {@see Logger::$flushInterval} is reached, Logger will call {@see Logger::flush()}
36
 * to send logged messages to different log targets, such as file or email according to the {@see Logger::$targets}.
37
 *
38
 * @psalm-import-type Backtrace from Message
39
 * @psalm-import-type LogMessageContext from Message
40
 */
41
final class Logger implements LoggerInterface
42
{
43
    use LoggerTrait;
44
45
    /**
46
     * The list of log message levels. See {@see LogLevel} constants for valid level names.
47
     */
48
    private const LEVELS = [
49
        LogLevel::EMERGENCY,
50
        LogLevel::ALERT,
51
        LogLevel::CRITICAL,
52
        LogLevel::ERROR,
53
        LogLevel::WARNING,
54
        LogLevel::NOTICE,
55
        LogLevel::INFO,
56
        LogLevel::DEBUG,
57
    ];
58
59
    /**
60
     * @var Message[] The log messages.
61
     */
62
    private array $messages = [];
63
64
    /**
65
     * @var Target[] the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance.
66
     */
67
    private array $targets = [];
68
69
    /**
70
     * @var string[] Array of paths to exclude from tracing when tracing is enabled with {@see Logger::setTraceLevel()}.
71
     */
72
    private array $excludedTracePaths = [];
73
74
    /**
75
     * @var int How many log messages should be logged before they are flushed from memory and sent to targets.
76
     *
77
     * Defaults to 1000, meaning the {@see Logger::flush()} method will be invoked once every 1000 messages logged.
78
     * Set this property to be 0 if you don't want to flush messages until the application terminates.
79
     * This property mainly affects how much memory will be taken by the logged messages.
80
     * A smaller value means less memory, but will increase the execution
81
     * time due to the overhead of {@see Logger::flush()}.
82
     */
83
    private int $flushInterval = 1000;
84
85
    /**
86
     * @var int How much call stack information (file name and line number) should be logged for each log message.
87
     *
88
     * If it is greater than 0, at most that number of call stacks will be logged.
89
     * Note that only application call stacks are counted.
90
     */
91
    private int $traceLevel = 0;
92
93
    /**
94
     * Initializes the logger by registering {@see Logger::flush()} as a shutdown function.
95
     *
96
     * @param Target[] $targets The log targets.
97
     */
98 56
    public function __construct(array $targets = [])
99
    {
100 56
        $this->setTargets($targets);
101
102 56
        register_shutdown_function(function () {
103
            // make regular flush before other shutdown functions, which allows session data collection and so on
104
            $this->flush();
105
            // make sure log entries written by shutdown functions are also flushed
106
            // ensure "flush()" is called last when there are multiple shutdown functions
107
            register_shutdown_function([$this, 'flush'], true);
108 56
        });
109
    }
110
111
    /**
112
     * Returns the text display of the specified level.
113
     *
114
     * @param mixed $level The message level, e.g. {@see LogLevel::ERROR}, {@see LogLevel::WARNING}.
115
     *
116
     * @throws \Psr\Log\InvalidArgumentException for invalid log message level.
117
     *
118
     * @return string The text display of the level.
119
     */
120 149
    public static function validateLevel(mixed $level): string
121
    {
122 149
        if (!is_string($level)) {
123 13
            throw new \Psr\Log\InvalidArgumentException(sprintf(
124 13
                'The log message level must be a string, %s provided.',
125 13
                gettype($level)
126 13
            ));
127
        }
128
129 136
        if (!in_array($level, self::LEVELS, true)) {
130 2
            throw new \Psr\Log\InvalidArgumentException(sprintf(
131 2
                'Invalid log message level "%s" provided. The following values are supported: "%s".',
132 2
                $level,
133 2
                implode('", "', self::LEVELS)
134 2
            ));
135
        }
136
137 134
        return $level;
138
    }
139
140
    /**
141
     * @return Target[] The log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance.
142
     */
143 1
    public function getTargets(): array
144
    {
145 1
        return $this->targets;
146
    }
147
148
    /**
149
     * @param string $level
150
     * @psalm-param LogMessageContext $context
151
     * @psalm-suppress MoreSpecificImplementedParamType
152
     */
153 31
    public function log(mixed $level, string|Stringable $message, array $context = []): void
154
    {
155 31
        $context['time'] ??= microtime(true);
156 31
        $context['trace'] ??= $this->collectTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
157 31
        $context['memory'] ??= memory_get_usage();
158 31
        $context['category'] ??= CategoryFilter::DEFAULT;
159
160 31
        $this->messages[] = new Message($level, $message, $context);
161
162 31
        if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
163 21
            $this->flush();
164
        }
165
    }
166
167
    /**
168
     * Flushes log messages from memory to targets.
169
     *
170
     * @param bool $final Whether this is a final call during a request.
171
     */
172 23
    public function flush(bool $final = false): void
173
    {
174 23
        $messages = $this->messages;
175
        // https://github.com/yiisoft/yii2/issues/5619
176
        // new messages could be logged while the existing ones are being handled by targets
177 23
        $this->messages = [];
178
179 23
        $this->dispatch($messages, $final);
180
    }
181
182
    /**
183
     * Sets how many log messages should be logged before they are flushed from memory and sent to targets.
184
     *
185
     * @param int $flushInterval The number of messages to accumulate before flushing.
186
     *
187
     * @see Logger::$flushInterval
188
     */
189 21
    public function setFlushInterval(int $flushInterval): self
190
    {
191 21
        $this->flushInterval = $flushInterval;
192 21
        return $this;
193
    }
194
195
    /**
196
     * Sets how much call stack information (file name and line number) should be logged for each log message.
197
     *
198
     * @param int $traceLevel The number of call stack information.
199
     *
200
     * @see Logger::$traceLevel
201
     */
202 2
    public function setTraceLevel(int $traceLevel): self
203
    {
204 2
        $this->traceLevel = $traceLevel;
205 2
        return $this;
206
    }
207
208
    /**
209
     * Sets an array of paths to exclude from tracing when tracing is enabled with {@see Logger::setTraceLevel()}.
210
     *
211
     * @param string[] $excludedTracePaths The paths to exclude from tracing.
212
     *
213
     * @throws InvalidArgumentException for non-string values.
214
     *
215
     * @see Logger::$excludedTracePaths
216
     */
217 8
    public function setExcludedTracePaths(array $excludedTracePaths): self
218
    {
219 8
        foreach ($excludedTracePaths as $excludedTracePath) {
220
            /** @psalm-suppress DocblockTypeContradiction */
221 8
            if (!is_string($excludedTracePath)) {
222 7
                throw new InvalidArgumentException(sprintf(
223 7
                    'The trace path must be a string, %s received.',
224 7
                    gettype($excludedTracePath)
225 7
                ));
226
            }
227
        }
228
229 1
        $this->excludedTracePaths = $excludedTracePaths;
230 1
        return $this;
231
    }
232
233
    /**
234
     * Sets a target to {@see Logger::$targets}.
235
     *
236
     * @param Target[] $targets The log targets. Each array element represents a single {@see \Yiisoft\Log\Target}
237
     * instance or the configuration for creating the log target instance.
238
     *
239
     * @throws InvalidArgumentException for non-instance Target.
240
     */
241 56
    private function setTargets(array $targets): void
242
    {
243 56
        foreach ($targets as $target) {
244 56
            if (!($target instanceof Target)) {
245 8
                throw new InvalidArgumentException('You must provide an instance of \Yiisoft\Log\Target.');
246
            }
247
        }
248
249 56
        $this->targets = $targets;
250
    }
251
252
    /**
253
     * Dispatches the logged messages to {@see Logger::$targets}.
254
     *
255
     * @param Message[] $messages The log messages.
256
     * @param bool $final Whether this method is called at the end of the current application.
257
     */
258 26
    private function dispatch(array $messages, bool $final): void
259
    {
260 26
        $targetErrors = [];
261
262 26
        foreach ($this->targets as $target) {
263 26
            if ($target->isEnabled()) {
264
                try {
265 25
                    $target->collect($messages, $final);
266 1
                } catch (Throwable $e) {
267 1
                    $target->disable();
268 1
                    $targetErrors[] = new Message(
269 1
                        LogLevel::WARNING,
270 1
                        'Unable to send log via ' . $target::class . ': ' . $e::class . ': ' . $e->getMessage(),
271 1
                        ['time' => microtime(true), 'exception' => $e],
272 1
                    );
273
                }
274
            }
275
        }
276
277 26
        if (!empty($targetErrors)) {
278 1
            $this->dispatch($targetErrors, true);
279
        }
280
    }
281
282
    /**
283
     * Collects a trace when tracing is enabled with {@see Logger::setTraceLevel()}.
284
     *
285
     * @param array $backtrace The list of call stack information.
286
     * @psalm-param Backtrace|list<array{object?:object,args?:array}> $backtrace
287
     *
288
     * @return array Collected a list of call stack information.
289
     * @psalm-return Backtrace
290
     */
291 31
    private function collectTrace(array $backtrace): array
292
    {
293 31
        $traces = [];
294
295 31
        if ($this->traceLevel > 0) {
296 2
            $count = 0;
297
298 2
            foreach ($backtrace as $trace) {
299 2
                if (isset($trace['file'], $trace['line'])) {
300 2
                    $excludedMatch = array_filter(
301 2
                        $this->excludedTracePaths,
302 2
                        static fn ($path) => str_contains($trace['file'], $path)
303 2
                    );
304
305 2
                    if (empty($excludedMatch)) {
306 2
                        unset($trace['object'], $trace['args']);
307 2
                        $traces[] = $trace;
308 2
                        if (++$count >= $this->traceLevel) {
309 1
                            break;
310
                        }
311
                    }
312
                }
313
            }
314
        }
315
316 31
        return $traces;
317
    }
318
}
319