Passed
Pull Request — master (#49)
by Evgeniy
01:57
created

Logger::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1.037

Importance

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