Passed
Pull Request — master (#40)
by Wilmer
02:13 queued 11s
created

Logger::getFlushInterval()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log;
6
7
use Exception;
8
use Psr\Log\InvalidArgumentException;
9
use Psr\Log\LoggerInterface;
10
use Psr\Log\LoggerTrait;
11
use Psr\Log\LogLevel;
12
use Throwable;
13
use Yiisoft\VarDumper\VarDumper;
14
15
use function array_filter;
16
use function count;
17
use function debug_backtrace;
18
use function get_class;
19
use function is_scalar;
20
use function is_string;
21
use function memory_get_usage;
22
use function method_exists;
23
use function preg_replace_callback;
24
use function register_shutdown_function;
25
use function strpos;
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
class Logger implements LoggerInterface
39
{
40
    use LoggerTrait;
41
42
    /**
43
     * @var array logged messages. This property is managed by {@see Logger::log()} and {@see Logger::flush()}.
44
     * Each log message is of the following structure:
45
     *
46
     * ```
47
     * [
48
     *   [0] => level (string)
49
     *   [1] => message (mixed, can be a string or some complex data, such as an exception object)
50
     *   [2] => context (array)
51
     * ]
52
     * ```
53
     *
54
     * Message context has a following keys:
55
     *
56
     * - category: string, message category.
57
     * - time: float, message timestamp obtained by microtime(true).
58
     * - trace: array, debug backtrace, contains the application code call stacks.
59
     * - memory: int, memory usage in bytes, obtained by `memory_get_usage()`.
60
     */
61
    private array $messages = [];
62
63
    /**
64
     * @var int how many messages should be logged before they are flushed from memory and sent to targets.
65
     * Defaults to 1000, meaning the {@see Logger::flush()} method will be invoked once every 1000 messages logged.
66
     * Set this property to be 0 if you don't want to flush messages until the application terminates.
67
     * This property mainly affects how much memory will be taken by the logged messages.
68
     * A smaller value means less memory, but will increase the execution time due to the overhead of {@see Logger::flush()}.
69
     */
70
    private int $flushInterval = 1000;
71
72
    /**
73
     * @var int how much call stack information (file name and line number) should be logged for each message.
74
     * If it is greater than 0, at most that number of call stacks will be logged. Note that only application
75
     * call stacks are counted.
76
     */
77
    private int $traceLevel = 0;
78
79
    /**
80
     * @var array An array of paths to exclude from the trace when tracing is enabled using
81
     * {@see Logger::setTraceLevel()}.
82
     */
83
    private array $excludedTracePaths = [];
84
85
    /**
86
     * @var Target[] the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance
87
     */
88
    private array $targets = [];
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 21
    public function __construct(array $targets = [])
96
    {
97 21
        $this->setTargets($targets);
98
99 21
        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 21
        });
106 21
    }
107
108
    /**
109
     * @return Target[] the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance.
110
     */
111 21
    public function getTargets(): array
112
    {
113 21
        return $this->targets;
114
    }
115
116
    /**
117
     * @param int|string $name string name or integer index
118
     * @return Target|null
119
     */
120
    public function getTarget($name): ?Target
121
    {
122
        return $this->getTargets()[$name] ?? null;
123
    }
124
125
    /**
126
     * @param array $targets the log targets. Each array element represents a single {@see \Yiisoft\Log\Target}
127
     * instance or the configuration for creating the log target instance.
128
     */
129 22
    public function setTargets(array $targets): void
130
    {
131 22
        foreach ($targets as $target) {
132 21
            if (!$target instanceof Target) {
133
                throw new InvalidArgumentException('You must provide an instance of \Yiisoft\Log\Target.');
134
            }
135
        }
136 22
        $this->targets = $targets;
137 22
    }
138
139
    /**
140
     * Adds extra target to {@see Logger::$targets}.
141
     * @param Target $target the log target instance.
142
     * @param string|null $name array key to be used to store target, if `null` is given target will be append
143
     * to the end of the array by natural integer key.
144
     */
145 1
    public function addTarget(Target $target, string $name = null): void
146
    {
147 1
        if ($name === null) {
148 1
            $this->targets[] = $target;
149
        } else {
150 1
            $this->targets[$name] = $target;
151
        }
152 1
    }
153
154
    /**
155
     * Prepares message for logging.
156
     * @param mixed $message
157
     * @return string
158
     */
159 21
    public static function prepareMessage($message): string
160
    {
161 21
        if (method_exists($message, '__toString')) {
162
            return (string)$message;
163
        }
164
165 21
        if (is_scalar($message)) {
166 21
            return (string)$message;
167
        }
168
169
        return VarDumper::create($message)->export();
170
    }
171
172 24
    public function log($level, $message, array $context = []): void
