LineFormatter.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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\Formatter;
  11. use Closure;
  12. use Monolog\Utils;
  13. use Monolog\LogRecord;
  14. /**
  15. * Formats incoming records into a one-line string
  16. *
  17. * This is especially useful for logging to files
  18. *
  19. * @author Jordi Boggiano <j.boggiano@seld.be>
  20. * @author Christophe Coevoet <stof@notk.org>
  21. */
  22. class LineFormatter extends NormalizerFormatter
  23. {
  24. public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
  25. protected string $format;
  26. protected bool $allowInlineLineBreaks;
  27. protected bool $ignoreEmptyContextAndExtra;
  28. protected bool $includeStacktraces;
  29. protected Closure|null $stacktracesParser = null;
  30. /**
  31. * @param string|null $format The format of the message
  32. * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
  33. * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries
  34. */
  35. public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
  36. {
  37. $this->format = $format === null ? static::SIMPLE_FORMAT : $format;
  38. $this->allowInlineLineBreaks = $allowInlineLineBreaks;
  39. $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
  40. $this->includeStacktraces($includeStacktraces);
  41. parent::__construct($dateFormat);
  42. }
  43. public function includeStacktraces(bool $include = true, ?Closure $parser = null): self
  44. {
  45. $this->includeStacktraces = $include;
  46. if ($this->includeStacktraces) {
  47. $this->allowInlineLineBreaks = true;
  48. $this->stacktracesParser = $parser;
  49. }
  50. return $this;
  51. }
  52. public function allowInlineLineBreaks(bool $allow = true): self
  53. {
  54. $this->allowInlineLineBreaks = $allow;
  55. return $this;
  56. }
  57. public function ignoreEmptyContextAndExtra(bool $ignore = true): self
  58. {
  59. $this->ignoreEmptyContextAndExtra = $ignore;
  60. return $this;
  61. }
  62. /**
  63. * @inheritDoc
  64. */
  65. public function format(LogRecord $record): string
  66. {
  67. $vars = parent::format($record);
  68. $output = $this->format;
  69. foreach ($vars['extra'] as $var => $val) {
  70. if (false !== strpos($output, '%extra.'.$var.'%')) {
  71. $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output);
  72. unset($vars['extra'][$var]);
  73. }
  74. }
  75. foreach ($vars['context'] as $var => $val) {
  76. if (false !== strpos($output, '%context.'.$var.'%')) {
  77. $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
  78. unset($vars['context'][$var]);
  79. }
  80. }
  81. if ($this->ignoreEmptyContextAndExtra) {
  82. if (\count($vars['context']) === 0) {
  83. unset($vars['context']);
  84. $output = str_replace('%context%', '', $output);
  85. }
  86. if (\count($vars['extra']) === 0) {
  87. unset($vars['extra']);
  88. $output = str_replace('%extra%', '', $output);
  89. }
  90. }
  91. foreach ($vars as $var => $val) {
  92. if (false !== strpos($output, '%'.$var.'%')) {
  93. $output = str_replace('%'.$var.'%', $this->stringify($val), $output);
  94. }
  95. }
  96. // remove leftover %extra.xxx% and %context.xxx% if any
  97. if (false !== strpos($output, '%')) {
  98. $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
  99. if (null === $output) {
  100. $pcreErrorCode = preg_last_error();
  101. throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
  102. }
  103. }
  104. return $output;
  105. }
  106. public function formatBatch(array $records): string
  107. {
  108. $message = '';
  109. foreach ($records as $record) {
  110. $message .= $this->format($record);
  111. }
  112. return $message;
  113. }
  114. /**
  115. * @param mixed $value
  116. */
  117. public function stringify($value): string
  118. {
  119. return $this->replaceNewlines($this->convertToString($value));
  120. }
  121. protected function normalizeException(\Throwable $e, int $depth = 0): string
  122. {
  123. $str = $this->formatException($e);
  124. if (($previous = $e->getPrevious()) instanceof \Throwable) {
  125. do {
  126. $depth++;
  127. if ($depth > $this->maxNormalizeDepth) {
  128. $str .= '\n[previous exception] Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
  129. break;
  130. }
  131. $str .= "\n[previous exception] " . $this->formatException($previous);
  132. } while ($previous = $previous->getPrevious());
  133. }
  134. return $str;
  135. }
  136. /**
  137. * @param mixed $data
  138. */
  139. protected function convertToString($data): string
  140. {
  141. if (null === $data || is_bool($data)) {
  142. return var_export($data, true);
  143. }
  144. if (is_scalar($data)) {
  145. return (string) $data;
  146. }
  147. return $this->toJson($data, true);
  148. }
  149. protected function replaceNewlines(string $str): string
  150. {
  151. if ($this->allowInlineLineBreaks) {
  152. if (0 === strpos($str, '{')) {
  153. return str_replace(['\r', '\n'], ["\r", "\n"], $str);
  154. }
  155. return $str;
  156. }
  157. return str_replace(["\r\n", "\r", "\n"], ' ', $str);
  158. }
  159. private function formatException(\Throwable $e): string
  160. {
  161. $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode();
  162. if ($e instanceof \SoapFault) {
  163. if (isset($e->faultcode)) {
  164. $str .= ' faultcode: ' . $e->faultcode;
  165. }
  166. if (isset($e->faultactor)) {
  167. $str .= ' faultactor: ' . $e->faultactor;
  168. }
  169. if (isset($e->detail)) {
  170. if (is_string($e->detail)) {
  171. $str .= ' detail: ' . $e->detail;
  172. } elseif (is_object($e->detail) || is_array($e->detail)) {
  173. $str .= ' detail: ' . $this->toJson($e->detail, true);
  174. }
  175. }
  176. }
  177. $str .= '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')';
  178. if ($this->includeStacktraces) {
  179. $str .= $this->stacktracesParser($e);
  180. }
  181. return $str;
  182. }
  183. private function stacktracesParser(\Throwable $e): string
  184. {
  185. $trace = $e->getTraceAsString();
  186. if ($this->stacktracesParser !== null) {
  187. $trace = $this->stacktracesParserCustom($trace);
  188. }
  189. return "\n[stacktrace]\n" . $trace . "\n";
  190. }
  191. private function stacktracesParserCustom(string $trace): string
  192. {
  193. return implode("\n", array_filter(array_map($this->stacktracesParser, explode("\n", $trace))));
  194. }
  195. }