LineFormatter.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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 ?int $maxLevelNameLength = null;
  30. protected string $indentStacktraces = '';
  31. protected Closure|null $stacktracesParser = null;
  32. protected string $basePath = '';
  33. /**
  34. * @param string|null $format The format of the message
  35. * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
  36. * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries
  37. *
  38. * @throws \RuntimeException If the function json_encode does not exist
  39. */
  40. public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
  41. {
  42. $this->format = $format === null ? static::SIMPLE_FORMAT : $format;
  43. $this->allowInlineLineBreaks = $allowInlineLineBreaks;
  44. $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
  45. $this->includeStacktraces($includeStacktraces);
  46. parent::__construct($dateFormat);
  47. }
  48. /**
  49. * Setting a base path will hide the base path from exception and stack trace file names to shorten them
  50. * @return $this
  51. */
  52. public function setBasePath(string $path = ''): self
  53. {
  54. if ($path !== '') {
  55. $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
  56. }
  57. $this->basePath = $path;
  58. return $this;
  59. }
  60. /**
  61. * @return $this
  62. */
  63. public function includeStacktraces(bool $include = true, ?Closure $parser = null): self
  64. {
  65. $this->includeStacktraces = $include;
  66. if ($this->includeStacktraces) {
  67. $this->allowInlineLineBreaks = true;
  68. $this->stacktracesParser = $parser;
  69. }
  70. return $this;
  71. }
  72. /**
  73. * Indent stack traces to separate them a bit from the main log record messages
  74. *
  75. * @param string $indent The string used to indent, for example " "
  76. * @return $this
  77. */
  78. public function indentStacktraces(string $indent): self
  79. {
  80. $this->indentStacktraces = $indent;
  81. return $this;
  82. }
  83. /**
  84. * @return $this
  85. */
  86. public function allowInlineLineBreaks(bool $allow = true): self
  87. {
  88. $this->allowInlineLineBreaks = $allow;
  89. return $this;
  90. }
  91. /**
  92. * @return $this
  93. */
  94. public function ignoreEmptyContextAndExtra(bool $ignore = true): self
  95. {
  96. $this->ignoreEmptyContextAndExtra = $ignore;
  97. return $this;
  98. }
  99. /**
  100. * Allows cutting the level name to get fixed-length levels like INF for INFO, ERR for ERROR if you set this to 3 for example
  101. *
  102. * @param int|null $maxLevelNameLength Maximum characters for the level name. Set null for infinite length (default)
  103. * @return $this
  104. */
  105. public function setMaxLevelNameLength(?int $maxLevelNameLength = null): self
  106. {
  107. $this->maxLevelNameLength = $maxLevelNameLength;
  108. return $this;
  109. }
  110. /**
  111. * @inheritDoc
  112. */
  113. public function format(LogRecord $record): string
  114. {
  115. $vars = parent::format($record);
  116. if ($this->maxLevelNameLength !== null) {
  117. $vars['level_name'] = substr($vars['level_name'], 0, $this->maxLevelNameLength);
  118. }
  119. $output = $this->format;
  120. foreach ($vars['extra'] as $var => $val) {
  121. if (false !== strpos($output, '%extra.'.$var.'%')) {
  122. $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output);
  123. unset($vars['extra'][$var]);
  124. }
  125. }
  126. foreach ($vars['context'] as $var => $val) {
  127. if (false !== strpos($output, '%context.'.$var.'%')) {
  128. $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
  129. unset($vars['context'][$var]);
  130. }
  131. }
  132. if ($this->ignoreEmptyContextAndExtra) {
  133. if (\count($vars['context']) === 0) {
  134. unset($vars['context']);
  135. $output = str_replace('%context%', '', $output);
  136. }
  137. if (\count($vars['extra']) === 0) {
  138. unset($vars['extra']);
  139. $output = str_replace('%extra%', '', $output);
  140. }
  141. }
  142. foreach ($vars as $var => $val) {
  143. if (false !== strpos($output, '%'.$var.'%')) {
  144. $output = str_replace('%'.$var.'%', $this->stringify($val), $output);
  145. }
  146. }
  147. // remove leftover %extra.xxx% and %context.xxx% if any
  148. if (false !== strpos($output, '%')) {
  149. $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
  150. if (null === $output) {
  151. $pcreErrorCode = preg_last_error();
  152. throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
  153. }
  154. }
  155. return $output;
  156. }
  157. public function formatBatch(array $records): string
  158. {
  159. $message = '';
  160. foreach ($records as $record) {
  161. $message .= $this->format($record);
  162. }
  163. return $message;
  164. }
  165. /**
  166. * @param mixed $value
  167. */
  168. public function stringify($value): string
  169. {
  170. return $this->replaceNewlines($this->convertToString($value));
  171. }
  172. protected function normalizeException(\Throwable $e, int $depth = 0): string
  173. {
  174. $str = $this->formatException($e);
  175. if (($previous = $e->getPrevious()) instanceof \Throwable) {
  176. do {
  177. $depth++;
  178. if ($depth > $this->maxNormalizeDepth) {
  179. $str .= "\n[previous exception] Over " . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
  180. break;
  181. }
  182. $str .= "\n[previous exception] " . $this->formatException($previous);
  183. } while ($previous = $previous->getPrevious());
  184. }
  185. return $str;
  186. }
  187. /**
  188. * @param mixed $data
  189. */
  190. protected function convertToString($data): string
  191. {
  192. if (null === $data || \is_bool($data)) {
  193. return var_export($data, true);
  194. }
  195. if (\is_scalar($data)) {
  196. return (string) $data;
  197. }
  198. return $this->toJson($data, true);
  199. }
  200. protected function replaceNewlines(string $str): string
  201. {
  202. if ($this->allowInlineLineBreaks) {
  203. if (0 === strpos($str, '{') || 0 === strpos($str, '[')) {
  204. $str = preg_replace('/(?<!\\\\)\\\\[rn]/', "\n", $str);
  205. if (null === $str) {
  206. $pcreErrorCode = preg_last_error();
  207. throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
  208. }
  209. }
  210. return $str;
  211. }
  212. return str_replace(["\r\n", "\r", "\n"], ' ', $str);
  213. }
  214. private function formatException(\Throwable $e): string
  215. {
  216. $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode();
  217. if ($e instanceof \SoapFault) {
  218. if (isset($e->faultcode)) {
  219. $str .= ' faultcode: ' . $e->faultcode;
  220. }
  221. if (isset($e->faultactor)) {
  222. $str .= ' faultactor: ' . $e->faultactor;
  223. }
  224. if (isset($e->detail)) {
  225. if (\is_string($e->detail)) {
  226. $str .= ' detail: ' . $e->detail;
  227. } elseif (\is_object($e->detail) || \is_array($e->detail)) {
  228. $str .= ' detail: ' . $this->toJson($e->detail, true);
  229. }
  230. }
  231. }
  232. $file = $e->getFile();
  233. if ($this->basePath !== '') {
  234. $file = preg_replace('{^'.preg_quote($this->basePath).'}', '', $file);
  235. }
  236. $str .= '): ' . $e->getMessage() . ' at ' . $file . ':' . $e->getLine() . ')';
  237. if ($this->includeStacktraces) {
  238. $str .= $this->stacktracesParser($e);
  239. }
  240. return $str;
  241. }
  242. private function stacktracesParser(\Throwable $e): string
  243. {
  244. $trace = $e->getTraceAsString();
  245. if ($this->basePath !== '') {
  246. $trace = preg_replace('{^(#\d+ )' . preg_quote($this->basePath) . '}m', '$1', $trace) ?? $trace;
  247. }
  248. if ($this->stacktracesParser !== null) {
  249. $trace = $this->stacktracesParserCustom($trace);
  250. }
  251. if ($this->indentStacktraces !== '') {
  252. $trace = str_replace("\n", "\n{$this->indentStacktraces}", $trace);
  253. }
  254. return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . $trace . "\n";
  255. }
  256. private function stacktracesParserCustom(string $trace): string
  257. {
  258. return implode("\n", array_filter(array_map($this->stacktracesParser, explode("\n", $trace))));
  259. }
  260. }