Passed
Push — master ( f85746...831e44 )
by Alexander
07:11
created

Logger   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Test Coverage

Coverage 86.67%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 90
dl 0
loc 304
ccs 91
cts 105
cp 0.8667
rs 9.0399
c 1
b 0
f 0
wmc 42

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setTargets() 0 8 3
A getTarget() 0 3 1
A getTargets() 0 3 1
A addTarget() 0 6 2
A prepareMessage() 0 11 3
A dispatch() 0 22 5
A getFlushInterval() 0 3 1
A getLevelName() 0 6 2
A flush() 0 8 1
A getTraceLevel() 0 3 1
A setFlushInterval() 0 4 1
A setExcludedTracePaths() 0 4 1
B log() 0 32 9
A setTraceLevel() 0 4 1
A getElapsedTime() 0 3 1
A parseMessage() 0 9 2
A collectTrace() 0 22 6
A __construct() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like Logger often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Logger, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Yiisoft\Log;
4
5
use Psr\Log\InvalidArgumentException;
6
use Psr\Log\LoggerInterface;
7
use Psr\Log\LoggerTrait;
8
use Psr\Log\LogLevel;
9
use Yiisoft\VarDumper\VarDumper;
10
11
/**
12
 * Logger records logged messages in memory and sends them to different targets according to {@see Logger::$targets}.
13
 *
14
 * You can call the method {@see Logger::log()} to record a single log message.
15
 *
16
 * For more details and usage information on Logger,
17
 * see [PSR-3 specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
18
 *
19
 * When the application ends or {@see Logger::$flushInterval} is reached, Logger will call {@see Logger::flush()}
20
 * to send logged messages to different log targets, such as file or email according to the {@see Logger::$targets}.
21
 */
