LineFormatter.php 7.5 KB

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