Passed
Pull Request — master (#35)
by Alexander
01:44
created

Logger::prepareMessage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.576

Importance

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