22
class Logger implements LoggerInterface
23
{
24
    use LoggerTrait;
25
26
    /**
27
     * @var array logged messages. This property is managed by {@see Logger::log()} and {@see Logger::flush()}.
28
     * Each log message is of the following structure:
29
     *
30
     * ```
31
     * [
32
     *   [0] => level (string)
33
     *   [1] => message (mixed, can be a string or some complex data, such as an exception object)
34
     *   [2] => context (array)
35
     * ]
36
     * ```
37
     *
38
     * Message context has a following keys:
39
     *
40
     * - category: string, message category.
41
     * - time: float, message timestamp obtained by microtime(true).
42
     * - trace: array, debug backtrace, contains the application code call stacks.
43
     * - memory: int, memory usage in bytes, obtained by `memory_get_usage()`.
44
     */
45
    private $messages = [];
46
47
    /**
48
     * @var int how many messages should be logged before they are flushed from memory and sent to targets.
49
     * Defaults to 1000, meaning the {@see Logger::flush()} method will be invoked once every 1000 messages logged.
50
     * Set this property to be 0 if you don't want to flush messages until the application terminates.
51
     * This property mainly affects how much memory will be taken by the logged messages.
52
     * A smaller value means less memory, but will increase the execution time due to the overhead of {@see Logger::flush()}.
53
     */
54
    private $flushInterval = 1000;
55
56
    /**
57
     * @var int how much call stack information (file name and line number) should be logged for each message.
58
     * If it is greater than 0, at most that number of call stacks will be logged. Note that only application
59
     * call stacks are counted.
60
     */
61
    private $traceLevel = 0;
62
63
    /**
64
     * @var array An array of paths to exclude from the trace when tracing is enabled using {@see Logger::setTraceLevel()}.
65
     */
66
    private $excludedTracePaths = [];
67
68
    /**
69
     * @var Target[] the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance
70
     */
71
    private $targets = [];
72
73
    /**
74
     * Initializes the logger by registering {@see Logger::flush()} as a shutdown function.
75
     *
76
     * @param Target[] $targets the log targets.
77
     */
78 21
    public function __construct(array $targets = [])
79
    {
80 21
        $this->setTargets($targets);
81
82 21
        \register_shutdown_function(function () {
83
            // make regular flush before other shutdown functions, which allows session data collection and so on
84
            $this->flush();
85
            // make sure log entries written by shutdown functions are also flushed
86
            // ensure "flush()" is called last when there are multiple shutdown functions
87
            \register_shutdown_function([$this, 'flush'], true);
88 21
        });
89 21
    }
90
91
    /**
92
     * @return Target[] the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance.
93
     */
94 21
    public function getTargets(): array
95
    {
96 21
        return $this->targets;
97
    }
98
99
    /**
100
     * @param int|string $name string name or integer index
101
     * @return Target|null
102
     */
103
    public function getTarget($name): ?Target
104
    {
105
        return $this->getTargets()[$name] ?? null;
106
    }
107
108
    /**
109
     * @param Target[] $targets the log targets. Each array element represents a single {@see \Yiisoft\Log\Target} instance
110
     * or the configuration for creating the log target instance.
111
     */
112 22
    public function setTargets(array $targets): void
113
    {
114 22
        foreach ($targets as $target) {
115 21
            if (!$target instanceof Target) {
116
                throw new InvalidArgumentException('You must provide an instance of \Yiisoft\Log\Target.');
117
            }
118
        }
119 22
        $this->targets = $targets;
120 22
    }
121
122
    /**
123
     * Adds extra target to {@see Logger::$targets}.
124
     * @param Target $target the log target instance.
125
     * @param string|null $name array key to be used to store target, if `null` is given target will be append
126
     * to the end of the array by natural integer key.
127
     */
128 1
    public function addTarget(Target $target, string $name = null): void
129
    {
130 1
        if ($name === null) {
131 1
            $this->targets[] = $target;
132
        } else {
133 1
            $this->targets[$name] = $target;
134
        }
135 1
    }
136
137
    /**
138
     * Prepares message for logging.
139
     * @param mixed $message
140
     * @return string
141
     */
142 21
    public static function prepareMessage($message): string
143
    {
144 21
        if (method_exists($message, '__toString')) {
145
            return (string)$message;
146
        }
147
148 21
        if (is_scalar($message)) {
149 21
            return (string)$message;
150
        }
151
152
        return VarDumper::export($message);
0 ignored issues
show
Unused Code introduced by
The call to Yiisoft\VarDumper\VarDumper::export() has too many arguments starting with $message. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        return VarDumper::/** @scrutinizer ignore-call */ export($message);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug Best Practice introduced by
The method Yiisoft\VarDumper\VarDumper::export() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        return VarDumper::/** @scrutinizer ignore-call */ export($message);
Loading history...
153
    }
154
155 24
    public function log($level, $message, array $context = []): void
156
    {
157 24
        if ($message instanceof \Throwable) {
158
            if (!isset($context['exception'])) {
159
                // exceptions are string-convertible, thus should be passed as it is to the logger
160
                // if exception instance is given to produce a stack trace, it MUST be in a key named "exception".
161
                $context['exception'] = $message;
162
            }
163
        }
164 24
        $message = static::prepareMessage($message);
165
166 24
        if (!isset($context['time'])) {
167 24
            $context['time'] = microtime(true);
168
        }
169 24
        if (!isset($context['trace'])) {
170 24
            $context['trace'] = $this->collectTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
171
        }
172
173 24
        if (!isset($context['memory'])) {
174 24
            $context['memory'] = memory_get_usage();
175
        }
176
177 24
        if (!isset($context['category'])) {
178 24
            $context['category'] = 'application';
179
        }
180
181 24
        $message = $this->parseMessage($message, $context);
182
183 24
        $this->messages[] = [$level, $message, $context];
184
185 24
        if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
186 21
            $this->flush();
187
        }
188 24
    }
189
190
    /**
191
     * Flushes log messages from memory to targets.
192
     * @param bool $final whether this is a final call during a request.
193
     */
194 22
    public function flush(bool $final = false): void
195
    {
196 22
        $messages = $this->messages;
197
        // https://github.com/yiisoft/yii2/issues/5619
198
        // new messages could be logged while the existing ones are being handled by targets
199 22
        $this->messages = [];
200
201 22
        $this->dispatch($messages, $final);
202 22
    }
