JsonFormatter.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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 Stringable;
  12. use Throwable;
  13. use Monolog\LogRecord;
  14. /**
  15. * Encodes whatever record data is passed to it as json
  16. *
  17. * This can be useful to log to databases or remote APIs
  18. *
  19. * @author Jordi Boggiano <j.boggiano@seld.be>
  20. */
  21. class JsonFormatter extends NormalizerFormatter
  22. {
  23. public const BATCH_MODE_JSON = 1;
  24. public const BATCH_MODE_NEWLINES = 2;
  25. /** @var self::BATCH_MODE_* */
  26. protected int $batchMode;
  27. protected bool $appendNewline;
  28. protected bool $ignoreEmptyContextAndExtra;
  29. protected bool $includeStacktraces = false;
  30. /**
  31. * @param self::BATCH_MODE_* $batchMode
  32. */
  33. public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
  34. {
  35. $this->batchMode = $batchMode;
  36. $this->appendNewline = $appendNewline;
  37. $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
  38. $this->includeStacktraces = $includeStacktraces;
  39. parent::__construct();
  40. }
  41. /**
  42. * The batch mode option configures the formatting style for
  43. * multiple records. By default, multiple records will be
  44. * formatted as a JSON-encoded array. However, for
  45. * compatibility with some API endpoints, alternative styles
  46. * are available.
  47. */
  48. public function getBatchMode(): int
  49. {
  50. return $this->batchMode;
  51. }
  52. /**
  53. * True if newlines are appended to every formatted record
  54. */
  55. public function isAppendingNewlines(): bool
  56. {
  57. return $this->appendNewline;
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. public function format(LogRecord $record): string
  63. {
  64. $normalized = parent::format($record);
  65. if (isset($normalized['context']) && $normalized['context'] === []) {
  66. if ($this->ignoreEmptyContextAndExtra) {
  67. unset($normalized['context']);
  68. } else {
  69. $normalized['context'] = new \stdClass;
  70. }
  71. }
  72. if (isset($normalized['extra']) && $normalized['extra'] === []) {
  73. if ($this->ignoreEmptyContextAndExtra) {
  74. unset($normalized['extra']);
  75. } else {
  76. $normalized['extra'] = new \stdClass;
  77. }
  78. }
  79. return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : '');
  80. }
  81. /**
  82. * @inheritDoc
  83. */
  84. public function formatBatch(array $records): string
  85. {
  86. return match ($this->batchMode) {
  87. static::BATCH_MODE_NEWLINES => $this->formatBatchNewlines($records),
  88. default => $this->formatBatchJson($records),
  89. };
  90. }
  91. public function includeStacktraces(bool $include = true): self
  92. {
  93. $this->includeStacktraces = $include;
  94. return $this;
  95. }
  96. /**
  97. * Return a JSON-encoded array of records.
  98. *
  99. * @phpstan-param LogRecord[] $records
  100. */
  101. protected function formatBatchJson(array $records): string
  102. {
  103. return $this->toJson($this->normalize($records), true);
  104. }
  105. /**
  106. * Use new lines to separate records instead of a
  107. * JSON-encoded array.
  108. *
  109. * @phpstan-param LogRecord[] $records
  110. */
  111. protected function formatBatchNewlines(array $records): string
  112. {
  113. $oldNewline = $this->appendNewline;
  114. $this->appendNewline = false;
  115. $formatted = array_map(fn (LogRecord $record) => $this->format($record), $records);
  116. $this->appendNewline = $oldNewline;
  117. return implode("\n", $formatted);
  118. }
  119. /**
  120. * Normalizes given $data.
  121. *
  122. * @return null|scalar|array<mixed[]|scalar|null|object>|object
  123. */
  124. protected function normalize(mixed $data, int $depth = 0): mixed
  125. {
  126. if ($depth > $this->maxNormalizeDepth) {
  127. return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization';
  128. }
  129. if (is_array($data)) {
  130. $normalized = [];
  131. $count = 1;
  132. foreach ($data as $key => $value) {
  133. if ($count++ > $this->maxNormalizeItemCount) {
  134. $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization';
  135. break;
  136. }
  137. $normalized[$key] = $this->normalize($value, $depth + 1);
  138. }
  139. return $normalized;
  140. }
  141. if (is_object($data)) {
  142. if ($data instanceof \DateTimeInterface) {
  143. return $this->formatDate($data);
  144. }
  145. if ($data instanceof Throwable) {
  146. return $this->normalizeException($data, $depth);
  147. }
  148. // if the object has specific json serializability we want to make sure we skip the __toString treatment below
  149. if ($data instanceof \JsonSerializable) {
  150. return $data;
  151. }
  152. if ($data instanceof Stringable) {
  153. return $data->__toString();
  154. }
  155. return $data;
  156. }
  157. if (is_resource($data)) {
  158. return parent::normalize($data);
  159. }
  160. return $data;
  161. }
  162. /**
  163. * Normalizes given exception with or without its own stack trace based on
  164. * `includeStacktraces` property.
  165. *
  166. * @inheritDoc
  167. */
  168. protected function normalizeException(Throwable $e, int $depth = 0): array
  169. {
  170. $data = parent::normalizeException($e, $depth);
  171. if (!$this->includeStacktraces) {
  172. unset($data['trace']);
  173. }
  174. return $data;
  175. }
  176. }