1<?php declare(strict_types=1);
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog;
13
14use DateTimeZone;
15use Monolog\Handler\HandlerInterface;
16use Psr\Log\LoggerInterface;
17use Psr\Log\InvalidArgumentException;
18use Psr\Log\LogLevel;
19use Throwable;
20use Stringable;
21
22/**
23 * Monolog log channel
24 *
25 * It contains a stack of Handlers and a stack of Processors,
26 * and uses them to store records that are added to it.
27 *
28 * @author Jordi Boggiano <j.boggiano@seld.be>
29 *
30 * @phpstan-type Level Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY
31 * @phpstan-type LevelName 'DEBUG'|'INFO'|'NOTICE'|'WARNING'|'ERROR'|'CRITICAL'|'ALERT'|'EMERGENCY'
32 * @phpstan-type Record array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[]}
33 */
34class Logger implements LoggerInterface, ResettableInterface
35{
36    /**
37     * Detailed debug information
38     */
39    public const DEBUG = 100;
40
41    /**
42     * Interesting events
43     *
44     * Examples: User logs in, SQL logs.
45     */
46    public const INFO = 200;
47
48    /**
49     * Uncommon events
50     */
51    public const NOTICE = 250;
52
53    /**
54     * Exceptional occurrences that are not errors
55     *
56     * Examples: Use of deprecated APIs, poor use of an API,
57     * undesirable things that are not necessarily wrong.
58     */
59    public const WARNING = 300;
60
61    /**
62     * Runtime errors
63     */
64    public const ERROR = 400;
65
66    /**
67     * Critical conditions
68     *
69     * Example: Application component unavailable, unexpected exception.
70     */
71    public const CRITICAL = 500;
72
73    /**
74     * Action must be taken immediately
75     *
76     * Example: Entire website down, database unavailable, etc.
77     * This should trigger the SMS alerts and wake you up.
78     */
79    public const ALERT = 550;
80
81    /**
82     * Urgent alert.
83     */
84    public const EMERGENCY = 600;
85
86    /**
87     * Monolog API version
88     *
89     * This is only bumped when API breaks are done and should
90     * follow the major version of the library
91     *
92     * @var int
93     */
94    public const API = 2;
95
96    /**
97     * This is a static variable and not a constant to serve as an extension point for custom levels
98     *
99     * @var array<int, string> $levels Logging levels with the levels as key
100     *
101     * @phpstan-var array<Level, LevelName> $levels Logging levels with the levels as key
102     */
103    protected static $levels = [
104        self::DEBUG     => 'DEBUG',
105        self::INFO      => 'INFO',
106        self::NOTICE    => 'NOTICE',
107        self::WARNING   => 'WARNING',
108        self::ERROR     => 'ERROR',
109        self::CRITICAL  => 'CRITICAL',
110        self::ALERT     => 'ALERT',
111        self::EMERGENCY => 'EMERGENCY',
112    ];
113
114    /**
115     * @var string
116     */
117    protected $name;
118
119    /**
120     * The handler stack
121     *
122     * @var HandlerInterface[]
123     */
124    protected $handlers;
125
126    /**
127     * Processors that will process all log records
128     *
129     * To process records of a single handler instead, add the processor on that specific handler
130     *
131     * @var callable[]
132     */
133    protected $processors;
134
135    /**
136     * @var bool
137     */
138    protected $microsecondTimestamps = true;
139
140    /**
141     * @var DateTimeZone
142     */
143    protected $timezone;
144
145    /**
146     * @var callable|null
147     */
148    protected $exceptionHandler;
149
150    /**
151     * @psalm-param array<callable(array): array> $processors
152     *
153     * @param string             $name       The logging channel, a simple descriptive name that is attached to all log records
154     * @param HandlerInterface[] $handlers   Optional stack of handlers, the first one in the array is called first, etc.
155     * @param callable[]         $processors Optional array of processors
156     * @param DateTimeZone|null  $timezone   Optional timezone, if not provided date_default_timezone_get() will be used
157     */
158    public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null)
159    {
160        $this->name = $name;
161        $this->setHandlers($handlers);
162        $this->processors = $processors;
163        $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC');
164    }
165
166    public function getName(): string
167    {
168        return $this->name;
169    }
170
171    /**
172     * Return a new cloned instance with the name changed
173     */
174    public function withName(string $name): self
175    {
176        $new = clone $this;
177        $new->name = $name;
178
179        return $new;
180    }
181
182    /**
183     * Pushes a handler on to the stack.
184     */
185    public function pushHandler(HandlerInterface $handler): self
186    {
187        array_unshift($this->handlers, $handler);
188
189        return $this;
190    }
191
192    /**
193     * Pops a handler from the stack
194     *
195     * @throws \LogicException If empty handler stack
196     */
197    public function popHandler(): HandlerInterface
198    {
199        if (!$this->handlers) {
200            throw new \LogicException('You tried to pop from an empty handler stack.');
201        }
202
203        return array_shift($this->handlers);
204    }
205
206    /**
207     * Set handlers, replacing all existing ones.
208     *
209     * If a map is passed, keys will be ignored.
210     *
211     * @param HandlerInterface[] $handlers
212     */
213    public function setHandlers(array $handlers): self
214    {
215        $this->handlers = [];
216        foreach (array_reverse($handlers) as $handler) {
217            $this->pushHandler($handler);
218        }
219
220        return $this;
221    }
222
223    /**
224     * @return HandlerInterface[]
225     */
226    public function getHandlers(): array
227    {
228        return $this->handlers;
229    }
230
231    /**
232     * Adds a processor on to the stack.
233     */
234    public function pushProcessor(callable $callback): self
235    {
236        array_unshift($this->processors, $callback);
237
238        return $this;
239    }
240
241    /**
242     * Removes the processor on top of the stack and returns it.
243     *
244     * @throws \LogicException If empty processor stack
245     * @return callable
246     */
247    public function popProcessor(): callable
248    {
249        if (!$this->processors) {
250            throw new \LogicException('You tried to pop from an empty processor stack.');
251        }
252
253        return array_shift($this->processors);
254    }
255
256    /**
257     * @return callable[]
258     */
259    public function getProcessors(): array
260    {
261        return $this->processors;
262    }
263
264    /**
265     * Control the use of microsecond resolution timestamps in the 'datetime'
266     * member of new records.
267     *
268     * As of PHP7.1 microseconds are always included by the engine, so
269     * there is no performance penalty and Monolog 2 enabled microseconds
270     * by default. This function lets you disable them though in case you want
271     * to suppress microseconds from the output.
272     *
273     * @param bool $micro True to use microtime() to create timestamps
274     */
275    public function useMicrosecondTimestamps(bool $micro): self
276    {
277        $this->microsecondTimestamps = $micro;
278
279        return $this;
280    }
281
282    /**
283     * Adds a log record.
284     *
285     * @param  int     $level   The logging level
286     * @param  string  $message The log message
287     * @param  mixed[] $context The log context
288     * @return bool    Whether the record has been processed
289     *
290     * @phpstan-param Level $level
291     */
292    public function addRecord(int $level, string $message, array $context = []): bool
293    {
294        $record = null;
295
296        foreach ($this->handlers as $handler) {
297            if (null === $record) {
298                // skip creating the record as long as no handler is going to handle it
299                if (!$handler->isHandling(['level' => $level])) {
300                    continue;
301                }
302
303                $levelName = static::getLevelName($level);
304
305                $record = [
306                    'message' => $message,
307                    'context' => $context,
308                    'level' => $level,
309                    'level_name' => $levelName,
310                    'channel' => $this->name,
311                    'datetime' => new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
312                    'extra' => [],
313                ];
314
315                try {
316                    foreach ($this->processors as $processor) {
317                        $record = $processor($record);
318                    }
319                } catch (Throwable $e) {
320                    $this->handleException($e, $record);
321
322                    return true;
323                }
324            }
325
326            // once the record exists, send it to all handlers as long as the bubbling chain is not interrupted
327            try {
328                if (true === $handler->handle($record)) {
329                    break;
330                }
331            } catch (Throwable $e) {
332                $this->handleException($e, $record);
333
334                return true;
335            }
336        }
337
338        return null !== $record;
339    }
340
341    /**
342     * Ends a log cycle and frees all resources used by handlers.
343     *
344     * Closing a Handler means flushing all buffers and freeing any open resources/handles.
345     * Handlers that have been closed should be able to accept log records again and re-open
346     * themselves on demand, but this may not always be possible depending on implementation.
347     *
348     * This is useful at the end of a request and will be called automatically on every handler
349     * when they get destructed.
350     */
351    public function close(): void
352    {
353        foreach ($this->handlers as $handler) {
354            $handler->close();
355        }
356    }
357
358    /**
359     * Ends a log cycle and resets all handlers and processors to their initial state.
360     *
361     * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal
362     * state, and getting it back to a state in which it can receive log records again.
363     *
364     * This is useful in case you want to avoid logs leaking between two requests or jobs when you
365     * have a long running process like a worker or an application server serving multiple requests
366     * in one process.
367     */
368    public function reset(): void
369    {
370        foreach ($this->handlers as $handler) {
371            if ($handler instanceof ResettableInterface) {
372                $handler->reset();
373            }
374        }
375
376        foreach ($this->processors as $processor) {
377            if ($processor instanceof ResettableInterface) {
378                $processor->reset();
379            }
380        }
381    }
382
383    /**
384     * Gets all supported logging levels.
385     *
386     * @return array<string, int> Assoc array with human-readable level names => level codes.
387     * @phpstan-return array<LevelName, Level>
388     */
389    public static function getLevels(): array
390    {
391        return array_flip(static::$levels);
392    }
393
394    /**
395     * Gets the name of the logging level.
396     *
397     * @throws \Psr\Log\InvalidArgumentException If level is not defined
398     *
399     * @phpstan-param  Level     $level
400     * @phpstan-return LevelName
401     */
402    public static function getLevelName(int $level): string
403    {
404        if (!isset(static::$levels[$level])) {
405            throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels)));
406        }
407
408        return static::$levels[$level];
409    }
410
411    /**
412     * Converts PSR-3 levels to Monolog ones if necessary
413     *
414     * @param  string|int                        $level Level number (monolog) or name (PSR-3)
415     * @throws \Psr\Log\InvalidArgumentException If level is not defined
416     *
417     * @phpstan-param  Level|LevelName|LogLevel::* $level
418     * @phpstan-return Level
419     */
420    public static function toMonologLevel($level): int
421    {
422        if (is_string($level)) {
423            if (is_numeric($level)) {
424                /** @phpstan-ignore-next-line */
425                return intval($level);
426            }
427
428            // Contains chars of all log levels and avoids using strtoupper() which may have
429            // strange results depending on locale (for example, "i" will become "İ" in Turkish locale)
430            $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY');
431            if (defined(__CLASS__.'::'.$upper)) {
432                return constant(__CLASS__ . '::' . $upper);
433            }
434
435            throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels));
436        }
437
438        if (!is_int($level)) {
439            throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels));
440        }
441
442        return $level;
443    }
444
445    /**
446     * Checks whether the Logger has a handler that listens on the given level
447     *
448     * @phpstan-param Level $level
449     */
450    public function isHandling(int $level): bool
451    {
452        $record = [
453            'level' => $level,
454        ];
455
456        foreach ($this->handlers as $handler) {
457            if ($handler->isHandling($record)) {
458                return true;
459            }
460        }
461
462        return false;
463    }
464
465    /**
466     * Set a custom exception handler that will be called if adding a new record fails
467     *
468     * The callable will receive an exception object and the record that failed to be logged
469     */
470    public function setExceptionHandler(?callable $callback): self
471    {
472        $this->exceptionHandler = $callback;
473
474        return $this;
475    }
476
477    public function getExceptionHandler(): ?callable
478    {
479        return $this->exceptionHandler;
480    }
481
482    /**
483     * Adds a log record at an arbitrary level.
484     *
485     * This method allows for compatibility with common interfaces.
486     *
487     * @param mixed             $level   The log level
488     * @param string|Stringable $message The log message
489     * @param mixed[]           $context The log context
490     *
491     * @phpstan-param Level|LevelName|LogLevel::* $level
492     */
493    public function log($level, $message, array $context = []): void
494    {
495        if (!is_int($level) && !is_string($level)) {
496            throw new \InvalidArgumentException('$level is expected to be a string or int');
497        }
498
499        $level = static::toMonologLevel($level);
500
501        $this->addRecord($level, (string) $message, $context);
502    }
503
504    /**
505     * Adds a log record at the DEBUG level.
506     *
507     * This method allows for compatibility with common interfaces.
508     *
509     * @param string|Stringable $message The log message
510     * @param mixed[]           $context The log context
511     */
512    public function debug($message, array $context = []): void
513    {
514        $this->addRecord(static::DEBUG, (string) $message, $context);
515    }
516
517    /**
518     * Adds a log record at the INFO level.
519     *
520     * This method allows for compatibility with common interfaces.
521     *
522     * @param string|Stringable $message The log message
523     * @param mixed[]           $context The log context
524     */
525    public function info($message, array $context = []): void
526    {
527        $this->addRecord(static::INFO, (string) $message, $context);
528    }
529
530    /**
531     * Adds a log record at the NOTICE level.
532     *
533     * This method allows for compatibility with common interfaces.
534     *
535     * @param string|Stringable $message The log message
536     * @param mixed[]           $context The log context
537     */
538    public function notice($message, array $context = []): void
539    {
540        $this->addRecord(static::NOTICE, (string) $message, $context);
541    }
542
543    /**
544     * Adds a log record at the WARNING level.
545     *
546     * This method allows for compatibility with common interfaces.
547     *
548     * @param string|Stringable $message The log message
549     * @param mixed[]           $context The log context
550     */
551    public function warning($message, array $context = []): void
552    {
553        $this->addRecord(static::WARNING, (string) $message, $context);
554    }
555
556    /**
557     * Adds a log record at the ERROR level.
558     *
559     * This method allows for compatibility with common interfaces.
560     *
561     * @param string|Stringable $message The log message
562     * @param mixed[]           $context The log context
563     */
564    public function error($message, array $context = []): void
565    {
566        $this->addRecord(static::ERROR, (string) $message, $context);
567    }
568
569    /**
570     * Adds a log record at the CRITICAL level.
571     *
572     * This method allows for compatibility with common interfaces.
573     *
574     * @param string|Stringable $message The log message
575     * @param mixed[]           $context The log context
576     */
577    public function critical($message, array $context = []): void
578    {
579        $this->addRecord(static::CRITICAL, (string) $message, $context);
580    }
581
582    /**
583     * Adds a log record at the ALERT level.
584     *
585     * This method allows for compatibility with common interfaces.
586     *
587     * @param string|Stringable $message The log message
588     * @param mixed[]           $context The log context
589     */
590    public function alert($message, array $context = []): void
591    {
592        $this->addRecord(static::ALERT, (string) $message, $context);
593    }
594
595    /**
596     * Adds a log record at the EMERGENCY level.
597     *
598     * This method allows for compatibility with common interfaces.
599     *
600     * @param string|Stringable $message The log message
601     * @param mixed[]           $context The log context
602     */
603    public function emergency($message, array $context = []): void
604    {
605        $this->addRecord(static::EMERGENCY, (string) $message, $context);
606    }
607
608    /**
609     * Sets the timezone to be used for the timestamp of log records.
610     */
611    public function setTimezone(DateTimeZone $tz): self
612    {
613        $this->timezone = $tz;
614
615        return $this;
616    }
617
618    /**
619     * Returns the timezone to be used for the timestamp of log records.
620     */
621    public function getTimezone(): DateTimeZone
622    {
623        return $this->timezone;
624    }
625
626    /**
627     * Delegates exception management to the custom exception handler,
628     * or throws the exception if no custom handler is set.
629     *
630     * @param array $record
631     * @phpstan-param Record $record
632     */
633    protected function handleException(Throwable $e, array $record): void
634    {
635        if (!$this->exceptionHandler) {
636            throw $e;
637        }
638
639        ($this->exceptionHandler)($e, $record);
640    }
641}
642