Passed
Push — master ( dc96af...1f8a30 )
by Alexander
02:08
created

Logger::log()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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