Passed
Push — master ( ee8ded...d342fd )
by Sergei
13:57 queued 11:27
created

Logger   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 283
Duplicated Lines 0 %

Test Coverage

Coverage 97.56%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 81
c 2
b 1
f 0
dl 0
loc 283
ccs 80
cts 82
cp 0.9756
rs 10
wmc 28

11 Methods

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