NormalizerFormatter.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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\DateTimeImmutable;
  12. use Monolog\Utils;
  13. use Throwable;
  14. use Monolog\LogRecord;
  15. /**
  16. * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
  17. *
  18. * @author Jordi Boggiano <j.boggiano@seld.be>
  19. */
  20. class NormalizerFormatter implements FormatterInterface
  21. {
  22. public const SIMPLE_DATE = "Y-m-d\TH:i:sP";
  23. protected string $dateFormat;
  24. protected int $maxNormalizeDepth = 9;
  25. protected int $maxNormalizeItemCount = 1000;
  26. private int $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS;
  27. /**
  28. * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format
  29. * @throws \RuntimeException If the function json_encode does not exist
  30. */
  31. public function __construct(?string $dateFormat = null)
  32. {
  33. $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat;
  34. if (!function_exists('json_encode')) {
  35. throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
  36. }
  37. }
  38. /**
  39. * @inheritDoc
  40. */
  41. public function format(LogRecord $record)
  42. {
  43. return $this->normalizeRecord($record);
  44. }
  45. /**
  46. * Normalize an arbitrary value to a scalar|array|null
  47. *
  48. * @return null|scalar|array<mixed[]|scalar|null>
  49. */
  50. public function normalizeValue(mixed $data): mixed
  51. {
  52. return $this->normalize($data);
  53. }
  54. /**
  55. * @inheritDoc
  56. */
  57. public function formatBatch(array $records)
  58. {
  59. foreach ($records as $key => $record) {
  60. $records[$key] = $this->format($record);
  61. }
  62. return $records;
  63. }
  64. public function getDateFormat(): string
  65. {
  66. return $this->dateFormat;
  67. }
  68. /**
  69. * @return $this
  70. */
  71. public function setDateFormat(string $dateFormat): self
  72. {
  73. $this->dateFormat = $dateFormat;
  74. return $this;
  75. }
  76. /**
  77. * The maximum number of normalization levels to go through
  78. */
  79. public function getMaxNormalizeDepth(): int
  80. {
  81. return $this->maxNormalizeDepth;
  82. }
  83. /**
  84. * @return $this
  85. */
  86. public function setMaxNormalizeDepth(int $maxNormalizeDepth): self
  87. {
  88. $this->maxNormalizeDepth = $maxNormalizeDepth;
  89. return $this;
  90. }
  91. /**
  92. * The maximum number of items to normalize per level
  93. */
  94. public function getMaxNormalizeItemCount(): int
  95. {
  96. return $this->maxNormalizeItemCount;
  97. }
  98. /**
  99. * @return $this
  100. */
  101. public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): self
  102. {
  103. $this->maxNormalizeItemCount = $maxNormalizeItemCount;
  104. return $this;
  105. }
  106. /**
  107. * Enables `json_encode` pretty print.
  108. *
  109. * @return $this
  110. */
  111. public function setJsonPrettyPrint(bool $enable): self
  112. {
  113. if ($enable) {
  114. $this->jsonEncodeOptions |= JSON_PRETTY_PRINT;
  115. } else {
  116. $this->jsonEncodeOptions &= ~JSON_PRETTY_PRINT;
  117. }
  118. return $this;
  119. }
  120. /**
  121. * Provided as extension point
  122. *
  123. * Because normalize is called with sub-values of context data etc, normalizeRecord can be
  124. * extended when data needs to be appended on the record array but not to other normalized data.
  125. *
  126. * @return array<mixed[]|scalar|null>
  127. */
  128. protected function normalizeRecord(LogRecord $record): array
  129. {
  130. /** @var array<mixed> $normalized */
  131. $normalized = $this->normalize($record->toArray());
  132. return $normalized;
  133. }
  134. /**
  135. * @return null|scalar|array<mixed[]|scalar|null>
  136. */
  137. protected function normalize(mixed $data, int $depth = 0): mixed
  138. {
  139. if ($depth > $this->maxNormalizeDepth) {
  140. return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
  141. }
  142. if (null === $data || is_scalar($data)) {
  143. if (is_float($data)) {
  144. if (is_infinite($data)) {
  145. return ($data > 0 ? '' : '-') . 'INF';
  146. }
  147. if (is_nan($data)) {
  148. return 'NaN';
  149. }
  150. }
  151. return $data;
  152. }
  153. if (is_array($data)) {
  154. $normalized = [];
  155. $count = 1;
  156. foreach ($data as $key => $value) {
  157. if ($count++ > $this->maxNormalizeItemCount) {
  158. $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization';
  159. break;
  160. }
  161. $normalized[$key] = $this->normalize($value, $depth + 1);
  162. }
  163. return $normalized;
  164. }
  165. if ($data instanceof \DateTimeInterface) {
  166. return $this->formatDate($data);
  167. }
  168. if (is_object($data)) {
  169. if ($data instanceof Throwable) {
  170. return $this->normalizeException($data, $depth);
  171. }
  172. if ($data instanceof \JsonSerializable) {
  173. /** @var null|scalar|array<mixed[]|scalar|null> $value */
  174. $value = $data->jsonSerialize();
  175. } elseif (\get_class($data) === '__PHP_Incomplete_Class') {
  176. $accessor = new \ArrayObject($data);
  177. $value = (string) $accessor['__PHP_Incomplete_Class_Name'];
  178. } elseif (method_exists($data, '__toString')) {
  179. try {
  180. /** @var string $value */
  181. $value = $data->__toString();
  182. } catch (\Throwable) {
  183. // if the toString method is failing, use the default behavior
  184. /** @var null|scalar|array<mixed[]|scalar|null> $value */
  185. $value = json_decode($this->toJson($data, true), true);
  186. }
  187. } else {
  188. // the rest is normalized by json encoding and decoding it
  189. /** @var null|scalar|array<mixed[]|scalar|null> $value */
  190. $value = json_decode($this->toJson($data, true), true);
  191. }
  192. return [Utils::getClass($data) => $value];
  193. }
  194. if (is_resource($data)) {
  195. return sprintf('[resource(%s)]', get_resource_type($data));
  196. }
  197. return '[unknown('.gettype($data).')]';
  198. }
  199. /**
  200. * @return mixed[]
  201. */
  202. protected function normalizeException(Throwable $e, int $depth = 0)
  203. {
  204. if ($depth > $this->maxNormalizeDepth) {
  205. return ['Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'];
  206. }
  207. if ($e instanceof \JsonSerializable) {
  208. return (array) $e->jsonSerialize();
  209. }
  210. $data = [
  211. 'class' => Utils::getClass($e),
  212. 'message' => $e->getMessage(),
  213. 'code' => (int) $e->getCode(),
  214. 'file' => $e->getFile().':'.$e->getLine(),
  215. ];
  216. if ($e instanceof \SoapFault) {
  217. if (isset($e->faultcode)) {
  218. $data['faultcode'] = $e->faultcode;
  219. }
  220. if (isset($e->faultactor)) {
  221. $data['faultactor'] = $e->faultactor;
  222. }
  223. if (isset($e->detail)) {
  224. if (is_string($e->detail)) {
  225. $data['detail'] = $e->detail;
  226. } elseif (is_object($e->detail) || is_array($e->detail)) {
  227. $data['detail'] = $this->toJson($e->detail, true);
  228. }
  229. }
  230. }
  231. $trace = $e->getTrace();
  232. foreach ($trace as $frame) {
  233. if (isset($frame['file'], $frame['line'])) {
  234. $data['trace'][] = $frame['file'].':'.$frame['line'];
  235. }
  236. }
  237. if (($previous = $e->getPrevious()) instanceof \Throwable) {
  238. $data['previous'] = $this->normalizeException($previous, $depth + 1);
  239. }
  240. return $data;
  241. }
  242. /**
  243. * Return the JSON representation of a value
  244. *
  245. * @param mixed $data
  246. * @throws \RuntimeException if encoding fails and errors are not ignored
  247. * @return string if encoding fails and ignoreErrors is true 'null' is returned
  248. */
  249. protected function toJson($data, bool $ignoreErrors = false): string
  250. {
  251. return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors);
  252. }
  253. protected function formatDate(\DateTimeInterface $date): string
  254. {
  255. // in case the date format isn't custom then we defer to the custom DateTimeImmutable
  256. // formatting logic, which will pick the right format based on whether useMicroseconds is on
  257. if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) {
  258. return (string) $date;
  259. }
  260. return $date->format($this->dateFormat);
  261. }
  262. /**
  263. * @return $this
  264. */
  265. public function addJsonEncodeOption(int $option): self
  266. {
  267. $this->jsonEncodeOptions |= $option;
  268. return $this;
  269. }
  270. /**
  271. * @return $this
  272. */
  273. public function removeJsonEncodeOption(int $option): self
  274. {
  275. $this->jsonEncodeOptions &= ~$option;
  276. return $this;
  277. }
  278. }