1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog\Formatter; 13 14use Throwable; 15 16/** 17 * Encodes whatever record data is passed to it as json 18 * 19 * This can be useful to log to databases or remote APIs 20 * 21 * @author Jordi Boggiano <j.boggiano@seld.be> 22 * 23 * @phpstan-import-type Record from \Monolog\Logger 24 */ 25class JsonFormatter extends NormalizerFormatter 26{ 27 public const BATCH_MODE_JSON = 1; 28 public const BATCH_MODE_NEWLINES = 2; 29 30 /** @var self::BATCH_MODE_* */ 31 protected $batchMode; 32 /** @var bool */ 33 protected $appendNewline; 34 /** @var bool */ 35 protected $ignoreEmptyContextAndExtra; 36 /** @var bool */ 37 protected $includeStacktraces = false; 38 39 /** 40 * @param self::BATCH_MODE_* $batchMode 41 */ 42 public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) 43 { 44 $this->batchMode = $batchMode; 45 $this->appendNewline = $appendNewline; 46 $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; 47 $this->includeStacktraces = $includeStacktraces; 48 49 parent::__construct(); 50 } 51 52 /** 53 * The batch mode option configures the formatting style for 54 * multiple records. By default, multiple records will be 55 * formatted as a JSON-encoded array. However, for 56 * compatibility with some API endpoints, alternative styles 57 * are available. 58 */ 59 public function getBatchMode(): int 60 { 61 return $this->batchMode; 62 } 63 64 /** 65 * True if newlines are appended to every formatted record 66 */ 67 public function isAppendingNewlines(): bool 68 { 69 return $this->appendNewline; 70 } 71 72 /** 73 * {@inheritDoc} 74 */ 75 public function format(array $record): string 76 { 77 $normalized = $this->normalize($record); 78 79 if (isset($normalized['context']) && $normalized['context'] === []) { 80 if ($this->ignoreEmptyContextAndExtra) { 81 unset($normalized['context']); 82 } else { 83 $normalized['context'] = new \stdClass; 84 } 85 } 86 if (isset($normalized['extra']) && $normalized['extra'] === []) { 87 if ($this->ignoreEmptyContextAndExtra) { 88 unset($normalized['extra']); 89 } else { 90 $normalized['extra'] = new \stdClass; 91 } 92 } 93 94 return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : ''); 95 } 96 97 /** 98 * {@inheritDoc} 99 */ 100 public function formatBatch(array $records): string 101 { 102 switch ($this->batchMode) { 103 case static::BATCH_MODE_NEWLINES: 104 return $this->formatBatchNewlines($records); 105 106 case static::BATCH_MODE_JSON: 107 default: 108 return $this->formatBatchJson($records); 109 } 110 } 111 112 /** 113 * @return self 114 */ 115 public function includeStacktraces(bool $include = true): self 116 { 117 $this->includeStacktraces = $include; 118 119 return $this; 120 } 121 122 /** 123 * Return a JSON-encoded array of records. 124 * 125 * @phpstan-param Record[] $records 126 */ 127 protected function formatBatchJson(array $records): string 128 { 129 return $this->toJson($this->normalize($records), true); 130 } 131 132 /** 133 * Use new lines to separate records instead of a 134 * JSON-encoded array. 135 * 136 * @phpstan-param Record[] $records 137 */ 138 protected function formatBatchNewlines(array $records): string 139 { 140 $instance = $this; 141 142 $oldNewline = $this->appendNewline; 143 $this->appendNewline = false; 144 array_walk($records, function (&$value, $key) use ($instance) { 145 $value = $instance->format($value); 146 }); 147 $this->appendNewline = $oldNewline; 148 149 return implode("\n", $records); 150 } 151 152 /** 153 * Normalizes given $data. 154 * 155 * @param mixed $data 156 * 157 * @return mixed 158 */ 159 protected function normalize($data, int $depth = 0) 160 { 161 if ($depth > $this->maxNormalizeDepth) { 162 return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization'; 163 } 164 165 if (is_array($data)) { 166 $normalized = []; 167 168 $count = 1; 169 foreach ($data as $key => $value) { 170 if ($count++ > $this->maxNormalizeItemCount) { 171 $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization'; 172 break; 173 } 174 175 $normalized[$key] = $this->normalize($value, $depth + 1); 176 } 177 178 return $normalized; 179 } 180 181 if ($data instanceof \DateTimeInterface) { 182 return $this->formatDate($data); 183 } 184 185 if ($data instanceof Throwable) { 186 return $this->normalizeException($data, $depth); 187 } 188 189 if (is_resource($data)) { 190 return parent::normalize($data); 191 } 192 193 return $data; 194 } 195 196 /** 197 * Normalizes given exception with or without its own stack trace based on 198 * `includeStacktraces` property. 199 * 200 * {@inheritDoc} 201 */ 202 protected function normalizeException(Throwable $e, int $depth = 0): array 203 { 204 $data = parent::normalizeException($e, $depth); 205 if (!$this->includeStacktraces) { 206 unset($data['trace']); 207 } 208 209 return $data; 210 } 211} 212