173
    {
174 24
        if (($message instanceof Throwable) && !isset($context['exception'])) {
175
            // exceptions are string-convertible, thus should be passed as it is to the logger
176
            // if exception instance is given to produce a stack trace, it MUST be in a key named "exception".
177
            $context['exception'] = $message;
178
        }
179 24
        $message = static::prepareMessage($message);
180
181 24
        if (!isset($context['time'])) {
182 24
            $context['time'] = microtime(true);
183
        }
184 24
        if (!isset($context['trace'])) {
185 24
            $context['trace'] = $this->collectTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
186
        }
187
188 24
        if (!isset($context['memory'])) {
189 24
            $context['memory'] = memory_get_usage();
190
        }
191
192 24
        if (!isset($context['category'])) {
193 24
            $context['category'] = 'application';
194
        }
195
196 24
        $message = $this->parseMessage($message, $context);
197
198 24
        $this->messages[] = [$level, $message, $context];
199
200 24
        if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
201 21
            $this->flush();
202
        }
203 24
    }
204
205
    /**
206
     * Flushes log messages from memory to targets.
207
     * @param bool $final whether this is a final call during a request.
208
     */
209 22
    public function flush(bool $final = false): void
210
    {
211 22
        $messages = $this->messages;
212
        // https://github.com/yiisoft/yii2/issues/5619
213
        // new messages could be logged while the existing ones are being handled by targets
214 22
        $this->messages = [];
215
216 22
        $this->dispatch($messages, $final);
217 22
    }
218
219
    /**
220
     * Dispatches the logged messages to {@see Logger::$targets}.
221
     * @param array $messages the logged messages
222
     * @param bool $final whether this method is called at the end of the current application
223
     */
224 23
    protected function dispatch(array $messages, bool $final): void
225
    {
226 23
        $targetErrors = [];
227 23
        foreach ($this->getTargets() as $target) {
228 23
            if ($target->isEnabled()) {
229
                try {
230 22
                    $target->collect($messages, $final);
231 1
                } catch (Exception $e) {
232 1
                    $target->disable();
233 1
                    $targetErrors[] = [
234 1
                        'Unable to send log via ' . get_class($target) . ': ' . get_class($e) . ': ' . $e->getMessage(),
235
                        LogLevel::WARNING,
236
                        __METHOD__,
237 1
                        microtime(true),
238
                        [],
239
                    ];
240
                }
241
            }
242
        }
243
244 23
        if (!empty($targetErrors)) {
245 1
            $this->dispatch($targetErrors, true);
246
        }
247 23
    }
248
249
    /**
250
     * Parses log message resolving placeholders in the form: '{foo}', where foo
251
     * will be replaced by the context data in key "foo".
252
     * @param string $message log message.
253
     * @param array $context message context.
254
     * @return string parsed message.
255
     */
256 24
    protected function parseMessage(string $message, array $context): string
257
    {
258 24
        return preg_replace_callback('/{([\w.]+)}/', static function (array $matches) use ($context) {
259 2
            $placeholderName = $matches[1];
260 2
            if (isset($context[$placeholderName])) {
261 1
                return (string)$context[$placeholderName];
262
            }
263 1
            return $matches[0];
264 24
        }, $message);
265
    }
266
267
    /**
268
     * Returns the total elapsed time since the start of the current request.
269
     * This method calculates the difference between now and the start of the
270
     * request ($_SERVER['REQUEST_TIME_FLOAT']).
271
     * @return float the total elapsed time in seconds for current request.
272
     */
273 1
    public function getElapsedTime(): float
274
    {
275 1
        return \microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
276
    }
277
278
    /**
279
     * Returns the text display of the specified level.
280
     * @param mixed $level the message level, e.g. {@see LogLevel::ERROR}, {@see LogLevel::WARNING}.
281
     * @return string the text display of the level
282
     */
283 2
    public static function getLevelName($level): string
284
    {
285 2
        if (is_string($level)) {
286 2
            return $level;
287
        }
288 1
        return 'unknown';
289
    }
290
291
    public function getFlushInterval(): int
292
    {
293
        return $this->flushInterval;
294
    }
295
296 20
    public function setFlushInterval(int $flushInterval): self
297
    {
298 20
        $this->flushInterval = $flushInterval;
299 20
        return $this;
300
    }
301
302
    public function getTraceLevel(): int
303
    {
304
        return $this->traceLevel;
305
    }
306
307 1
    public function setTraceLevel(int $traceLevel): self
308
    {
309 1
        $this->traceLevel = $traceLevel;
310 1
        return $this;
311
    }
312
313 1
    public function setExcludedTracePaths(array $excludedTracePaths): self
314
    {
315 1
        $this->excludedTracePaths = $excludedTracePaths;
316 1
        return $this;
317
    }
318
319 21
    private function collectTrace(array $backtrace): array
320
    {
321 21
        $traces = [];
322 21
        if ($this->traceLevel > 0) {
323 1
            $count = 0;
324 1
            foreach ($backtrace as $trace) {
325 1
                if (isset($trace['file'], $trace['line'])) {
326 1
                    $excludedMatch = array_filter($this->excludedTracePaths, static function ($path) use ($trace) {
327 1
                        return strpos($trace['file'], $path) !== false;
328 1
                    });
329
330 1
                    if (empty($excludedMatch)) {
331 1
                        unset($trace['object'], $trace['args']);
332 1
                        $traces[] = $trace;
333 1
                        if (++$count >= $this->traceLevel) {
334
                            break;
335
                        }
336
                    }
337
                }
338
            }
339
        }
340 21
        return $traces;
341
    }
342
}
343