Passed
Pull Request — master (#73)
by Rustam
14:49 queued 02:49
created

Logger   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Test Coverage

Coverage 97.18%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 81
c 2
b 0
f 0
dl 0
loc 273
ccs 69
cts 71
cp 0.9718
rs 10
wmc 28

11 Methods

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