Logger.php 20 KB

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