JsonFormatterTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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\Level;
  12. use Monolog\LogRecord;
  13. use Monolog\Test\TestCase;
  14. class JsonFormatterTest extends TestCase
  15. {
  16. /**
  17. * @covers Monolog\Formatter\JsonFormatter::__construct
  18. * @covers Monolog\Formatter\JsonFormatter::getBatchMode
  19. * @covers Monolog\Formatter\JsonFormatter::isAppendingNewlines
  20. */
  21. public function testConstruct()
  22. {
  23. $formatter = new JsonFormatter();
  24. $this->assertEquals(JsonFormatter::BATCH_MODE_JSON, $formatter->getBatchMode());
  25. $this->assertEquals(true, $formatter->isAppendingNewlines());
  26. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_NEWLINES, false);
  27. $this->assertEquals(JsonFormatter::BATCH_MODE_NEWLINES, $formatter->getBatchMode());
  28. $this->assertEquals(false, $formatter->isAppendingNewlines());
  29. }
  30. /**
  31. * @covers Monolog\Formatter\JsonFormatter::format
  32. */
  33. public function testFormat()
  34. {
  35. $formatter = new JsonFormatter();
  36. $record = $this->getRecord();
  37. $this->assertEquals(json_encode($record->toArray(), JSON_FORCE_OBJECT)."\n", $formatter->format($record));
  38. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
  39. $record = $this->getRecord();
  40. $this->assertEquals('{"message":"test","context":{},"level":300,"level_name":"WARNING","channel":"test","datetime":"'.$record->datetime->format('Y-m-d\TH:i:s.uP').'","extra":{}}', $formatter->format($record));
  41. }
  42. /**
  43. * @covers Monolog\Formatter\JsonFormatter::format
  44. */
  45. public function testFormatWithPrettyPrint()
  46. {
  47. $formatter = new JsonFormatter();
  48. $formatter->setJsonPrettyPrint(true);
  49. $record = $this->getRecord();
  50. $this->assertEquals(json_encode($record->toArray(), JSON_PRETTY_PRINT | JSON_FORCE_OBJECT)."\n", $formatter->format($record));
  51. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
  52. $formatter->setJsonPrettyPrint(true);
  53. $record = $this->getRecord();
  54. $this->assertEquals(
  55. '{
  56. "message": "test",
  57. "context": {},
  58. "level": 300,
  59. "level_name": "WARNING",
  60. "channel": "test",
  61. "datetime": "'.$record->datetime->format('Y-m-d\TH:i:s.uP').'",
  62. "extra": {}
  63. }',
  64. $formatter->format($record)
  65. );
  66. $formatter->setJsonPrettyPrint(false);
  67. $record = $this->getRecord();
  68. $this->assertEquals('{"message":"test","context":{},"level":300,"level_name":"WARNING","channel":"test","datetime":"'.$record->datetime->format('Y-m-d\TH:i:s.uP').'","extra":{}}', $formatter->format($record));
  69. }
  70. /**
  71. * @covers Monolog\Formatter\JsonFormatter::formatBatch
  72. * @covers Monolog\Formatter\JsonFormatter::formatBatchJson
  73. */
  74. public function testFormatBatch()
  75. {
  76. $formatter = new JsonFormatter();
  77. $records = [
  78. $this->getRecord(Level::Warning),
  79. $this->getRecord(Level::Debug),
  80. ];
  81. $this->assertEquals(json_encode($records), $formatter->formatBatch($records));
  82. }
  83. /**
  84. * @covers Monolog\Formatter\JsonFormatter::formatBatch
  85. * @covers Monolog\Formatter\JsonFormatter::formatBatchNewlines
  86. */
  87. public function testFormatBatchNewlines()
  88. {
  89. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_NEWLINES);
  90. $records = [
  91. $this->getRecord(Level::Warning),
  92. $this->getRecord(Level::Debug),
  93. ];
  94. $expected = array_map(fn (LogRecord $record) => json_encode($record->toArray(), JSON_FORCE_OBJECT), $records);
  95. $this->assertEquals(implode("\n", $expected), $formatter->formatBatch($records));
  96. }
  97. public function testDefFormatWithException()
  98. {
  99. $formatter = new JsonFormatter();
  100. $exception = new \RuntimeException('Foo');
  101. $formattedException = $this->formatException($exception);
  102. $message = $this->formatRecordWithExceptionInContext($formatter, $exception);
  103. $this->assertContextContainsFormattedException($formattedException, $message);
  104. }
  105. public function testDefFormatWithPreviousException()
  106. {
  107. $formatter = new JsonFormatter();
  108. $exception = new \RuntimeException('Foo', 0, new \LogicException('Wut?'));
  109. $formattedPrevException = $this->formatException($exception->getPrevious());
  110. $formattedException = $this->formatException($exception, $formattedPrevException);
  111. $message = $this->formatRecordWithExceptionInContext($formatter, $exception);
  112. $this->assertContextContainsFormattedException($formattedException, $message);
  113. }
  114. public function testDefFormatWithThrowable()
  115. {
  116. $formatter = new JsonFormatter();
  117. $throwable = new \Error('Foo');
  118. $formattedThrowable = $this->formatException($throwable);
  119. $message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
  120. $this->assertContextContainsFormattedException($formattedThrowable, $message);
  121. }
  122. public function testMaxNormalizeDepth()
  123. {
  124. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true);
  125. $formatter->setMaxNormalizeDepth(1);
  126. $throwable = new \Error('Foo');
  127. $message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
  128. $this->assertContextContainsFormattedException('"Over 1 levels deep, aborting normalization"', $message);
  129. }
  130. public function testMaxNormalizeItemCountWith0ItemsMax()
  131. {
  132. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true);
  133. $formatter->setMaxNormalizeDepth(9);
  134. $formatter->setMaxNormalizeItemCount(0);
  135. $throwable = new \Error('Foo');
  136. $message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
  137. $this->assertEquals(
  138. '{"...":"Over 0 items (7 total), aborting normalization"}'."\n",
  139. $message
  140. );
  141. }
  142. public function testMaxNormalizeItemCountWith2ItemsMax()
  143. {
  144. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true);
  145. $formatter->setMaxNormalizeDepth(9);
  146. $formatter->setMaxNormalizeItemCount(2);
  147. $throwable = new \Error('Foo');
  148. $message = $this->formatRecordWithExceptionInContext($formatter, $throwable);
  149. $this->assertEquals(
  150. '{"message":"foobar","context":{"exception":{"class":"Error","message":"Foo","code":0,"file":"'.__FILE__.':'.(__LINE__ - 5).'"}},"...":"Over 2 items (7 total), aborting normalization"}'."\n",
  151. $message
  152. );
  153. }
  154. public function testDefFormatWithResource()
  155. {
  156. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
  157. $record = $this->getRecord(
  158. context: ['field_resource' => opendir(__DIR__)],
  159. );
  160. $this->assertEquals('{"message":"test","context":{"field_resource":"[resource(stream)]"},"level":300,"level_name":"WARNING","channel":"test","datetime":"'.$record->datetime->format('Y-m-d\TH:i:s.uP').'","extra":{}}', $formatter->format($record));
  161. }
  162. /**
  163. * @param string $expected
  164. * @param string $actual
  165. *
  166. * @internal param string $exception
  167. */
  168. private function assertContextContainsFormattedException($expected, $actual)
  169. {
  170. $this->assertEquals(
  171. '{"message":"foobar","context":{"exception":'.$expected.'},"level":500,"level_name":"CRITICAL","channel":"core","datetime":"2022-02-22T00:00:00+00:00","extra":{}}'."\n",
  172. $actual
  173. );
  174. }
  175. /**
  176. * @param JsonFormatter $formatter
  177. * @param \Throwable $exception
  178. *
  179. * @return string
  180. */
  181. private function formatRecordWithExceptionInContext(JsonFormatter $formatter, \Throwable $exception)
  182. {
  183. $message = $formatter->format($this->getRecord(
  184. Level::Critical,
  185. 'foobar',
  186. channel: 'core',
  187. context: ['exception' => $exception],
  188. datetime: new \DateTimeImmutable('2022-02-22 00:00:00'),
  189. ));
  190. return $message;
  191. }
  192. /**
  193. * @param \Exception|\Throwable $exception
  194. *
  195. * @return string
  196. */
  197. private function formatExceptionFilePathWithLine($exception)
  198. {
  199. $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
  200. $path = substr(json_encode($exception->getFile(), $options), 1, -1);
  201. return $path . ':' . $exception->getLine();
  202. }
  203. /**
  204. * @param \Exception|\Throwable $exception
  205. *
  206. * @param null|string $previous
  207. *
  208. * @return string
  209. */
  210. private function formatException($exception, $previous = null)
  211. {
  212. $formattedException =
  213. '{"class":"' . get_class($exception) .
  214. '","message":"' . $exception->getMessage() .
  215. '","code":' . $exception->getCode() .
  216. ',"file":"' . $this->formatExceptionFilePathWithLine($exception) .
  217. ($previous ? '","previous":' . $previous : '"') .
  218. '}';
  219. return $formattedException;
  220. }
  221. public function testNormalizeHandleLargeArraysWithExactly1000Items()
  222. {
  223. $formatter = new NormalizerFormatter();
  224. $largeArray = range(1, 1000);
  225. $res = $formatter->format($this->getRecord(
  226. Level::Critical,
  227. 'bar',
  228. channel: 'test',
  229. context: array($largeArray),
  230. ));
  231. $this->assertCount(1000, $res['context'][0]);
  232. $this->assertArrayNotHasKey('...', $res['context'][0]);
  233. }
  234. public function testNormalizeHandleLargeArrays()
  235. {
  236. $formatter = new NormalizerFormatter();
  237. $largeArray = range(1, 2000);
  238. $res = $formatter->format($this->getRecord(
  239. Level::Critical,
  240. 'bar',
  241. channel: 'test',
  242. context: array($largeArray),
  243. ));
  244. $this->assertCount(1001, $res['context'][0]);
  245. $this->assertEquals('Over 1000 items (2000 total), aborting normalization', $res['context'][0]['...']);
  246. }
  247. public function testEmptyContextAndExtraFieldsCanBeIgnored()
  248. {
  249. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true, true);
  250. $record = $formatter->format($this->getRecord(
  251. Level::Debug,
  252. 'Testing',
  253. channel: 'test',
  254. datetime: new \DateTimeImmutable('2022-02-22 00:00:00'),
  255. ));
  256. $this->assertSame(
  257. '{"message":"Testing","level":100,"level_name":"DEBUG","channel":"test","datetime":"2022-02-22T00:00:00+00:00"}'."\n",
  258. $record
  259. );
  260. }
  261. }