envelope.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import io
  2. import json
  3. import mimetypes
  4. from sentry_sdk.session import Session
  5. from sentry_sdk.utils import json_dumps, capture_internal_exceptions
  6. from typing import TYPE_CHECKING
  7. if TYPE_CHECKING:
  8. from typing import Any
  9. from typing import Optional
  10. from typing import Union
  11. from typing import Dict
  12. from typing import List
  13. from typing import Iterator
  14. from sentry_sdk._types import Event, EventDataCategory
  15. def parse_json(data: "Union[bytes, str]") -> "Any":
  16. # on some python 3 versions this needs to be bytes
  17. if isinstance(data, bytes):
  18. data = data.decode("utf-8", "replace")
  19. return json.loads(data)
  20. class Envelope:
  21. """
  22. Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
  23. documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
  24. each envelope may have at most one Item with type "event" or "transaction" (but not both).
  25. """
  26. def __init__(
  27. self,
  28. headers: "Optional[Dict[str, Any]]" = None,
  29. items: "Optional[List[Item]]" = None,
  30. ) -> None:
  31. if headers is not None:
  32. headers = dict(headers)
  33. self.headers = headers or {}
  34. if items is None:
  35. items = []
  36. else:
  37. items = list(items)
  38. self.items = items
  39. @property
  40. def description(self) -> str:
  41. return "envelope with %s items (%s)" % (
  42. len(self.items),
  43. ", ".join(x.data_category for x in self.items),
  44. )
  45. def add_event(
  46. self,
  47. event: "Event",
  48. ) -> None:
  49. self.add_item(Item(payload=PayloadRef(json=event), type="event"))
  50. def add_transaction(
  51. self,
  52. transaction: "Event",
  53. ) -> None:
  54. self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
  55. def add_profile(
  56. self,
  57. profile: "Any",
  58. ) -> None:
  59. self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
  60. def add_profile_chunk(
  61. self,
  62. profile_chunk: "Any",
  63. ) -> None:
  64. self.add_item(
  65. Item(
  66. payload=PayloadRef(json=profile_chunk),
  67. type="profile_chunk",
  68. headers={"platform": profile_chunk.get("platform", "python")},
  69. )
  70. )
  71. def add_checkin(
  72. self,
  73. checkin: "Any",
  74. ) -> None:
  75. self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
  76. def add_session(
  77. self,
  78. session: "Union[Session, Any]",
  79. ) -> None:
  80. if isinstance(session, Session):
  81. session = session.to_json()
  82. self.add_item(Item(payload=PayloadRef(json=session), type="session"))
  83. def add_sessions(
  84. self,
  85. sessions: "Any",
  86. ) -> None:
  87. self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
  88. def add_item(
  89. self,
  90. item: "Item",
  91. ) -> None:
  92. self.items.append(item)
  93. def get_event(self) -> "Optional[Event]":
  94. for items in self.items:
  95. event = items.get_event()
  96. if event is not None:
  97. return event
  98. return None
  99. def get_transaction_event(self) -> "Optional[Event]":
  100. for item in self.items:
  101. event = item.get_transaction_event()
  102. if event is not None:
  103. return event
  104. return None
  105. def __iter__(self) -> "Iterator[Item]":
  106. return iter(self.items)
  107. def serialize_into(
  108. self,
  109. f: "Any",
  110. ) -> None:
  111. f.write(json_dumps(self.headers))
  112. f.write(b"\n")
  113. for item in self.items:
  114. item.serialize_into(f)
  115. def serialize(self) -> bytes:
  116. out = io.BytesIO()
  117. self.serialize_into(out)
  118. return out.getvalue()
  119. @classmethod
  120. def deserialize_from(
  121. cls,
  122. f: "Any",
  123. ) -> "Envelope":
  124. headers = parse_json(f.readline())
  125. items = []
  126. while 1:
  127. item = Item.deserialize_from(f)
  128. if item is None:
  129. break
  130. items.append(item)
  131. return cls(headers=headers, items=items)
  132. @classmethod
  133. def deserialize(
  134. cls,
  135. bytes: bytes,
  136. ) -> "Envelope":
  137. return cls.deserialize_from(io.BytesIO(bytes))
  138. def __repr__(self) -> str:
  139. return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
  140. class PayloadRef:
  141. def __init__(
  142. self,
  143. bytes: "Optional[bytes]" = None,
  144. path: "Optional[Union[bytes, str]]" = None,
  145. json: "Optional[Any]" = None,
  146. ) -> None:
  147. self.json = json
  148. self.bytes = bytes
  149. self.path = path
  150. def get_bytes(self) -> bytes:
  151. if self.bytes is None:
  152. if self.path is not None:
  153. with capture_internal_exceptions():
  154. with open(self.path, "rb") as f:
  155. self.bytes = f.read()
  156. elif self.json is not None:
  157. self.bytes = json_dumps(self.json)
  158. return self.bytes or b""
  159. @property
  160. def inferred_content_type(self) -> str:
  161. if self.json is not None:
  162. return "application/json"
  163. elif self.path is not None:
  164. path = self.path
  165. if isinstance(path, bytes):
  166. path = path.decode("utf-8", "replace")
  167. ty = mimetypes.guess_type(path)[0]
  168. if ty:
  169. return ty
  170. return "application/octet-stream"
  171. def __repr__(self) -> str:
  172. return "<Payload %r>" % (self.inferred_content_type,)
  173. class Item:
  174. def __init__(
  175. self,
  176. payload: "Union[bytes, str, PayloadRef]",
  177. headers: "Optional[Dict[str, Any]]" = None,
  178. type: "Optional[str]" = None,
  179. content_type: "Optional[str]" = None,
  180. filename: "Optional[str]" = None,
  181. ):
  182. if headers is not None:
  183. headers = dict(headers)
  184. elif headers is None:
  185. headers = {}
  186. self.headers = headers
  187. if isinstance(payload, bytes):
  188. payload = PayloadRef(bytes=payload)
  189. elif isinstance(payload, str):
  190. payload = PayloadRef(bytes=payload.encode("utf-8"))
  191. else:
  192. payload = payload
  193. if filename is not None:
  194. headers["filename"] = filename
  195. if type is not None:
  196. headers["type"] = type
  197. if content_type is not None:
  198. headers["content_type"] = content_type
  199. elif "content_type" not in headers:
  200. headers["content_type"] = payload.inferred_content_type
  201. self.payload = payload
  202. def __repr__(self) -> str:
  203. return "<Item headers=%r payload=%r data_category=%r>" % (
  204. self.headers,
  205. self.payload,
  206. self.data_category,
  207. )
  208. @property
  209. def type(self) -> "Optional[str]":
  210. return self.headers.get("type")
  211. @property
  212. def data_category(self) -> "EventDataCategory":
  213. ty = self.headers.get("type")
  214. if ty == "session" or ty == "sessions":
  215. return "session"
  216. elif ty == "attachment":
  217. return "attachment"
  218. elif ty == "transaction":
  219. return "transaction"
  220. elif ty == "span":
  221. return "span"
  222. elif ty == "event":
  223. return "error"
  224. elif ty == "log":
  225. return "log_item"
  226. elif ty == "trace_metric":
  227. return "trace_metric"
  228. elif ty == "client_report":
  229. return "internal"
  230. elif ty == "profile":
  231. return "profile"
  232. elif ty == "profile_chunk":
  233. return "profile_chunk"
  234. elif ty == "check_in":
  235. return "monitor"
  236. else:
  237. return "default"
  238. def get_bytes(self) -> bytes:
  239. return self.payload.get_bytes()
  240. def get_event(self) -> "Optional[Event]":
  241. """
  242. Returns an error event if there is one.
  243. """
  244. if self.type == "event" and self.payload.json is not None:
  245. return self.payload.json
  246. return None
  247. def get_transaction_event(self) -> "Optional[Event]":
  248. if self.type == "transaction" and self.payload.json is not None:
  249. return self.payload.json
  250. return None
  251. def serialize_into(
  252. self,
  253. f: "Any",
  254. ) -> None:
  255. headers = dict(self.headers)
  256. bytes = self.get_bytes()
  257. headers["length"] = len(bytes)
  258. f.write(json_dumps(headers))
  259. f.write(b"\n")
  260. f.write(bytes)
  261. f.write(b"\n")
  262. def serialize(self) -> bytes:
  263. out = io.BytesIO()
  264. self.serialize_into(out)
  265. return out.getvalue()
  266. @classmethod
  267. def deserialize_from(
  268. cls,
  269. f: "Any",
  270. ) -> "Optional[Item]":
  271. line = f.readline().rstrip()
  272. if not line:
  273. return None
  274. headers = parse_json(line)
  275. length = headers.get("length")
  276. if length is not None:
  277. payload = f.read(length)
  278. f.readline()
  279. else:
  280. # if no length was specified we need to read up to the end of line
  281. # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
  282. payload = f.readline().rstrip(b"\n")
  283. if headers.get("type") in ("event", "transaction"):
  284. rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
  285. else:
  286. rv = cls(headers=headers, payload=payload)
  287. return rv
  288. @classmethod
  289. def deserialize(
  290. cls,
  291. bytes: bytes,
  292. ) -> "Optional[Item]":
  293. return cls.deserialize_from(io.BytesIO(bytes))