JsonFormatterTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. * @return string
  177. */
  178. private function formatRecordWithExceptionInContext(JsonFormatter $formatter, \Throwable $exception)
  179. {
  180. $message = $formatter->format($this->getRecord(
  181. Level::Critical,
  182. 'foobar',
  183. channel: 'core',
  184. context: ['exception' => $exception],
  185. datetime: new \DateTimeImmutable('2022-02-22 00:00:00'),
  186. ));
  187. return $message;
  188. }
  189. /**
  190. * @param \Exception|\Throwable $exception
  191. *
  192. * @return string
  193. */
  194. private function formatExceptionFilePathWithLine($exception)
  195. {
  196. $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
  197. $path = substr(json_encode($exception->getFile(), $options), 1, -1);
  198. return $path . ':' . $exception->getLine();
  199. }
  200. /**
  201. * @param \Exception|\Throwable $exception
  202. *
  203. * @param null|string $previous
  204. *
  205. * @return string
  206. */
  207. private function formatException($exception, $previous = null)
  208. {
  209. $formattedException =
  210. '{"class":"' . get_class($exception) .
  211. '","message":"' . $exception->getMessage() .
  212. '","code":' . $exception->getCode() .
  213. ',"file":"' . $this->formatExceptionFilePathWithLine($exception) .
  214. ($previous ? '","previous":' . $previous : '"') .
  215. '}';
  216. return $formattedException;
  217. }
  218. public function testNormalizeHandleLargeArraysWithExactly1000Items()
  219. {
  220. $formatter = new NormalizerFormatter();
  221. $largeArray = range(1, 1000);
  222. $res = $formatter->format($this->getRecord(
  223. Level::Critical,
  224. 'bar',
  225. channel: 'test',
  226. context: [$largeArray],
  227. ));
  228. $this->assertCount(1000, $res['context'][0]);
  229. $this->assertArrayNotHasKey('...', $res['context'][0]);
  230. }
  231. public function testNormalizeHandleLargeArrays()
  232. {
  233. $formatter = new NormalizerFormatter();
  234. $largeArray = range(1, 2000);
  235. $res = $formatter->format($this->getRecord(
  236. Level::Critical,
  237. 'bar',
  238. channel: 'test',
  239. context: [$largeArray],
  240. ));
  241. $this->assertCount(1001, $res['context'][0]);
  242. $this->assertEquals('Over 1000 items (2000 total), aborting normalization', $res['context'][0]['...']);
  243. }
  244. public function testEmptyContextAndExtraFieldsCanBeIgnored()
  245. {
  246. $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true, true);
  247. $record = $formatter->format($this->getRecord(
  248. Level::Debug,
  249. 'Testing',
  250. channel: 'test',
  251. datetime: new \DateTimeImmutable('2022-02-22 00:00:00'),
  252. ));
  253. $this->assertSame(
  254. '{"message":"Testing","level":100,"level_name":"DEBUG","channel":"test","datetime":"2022-02-22T00:00:00+00:00"}'."\n",
  255. $record
  256. );
  257. }
  258. }