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

Logger   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Test Coverage

Coverage 97.94%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 91
dl 0
loc 313
ccs 95
cts 97
cp 0.9794
rs 9.6
c 1
b 0
f 0
wmc 35

15 Methods

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