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