_json.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. # -*- test-case-name: twisted.logger.test.test_json -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Tools for saving and loading log events in a structured format.
  6. """
  7. from json import dumps, loads
  8. from typing import IO, Any, AnyStr, Dict, Iterable, Optional, Union, cast
  9. from uuid import UUID
  10. from constantly import NamedConstant
  11. from twisted.python.failure import Failure
  12. from ._file import FileLogObserver
  13. from ._flatten import flattenEvent
  14. from ._interfaces import LogEvent
  15. from ._levels import LogLevel
  16. from ._logger import Logger
  17. log = Logger()
  18. JSONDict = Dict[str, Any]
  19. def failureAsJSON(failure: Failure) -> JSONDict:
  20. """
  21. Convert a failure to a JSON-serializable data structure.
  22. @param failure: A failure to serialize.
  23. @return: a mapping of strings to ... stuff, mostly reminiscent of
  24. L{Failure.__getstate__}
  25. """
  26. return dict(
  27. failure.__getstate__(),
  28. type=dict(
  29. __module__=failure.type.__module__,
  30. __name__=failure.type.__name__,
  31. ),
  32. )
  33. def failureFromJSON(failureDict: JSONDict) -> Failure:
  34. """
  35. Load a L{Failure} from a dictionary deserialized from JSON.
  36. @param failureDict: a JSON-deserialized object like one previously returned
  37. by L{failureAsJSON}.
  38. @return: L{Failure}
  39. """
  40. f = Failure.__new__(Failure)
  41. typeInfo = failureDict["type"]
  42. failureDict["type"] = type(typeInfo["__name__"], (), typeInfo)
  43. f.__dict__ = failureDict
  44. return f
  45. classInfo = [
  46. (
  47. lambda level: (
  48. isinstance(level, NamedConstant)
  49. and getattr(LogLevel, level.name, None) is level
  50. ),
  51. UUID("02E59486-F24D-46AD-8224-3ACDF2A5732A"),
  52. lambda level: dict(name=level.name),
  53. lambda level: getattr(LogLevel, level["name"], None),
  54. ),
  55. (
  56. lambda o: isinstance(o, Failure),
  57. UUID("E76887E2-20ED-49BF-A8F8-BA25CC586F2D"),
  58. failureAsJSON,
  59. failureFromJSON,
  60. ),
  61. ]
  62. uuidToLoader = {uuid: loader for (predicate, uuid, saver, loader) in classInfo}
  63. def objectLoadHook(aDict: JSONDict) -> object:
  64. """
  65. Dictionary-to-object-translation hook for certain value types used within
  66. the logging system.
  67. @see: the C{object_hook} parameter to L{json.load}
  68. @param aDict: A dictionary loaded from a JSON object.
  69. @return: C{aDict} itself, or the object represented by C{aDict}
  70. """
  71. if "__class_uuid__" in aDict:
  72. return uuidToLoader[UUID(aDict["__class_uuid__"])](aDict)
  73. return aDict
  74. def objectSaveHook(pythonObject: object) -> JSONDict:
  75. """
  76. Object-to-serializable hook for certain value types used within the logging
  77. system.
  78. @see: the C{default} parameter to L{json.dump}
  79. @param pythonObject: Any object.
  80. @return: If the object is one of the special types the logging system
  81. supports, a specially-formatted dictionary; otherwise, a marker
  82. dictionary indicating that it could not be serialized.
  83. """
  84. for predicate, uuid, saver, loader in classInfo:
  85. if predicate(pythonObject):
  86. result = saver(pythonObject)
  87. result["__class_uuid__"] = str(uuid)
  88. return result
  89. return {"unpersistable": True}
  90. def eventAsJSON(event: LogEvent) -> str:
  91. """
  92. Encode an event as JSON, flattening it if necessary to preserve as much
  93. structure as possible.
  94. Not all structure from the log event will be preserved when it is
  95. serialized.
  96. @param event: A log event dictionary.
  97. @return: A string of the serialized JSON; note that this will contain no
  98. newline characters, and may thus safely be stored in a line-delimited
  99. file.
  100. """
  101. def default(unencodable: object) -> Union[JSONDict, str]:
  102. """
  103. Serialize an object not otherwise serializable by L{dumps}.
  104. @param unencodable: An unencodable object.
  105. @return: C{unencodable}, serialized
  106. """
  107. if isinstance(unencodable, bytes):
  108. return unencodable.decode("charmap")
  109. return objectSaveHook(unencodable)
  110. flattenEvent(event)
  111. return dumps(event, default=default, skipkeys=True)
  112. def eventFromJSON(eventText: str) -> JSONDict:
  113. """
  114. Decode a log event from JSON.
  115. @param eventText: The output of a previous call to L{eventAsJSON}
  116. @return: A reconstructed version of the log event.
  117. """
  118. return cast(JSONDict, loads(eventText, object_hook=objectLoadHook))
  119. def jsonFileLogObserver(
  120. outFile: IO[Any], recordSeparator: str = "\x1e"
  121. ) -> FileLogObserver:
  122. """
  123. Create a L{FileLogObserver} that emits JSON-serialized events to a
  124. specified (writable) file-like object.
  125. Events are written in the following form::
  126. RS + JSON + NL
  127. C{JSON} is the serialized event, which is JSON text. C{NL} is a newline
  128. (C{"\\n"}). C{RS} is a record separator. By default, this is a single
  129. RS character (C{"\\x1e"}), which makes the default output conform to the
  130. IETF draft document "draft-ietf-json-text-sequence-13".
  131. @param outFile: A file-like object. Ideally one should be passed which
  132. accepts L{str} data. Otherwise, UTF-8 L{bytes} will be used.
  133. @param recordSeparator: The record separator to use.
  134. @return: A file log observer.
  135. """
  136. return FileLogObserver(
  137. outFile, lambda event: f"{recordSeparator}{eventAsJSON(event)}\n"
  138. )
  139. def eventsFromJSONLogFile(
  140. inFile: IO[Any],
  141. recordSeparator: Optional[str] = None,
  142. bufferSize: int = 4096,
  143. ) -> Iterable[LogEvent]:
  144. """
  145. Load events from a file previously saved with L{jsonFileLogObserver}.
  146. Event records that are truncated or otherwise unreadable are ignored.
  147. @param inFile: A (readable) file-like object. Data read from C{inFile}
  148. should be L{str} or UTF-8 L{bytes}.
  149. @param recordSeparator: The expected record separator.
  150. If L{None}, attempt to automatically detect the record separator from
  151. one of C{"\\x1e"} or C{""}.
  152. @param bufferSize: The size of the read buffer used while reading from
  153. C{inFile}.
  154. @return: Log events as read from C{inFile}.
  155. """
  156. def asBytes(s: AnyStr) -> bytes:
  157. if isinstance(s, bytes):
  158. return s
  159. else:
  160. return s.encode("utf-8")
  161. def eventFromBytearray(record: bytearray) -> Optional[LogEvent]:
  162. try:
  163. text = bytes(record).decode("utf-8")
  164. except UnicodeDecodeError:
  165. log.error(
  166. "Unable to decode UTF-8 for JSON record: {record!r}",
  167. record=bytes(record),
  168. )
  169. return None
  170. try:
  171. return eventFromJSON(text)
  172. except ValueError:
  173. log.error("Unable to read JSON record: {record!r}", record=bytes(record))
  174. return None
  175. if recordSeparator is None:
  176. first = asBytes(inFile.read(1))
  177. if first == b"\x1e":
  178. # This looks json-text-sequence compliant.
  179. recordSeparatorBytes = first
  180. else:
  181. # Default to simpler newline-separated stream, which does not use
  182. # a record separator.
  183. recordSeparatorBytes = b""
  184. else:
  185. recordSeparatorBytes = asBytes(recordSeparator)
  186. first = b""
  187. if recordSeparatorBytes == b"":
  188. recordSeparatorBytes = b"\n" # Split on newlines below
  189. eventFromRecord = eventFromBytearray
  190. else:
  191. def eventFromRecord(record: bytearray) -> Optional[LogEvent]:
  192. if record[-1] == ord("\n"):
  193. return eventFromBytearray(record)
  194. else:
  195. log.error(
  196. "Unable to read truncated JSON record: {record!r}",
  197. record=bytes(record),
  198. )
  199. return None
  200. buffer = bytearray(first)
  201. while True:
  202. newData = inFile.read(bufferSize)
  203. if not newData:
  204. if len(buffer) > 0:
  205. event = eventFromRecord(buffer)
  206. if event is not None:
  207. yield event
  208. break
  209. buffer += asBytes(newData)
  210. records = buffer.split(recordSeparatorBytes)
  211. for record in records[:-1]:
  212. if len(record) > 0:
  213. event = eventFromRecord(record)
  214. if event is not None:
  215. yield event
  216. buffer = records[-1]