test_json.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.logger._json}.
  5. """
  6. from io import StringIO, BytesIO
  7. from zope.interface.verify import verifyObject, BrokenMethodImplementation
  8. from twisted.python.compat import unicode
  9. from twisted.trial.unittest import TestCase
  10. from twisted.python.failure import Failure
  11. from .._observer import ILogObserver, LogPublisher
  12. from .._format import formatEvent
  13. from .._levels import LogLevel
  14. from .._flatten import extractField
  15. from .._global import globalLogPublisher
  16. from .._json import (
  17. eventAsJSON, eventFromJSON, jsonFileLogObserver, eventsFromJSONLogFile,
  18. log as jsonLog
  19. )
  20. from .._logger import Logger
  21. def savedJSONInvariants(testCase, savedJSON):
  22. """
  23. Assert a few things about the result of L{eventAsJSON}, then return it.
  24. @param testCase: The L{TestCase} with which to perform the assertions.
  25. @type testCase: L{TestCase}
  26. @param savedJSON: The result of L{eventAsJSON}.
  27. @type savedJSON: L{unicode} (we hope)
  28. @return: C{savedJSON}
  29. @rtype: L{unicode}
  30. @raise AssertionError: If any of the preconditions fail.
  31. """
  32. testCase.assertIsInstance(savedJSON, unicode)
  33. testCase.assertEqual(savedJSON.count("\n"), 0)
  34. return savedJSON
  35. class SaveLoadTests(TestCase):
  36. """
  37. Tests for loading and saving log events.
  38. """
  39. def savedEventJSON(self, event):
  40. """
  41. Serialize some an events, assert some things about it, and return the
  42. JSON.
  43. @param event: An event.
  44. @type event: L{dict}
  45. @return: JSON.
  46. """
  47. return savedJSONInvariants(self, eventAsJSON(event))
  48. def test_simpleSaveLoad(self):
  49. """
  50. Saving and loading an empty dictionary results in an empty dictionary.
  51. """
  52. self.assertEqual(eventFromJSON(self.savedEventJSON({})), {})
  53. def test_saveLoad(self):
  54. """
  55. Saving and loading a dictionary with some simple values in it results
  56. in those same simple values in the output; according to JSON's rules,
  57. though, all dictionary keys must be L{unicode} and any non-L{unicode}
  58. keys will be converted.
  59. """
  60. self.assertEqual(
  61. eventFromJSON(self.savedEventJSON({1: 2, u"3": u"4"})),
  62. {u"1": 2, u"3": u"4"}
  63. )
  64. def test_saveUnPersistable(self):
  65. """
  66. Saving and loading an object which cannot be represented in JSON will
  67. result in a placeholder.
  68. """
  69. self.assertEqual(
  70. eventFromJSON(self.savedEventJSON({u"1": 2, u"3": object()})),
  71. {u"1": 2, u"3": {u"unpersistable": True}}
  72. )
  73. def test_saveNonASCII(self):
  74. """
  75. Non-ASCII keys and values can be saved and loaded.
  76. """
  77. self.assertEqual(
  78. eventFromJSON(self.savedEventJSON(
  79. {u"\u1234": u"\u4321", u"3": object()}
  80. )),
  81. {u"\u1234": u"\u4321", u"3": {u"unpersistable": True}}
  82. )
  83. def test_saveBytes(self):
  84. """
  85. Any L{bytes} objects will be saved as if they are latin-1 so they can
  86. be faithfully re-loaded.
  87. """
  88. def asbytes(x):
  89. if bytes is str:
  90. return b"".join(map(chr, x))
  91. else:
  92. return bytes(x)
  93. inputEvent = {"hello": asbytes(range(255))}
  94. if bytes is not str:
  95. # On Python 3, bytes keys will be skipped by the JSON encoder. Not
  96. # much we can do about that. Let's make sure that we don't get an
  97. # error, though.
  98. inputEvent.update({b"skipped": "okay"})
  99. self.assertEqual(
  100. eventFromJSON(self.savedEventJSON(inputEvent)),
  101. {u"hello": asbytes(range(255)).decode("charmap")}
  102. )
  103. def test_saveUnPersistableThenFormat(self):
  104. """
  105. Saving and loading an object which cannot be represented in JSON, but
  106. has a string representation which I{can} be saved as JSON, will result
  107. in the same string formatting; any extractable fields will retain their
  108. data types.
  109. """
  110. class Reprable(object):
  111. def __init__(self, value):
  112. self.value = value
  113. def __repr__(self):
  114. return("reprable")
  115. inputEvent = {
  116. "log_format": "{object} {object.value}",
  117. "object": Reprable(7)
  118. }
  119. outputEvent = eventFromJSON(self.savedEventJSON(inputEvent))
  120. self.assertEqual(formatEvent(outputEvent), "reprable 7")
  121. def test_extractingFieldsPostLoad(self):
  122. """
  123. L{extractField} can extract fields from an object that's been saved and
  124. loaded from JSON.
  125. """
  126. class Obj(object):
  127. def __init__(self):
  128. self.value = 345
  129. inputEvent = dict(log_format="{object.value}", object=Obj())
  130. loadedEvent = eventFromJSON(self.savedEventJSON(inputEvent))
  131. self.assertEqual(extractField("object.value", loadedEvent), 345)
  132. # The behavior of extractField is consistent between pre-persistence
  133. # and post-persistence events, although looking up the key directly
  134. # won't be:
  135. self.assertRaises(KeyError, extractField, "object", loadedEvent)
  136. self.assertRaises(KeyError, extractField, "object", inputEvent)
  137. def test_failureStructurePreserved(self):
  138. """
  139. Round-tripping a failure through L{eventAsJSON} preserves its class and
  140. structure.
  141. """
  142. events = []
  143. log = Logger(observer=events.append)
  144. try:
  145. 1 / 0
  146. except ZeroDivisionError:
  147. f = Failure()
  148. log.failure("a message about failure", f)
  149. import sys
  150. if sys.exc_info()[0] is not None:
  151. # Make sure we don't get the same Failure by accident.
  152. sys.exc_clear()
  153. self.assertEqual(len(events), 1)
  154. loaded = eventFromJSON(self.savedEventJSON(events[0]))['log_failure']
  155. self.assertIsInstance(loaded, Failure)
  156. self.assertTrue(loaded.check(ZeroDivisionError))
  157. self.assertIsInstance(loaded.getTraceback(), str)
  158. def test_saveLoadLevel(self):
  159. """
  160. It's important that the C{log_level} key remain a
  161. L{constantly.NamedConstant} object.
  162. """
  163. inputEvent = dict(log_level=LogLevel.warn)
  164. loadedEvent = eventFromJSON(self.savedEventJSON(inputEvent))
  165. self.assertIs(loadedEvent["log_level"], LogLevel.warn)
  166. def test_saveLoadUnknownLevel(self):
  167. """
  168. If a saved bit of JSON (let's say, from a future version of Twisted)
  169. were to persist a different log_level, it will resolve as None.
  170. """
  171. loadedEvent = eventFromJSON(
  172. '{"log_level": {"name": "other", '
  173. '"__class_uuid__": "02E59486-F24D-46AD-8224-3ACDF2A5732A"}}'
  174. )
  175. self.assertEqual(loadedEvent, dict(log_level=None))
  176. class FileLogObserverTests(TestCase):
  177. """
  178. Tests for L{jsonFileLogObserver}.
  179. """
  180. def test_interface(self):
  181. """
  182. A L{FileLogObserver} returned by L{jsonFileLogObserver} is an
  183. L{ILogObserver}.
  184. """
  185. with StringIO() as fileHandle:
  186. observer = jsonFileLogObserver(fileHandle)
  187. try:
  188. verifyObject(ILogObserver, observer)
  189. except BrokenMethodImplementation as e:
  190. self.fail(e)
  191. def assertObserverWritesJSON(self, **kwargs):
  192. """
  193. Asserts that an observer created by L{jsonFileLogObserver} with the
  194. given arguments writes events serialized as JSON text, using the given
  195. record separator.
  196. @param recordSeparator: A record separator.
  197. @type recordSeparator: L{unicode}
  198. @param kwargs: Keyword arguments to pass to L{jsonFileLogObserver}.
  199. @type kwargs: L{dict}
  200. """
  201. recordSeparator = kwargs.get("recordSeparator", u"\x1e")
  202. with StringIO() as fileHandle:
  203. observer = jsonFileLogObserver(fileHandle, **kwargs)
  204. event = dict(x=1)
  205. observer(event)
  206. self.assertEqual(
  207. fileHandle.getvalue(),
  208. u'{0}{{"x": 1}}\n'.format(recordSeparator)
  209. )
  210. def test_observeWritesDefaultRecordSeparator(self):
  211. """
  212. A L{FileLogObserver} created by L{jsonFileLogObserver} writes events
  213. serialzed as JSON text to a file when it observes events.
  214. By default, the record separator is C{u"\\x1e"}.
  215. """
  216. self.assertObserverWritesJSON()
  217. def test_observeWritesEmptyRecordSeparator(self):
  218. """
  219. A L{FileLogObserver} created by L{jsonFileLogObserver} writes events
  220. serialzed as JSON text to a file when it observes events.
  221. This test sets the record separator to C{u""}.
  222. """
  223. self.assertObserverWritesJSON(recordSeparator=u"")
  224. def test_failureFormatting(self):
  225. """
  226. A L{FileLogObserver} created by L{jsonFileLogObserver} writes failures
  227. serialized as JSON text to a file when it observes events.
  228. """
  229. io = StringIO()
  230. publisher = LogPublisher()
  231. logged = []
  232. publisher.addObserver(logged.append)
  233. publisher.addObserver(jsonFileLogObserver(io))
  234. logger = Logger(observer=publisher)
  235. try:
  236. 1 / 0
  237. except:
  238. logger.failure("failed as expected")
  239. reader = StringIO(io.getvalue())
  240. deserialized = list(eventsFromJSONLogFile(reader))
  241. def checkEvents(logEvents):
  242. self.assertEqual(len(logEvents), 1)
  243. [failureEvent] = logEvents
  244. self.assertIn("log_failure", failureEvent)
  245. failureObject = failureEvent["log_failure"]
  246. self.assertIsInstance(failureObject, Failure)
  247. tracebackObject = failureObject.getTracebackObject()
  248. self.assertEqual(
  249. tracebackObject.tb_frame.f_code.co_filename.rstrip("co"),
  250. __file__.rstrip("co")
  251. )
  252. checkEvents(logged)
  253. checkEvents(deserialized)
  254. class LogFileReaderTests(TestCase):
  255. """
  256. Tests for L{eventsFromJSONLogFile}.
  257. """
  258. def setUp(self):
  259. self.errorEvents = []
  260. def observer(event):
  261. if (
  262. event["log_namespace"] == jsonLog.namespace and
  263. "record" in event
  264. ):
  265. self.errorEvents.append(event)
  266. self.logObserver = observer
  267. globalLogPublisher.addObserver(observer)
  268. def tearDown(self):
  269. globalLogPublisher.removeObserver(self.logObserver)
  270. def _readEvents(self, fileHandle, **kwargs):
  271. """
  272. Test that L{eventsFromJSONLogFile} reads two pre-defined events from a
  273. file: C{{u"x": 1}} and C{{u"y": 2}}.
  274. @param fileHandle: The file to read from.
  275. @param kwargs: Keyword arguments to pass to L{eventsFromJSONLogFile}.
  276. """
  277. events = eventsFromJSONLogFile(fileHandle, **kwargs)
  278. self.assertEqual(next(events), {u"x": 1})
  279. self.assertEqual(next(events), {u"y": 2})
  280. self.assertRaises(StopIteration, next, events) # No more events
  281. def test_readEventsAutoWithRecordSeparator(self):
  282. """
  283. L{eventsFromJSONLogFile} reads events from a file and automatically
  284. detects use of C{u"\\x1e"} as the record separator.
  285. """
  286. with StringIO(
  287. u'\x1e{"x": 1}\n'
  288. u'\x1e{"y": 2}\n'
  289. ) as fileHandle:
  290. self._readEvents(fileHandle)
  291. self.assertEqual(len(self.errorEvents), 0)
  292. def test_readEventsAutoEmptyRecordSeparator(self):
  293. """
  294. L{eventsFromJSONLogFile} reads events from a file and automatically
  295. detects use of C{u""} as the record separator.
  296. """
  297. with StringIO(
  298. u'{"x": 1}\n'
  299. u'{"y": 2}\n'
  300. ) as fileHandle:
  301. self._readEvents(fileHandle)
  302. self.assertEqual(len(self.errorEvents), 0)
  303. def test_readEventsExplicitRecordSeparator(self):
  304. """
  305. L{eventsFromJSONLogFile} reads events from a file and is told to use
  306. a specific record separator.
  307. """
  308. # Use u"\x08" (backspace)... because that seems weird enough.
  309. with StringIO(
  310. u'\x08{"x": 1}\n'
  311. u'\x08{"y": 2}\n'
  312. ) as fileHandle:
  313. self._readEvents(fileHandle, recordSeparator=u"\x08")
  314. self.assertEqual(len(self.errorEvents), 0)
  315. def test_readEventsPartialBuffer(self):
  316. """
  317. L{eventsFromJSONLogFile} handles buffering a partial event.
  318. """
  319. with StringIO(
  320. u'\x1e{"x": 1}\n'
  321. u'\x1e{"y": 2}\n'
  322. ) as fileHandle:
  323. # Use a buffer size smaller than the event text.
  324. self._readEvents(fileHandle, bufferSize=1)
  325. self.assertEqual(len(self.errorEvents), 0)
  326. def test_readTruncated(self):
  327. """
  328. If the JSON text for a record is truncated, skip it.
  329. """
  330. with StringIO(
  331. u'\x1e{"x": 1'
  332. u'\x1e{"y": 2}\n'
  333. ) as fileHandle:
  334. events = eventsFromJSONLogFile(fileHandle)
  335. self.assertEqual(next(events), {u"y": 2})
  336. self.assertRaises(StopIteration, next, events) # No more events
  337. # We should have logged the lost record
  338. self.assertEqual(len(self.errorEvents), 1)
  339. self.assertEqual(
  340. self.errorEvents[0]["log_format"],
  341. u"Unable to read truncated JSON record: {record!r}"
  342. )
  343. self.assertEqual(self.errorEvents[0]["record"], b'{"x": 1')
  344. def test_readUnicode(self):
  345. """
  346. If the file being read from vends L{unicode}, strings decode from JSON
  347. as-is.
  348. """
  349. # The Euro currency sign is u"\u20ac"
  350. with StringIO(u'\x1e{"currency": "\u20ac"}\n') as fileHandle:
  351. events = eventsFromJSONLogFile(fileHandle)
  352. self.assertEqual(next(events), {u"currency": u"\u20ac"})
  353. self.assertRaises(StopIteration, next, events) # No more events
  354. self.assertEqual(len(self.errorEvents), 0)
  355. def test_readUTF8Bytes(self):
  356. """
  357. If the file being read from vends L{bytes}, strings decode from JSON as
  358. UTF-8.
  359. """
  360. # The Euro currency sign is b"\xe2\x82\xac" in UTF-8
  361. with BytesIO(b'\x1e{"currency": "\xe2\x82\xac"}\n') as fileHandle:
  362. events = eventsFromJSONLogFile(fileHandle)
  363. # The Euro currency sign is u"\u20ac"
  364. self.assertEqual(next(events), {u"currency": u"\u20ac"})
  365. self.assertRaises(StopIteration, next, events) # No more events
  366. self.assertEqual(len(self.errorEvents), 0)
  367. def test_readTruncatedUTF8Bytes(self):
  368. """
  369. If the JSON text for a record is truncated in the middle of a two-byte
  370. Unicode codepoint, we don't want to see a codec exception and the
  371. stream is read properly when the additional data arrives.
  372. """
  373. # The Euro currency sign is u"\u20ac" and encodes in UTF-8 as three
  374. # bytes: b"\xe2\x82\xac".
  375. with BytesIO(b'\x1e{"x": "\xe2\x82\xac"}\n') as fileHandle:
  376. events = eventsFromJSONLogFile(fileHandle, bufferSize=8)
  377. self.assertEqual(next(events), {u"x": u"\u20ac"}) # Got unicode
  378. self.assertRaises(StopIteration, next, events) # No more events
  379. self.assertEqual(len(self.errorEvents), 0)
  380. def test_readInvalidUTF8Bytes(self):
  381. """
  382. If the JSON text for a record contains invalid UTF-8 text, ignore that
  383. record.
  384. """
  385. # The string b"\xe2\xac" is bogus
  386. with BytesIO(
  387. b'\x1e{"x": "\xe2\xac"}\n'
  388. b'\x1e{"y": 2}\n'
  389. ) as fileHandle:
  390. events = eventsFromJSONLogFile(fileHandle)
  391. self.assertEqual(next(events), {u"y": 2})
  392. self.assertRaises(StopIteration, next, events) # No more events
  393. # We should have logged the lost record
  394. self.assertEqual(len(self.errorEvents), 1)
  395. self.assertEqual(
  396. self.errorEvents[0]["log_format"],
  397. u"Unable to decode UTF-8 for JSON record: {record!r}"
  398. )
  399. self.assertEqual(
  400. self.errorEvents[0]["record"],
  401. b'{"x": "\xe2\xac"}\n'
  402. )
  403. def test_readInvalidJSON(self):
  404. """
  405. If the JSON text for a record is invalid, skip it.
  406. """
  407. with StringIO(
  408. u'\x1e{"x": }\n'
  409. u'\x1e{"y": 2}\n'
  410. ) as fileHandle:
  411. events = eventsFromJSONLogFile(fileHandle)
  412. self.assertEqual(next(events), {u"y": 2})
  413. self.assertRaises(StopIteration, next, events) # No more events
  414. # We should have logged the lost record
  415. self.assertEqual(len(self.errorEvents), 1)
  416. self.assertEqual(
  417. self.errorEvents[0]["log_format"],
  418. u"Unable to read JSON record: {record!r}"
  419. )
  420. self.assertEqual(self.errorEvents[0]["record"], b'{"x": }\n')
  421. def test_readUnseparated(self):
  422. """
  423. Multiple events without a record separator are skipped.
  424. """
  425. with StringIO(
  426. u'\x1e{"x": 1}\n'
  427. u'{"y": 2}\n'
  428. ) as fileHandle:
  429. events = eventsFromJSONLogFile(fileHandle)
  430. self.assertRaises(StopIteration, next, events) # No more events
  431. # We should have logged the lost record
  432. self.assertEqual(len(self.errorEvents), 1)
  433. self.assertEqual(
  434. self.errorEvents[0]["log_format"],
  435. u"Unable to read JSON record: {record!r}"
  436. )
  437. self.assertEqual(
  438. self.errorEvents[0]["record"],
  439. b'{"x": 1}\n{"y": 2}\n'
  440. )
  441. def test_roundTrip(self):
  442. """
  443. Data written by a L{FileLogObserver} returned by L{jsonFileLogObserver}
  444. and read by L{eventsFromJSONLogFile} is reconstructed properly.
  445. """
  446. event = dict(x=1)
  447. with StringIO() as fileHandle:
  448. observer = jsonFileLogObserver(fileHandle)
  449. observer(event)
  450. fileHandle.seek(0)
  451. events = eventsFromJSONLogFile(fileHandle)
  452. self.assertEqual(tuple(events), (event,))
  453. self.assertEqual(len(self.errorEvents), 0)