203
204
    /**
205
     * Dispatches the logged messages to {@see Logger::$targets}.
206
     * @param array $messages the logged messages
207
     * @param bool $final whether this method is called at the end of the current application
208
     */
209 23
    protected function dispatch(array $messages, bool $final): void
210
    {
211 23
        $targetErrors = [];
212 23
        foreach ($this->getTargets() as $target) {
213 23
            if ($target->isEnabled()) {
214
                try {
215 22
                    $target->collect($messages, $final);
216 1
                } catch (\Exception $e) {
217 1
                    $target->disable();
218 1
                    $targetErrors[] = [
219 1
                        'Unable to send log via ' . get_class($target) . ': ' . get_class($e) . ': ' . $e->getMessage(),
220
                        LogLevel::WARNING,
221
                        __METHOD__,
222 1
                        microtime(true),
223
                        [],
224
                    ];
225
                }
226
            }
227
        }
228
229 23
        if (!empty($targetErrors)) {
230 1
            $this->dispatch($targetErrors, true);
231
        }
232 23
    }
233
234
    /**
235
     * Parses log message resolving placeholders in the form: '{foo}', where foo
236
     * will be replaced by the context data in key "foo".
237
     * @param string $message log message.
238
     * @param array $context message context.
239
     * @return string parsed message.
240
     */
241 24
    protected function parseMessage(string $message, array $context): string
242
    {
243 24
        return preg_replace_callback('/{([\w.]+)}/', static function ($matches) use ($context) {
244 2
            $placeholderName = $matches[1];
245 2
            if (isset($context[$placeholderName])) {
246 1
                return (string)$context[$placeholderName];
247
            }
248 1
            return $matches[0];
249 24
        }, $message);
250
    }
251
252
    /**
253
     * Returns the total elapsed time since the start of the current request.
254
     * This method calculates the difference between now and the start of the
255
     * request ($_SERVER['REQUEST_TIME_FLOAT']).
256
     * @return float the total elapsed time in seconds for current request.
257
     */
258 1
    public function getElapsedTime(): float
259
    {
260 1
        return \microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
261
    }
262
263
    /**
264
     * Returns the text display of the specified level.
265
     * @param mixed $level the message level, e.g. {@see LogLevel::ERROR}, {@see LogLevel::WARNING}.
266
     * @return string the text display of the level
267
     */
268 2
    public static function getLevelName($level): string
269
    {
270 2
        if (is_string($level)) {
271 2
            return $level;
272
        }
273 1
        return 'unknown';
274
    }
275
276
    public function getFlushInterval(): int
277
    {
278
        return $this->flushInterval;
279
    }
280
281 20
    public function setFlushInterval(int $flushInterval): self
282
    {
283 20
        $this->flushInterval = $flushInterval;
284 20
        return $this;
285
    }
286
287
    public function getTraceLevel(): int
288
    {
289
        return $this->traceLevel;
290
    }
291
292 1
    public function setTraceLevel(int $traceLevel): self
293
    {
294 1
        $this->traceLevel = $traceLevel;
295 1
        return $this;
296
    }
297
298 1
    public function setExcludedTracePaths(array $excludedTracePaths): self
299
    {
300 1
        $this->excludedTracePaths = $excludedTracePaths;
301 1
        return $this;
302
    }
303
304 21
    private function collectTrace(array $backtrace): array
305
    {
306 21
        $traces = [];
307 21
        if ($this->traceLevel > 0) {
308 1
            $count = 0;
309 1
            foreach ($backtrace as $trace) {
310 1
                if (isset($trace['file'], $trace['line'])) {
311 1
                    $excludedMatch = array_filter($this->excludedTracePaths, static function ($path) use ($trace) {
312 1
                        return strpos($trace['file'], $path) !== false;
313 1
                    });
314
315 1
                    if (empty($excludedMatch)) {
316 1
                        unset($trace['object'], $trace['args']);
317 1
                        $traces[] = $trace;
318 1
                        if (++$count >= $this->traceLevel) {
319
                            break;
320
                        }
321
                    }
322
                }
323
            }
324
        }
325 21
        return $traces;
326
    }
327
}
328