stdlib.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import os
  2. import subprocess
  3. import sys
  4. import platform
  5. from http.client import HTTPConnection
  6. import sentry_sdk
  7. from sentry_sdk.consts import OP, SPANDATA
  8. from sentry_sdk.integrations import Integration
  9. from sentry_sdk.scope import add_global_event_processor
  10. from sentry_sdk.tracing_utils import (
  11. EnvironHeaders,
  12. should_propagate_trace,
  13. add_http_request_source,
  14. )
  15. from sentry_sdk.utils import (
  16. SENSITIVE_DATA_SUBSTITUTE,
  17. capture_internal_exceptions,
  18. ensure_integration_enabled,
  19. is_sentry_url,
  20. logger,
  21. safe_repr,
  22. parse_url,
  23. )
  24. from typing import TYPE_CHECKING
  25. if TYPE_CHECKING:
  26. from typing import Any
  27. from typing import Callable
  28. from typing import Dict
  29. from typing import Optional
  30. from typing import List
  31. from sentry_sdk._types import Event, Hint
  32. _RUNTIME_CONTEXT: "dict[str, object]" = {
  33. "name": platform.python_implementation(),
  34. "version": "%s.%s.%s" % (sys.version_info[:3]),
  35. "build": sys.version,
  36. }
  37. class StdlibIntegration(Integration):
  38. identifier = "stdlib"
  39. @staticmethod
  40. def setup_once() -> None:
  41. _install_httplib()
  42. _install_subprocess()
  43. @add_global_event_processor
  44. def add_python_runtime_context(
  45. event: "Event", hint: "Hint"
  46. ) -> "Optional[Event]":
  47. if sentry_sdk.get_client().get_integration(StdlibIntegration) is not None:
  48. contexts = event.setdefault("contexts", {})
  49. if isinstance(contexts, dict) and "runtime" not in contexts:
  50. contexts["runtime"] = _RUNTIME_CONTEXT
  51. return event
  52. def _install_httplib() -> None:
  53. real_putrequest = HTTPConnection.putrequest
  54. real_getresponse = HTTPConnection.getresponse
  55. def putrequest(
  56. self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any"
  57. ) -> "Any":
  58. default_port = self.default_port
  59. # proxies go through set_tunnel
  60. tunnel_host = getattr(self, "_tunnel_host", None)
  61. if tunnel_host:
  62. host = tunnel_host
  63. port = getattr(self, "_tunnel_port", default_port)
  64. else:
  65. host = self.host
  66. port = self.port
  67. client = sentry_sdk.get_client()
  68. if client.get_integration(StdlibIntegration) is None or is_sentry_url(
  69. client, host
  70. ):
  71. return real_putrequest(self, method, url, *args, **kwargs)
  72. real_url = url
  73. if real_url is None or not real_url.startswith(("http://", "https://")):
  74. real_url = "%s://%s%s%s" % (
  75. default_port == 443 and "https" or "http",
  76. host,
  77. port != default_port and ":%s" % port or "",
  78. url,
  79. )
  80. parsed_url = None
  81. with capture_internal_exceptions():
  82. parsed_url = parse_url(real_url, sanitize=False)
  83. span = sentry_sdk.start_span(
  84. op=OP.HTTP_CLIENT,
  85. name="%s %s"
  86. % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
  87. origin="auto.http.stdlib.httplib",
  88. )
  89. span.set_data(SPANDATA.HTTP_METHOD, method)
  90. if parsed_url is not None:
  91. span.set_data("url", parsed_url.url)
  92. span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
  93. span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
  94. # for proxies, these point to the proxy host/port
  95. if tunnel_host:
  96. span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
  97. span.set_data(SPANDATA.NETWORK_PEER_PORT, self.port)
  98. rv = real_putrequest(self, method, url, *args, **kwargs)
  99. if should_propagate_trace(client, real_url):
  100. for (
  101. key,
  102. value,
  103. ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
  104. span=span
  105. ):
  106. logger.debug(
  107. "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format(
  108. key=key, value=value, real_url=real_url
  109. )
  110. )
  111. self.putheader(key, value)
  112. self._sentrysdk_span = span # type: ignore[attr-defined]
  113. return rv
  114. def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any":
  115. span = getattr(self, "_sentrysdk_span", None)
  116. if span is None:
  117. return real_getresponse(self, *args, **kwargs)
  118. try:
  119. rv = real_getresponse(self, *args, **kwargs)
  120. span.set_http_status(int(rv.status))
  121. span.set_data("reason", rv.reason)
  122. finally:
  123. span.finish()
  124. with capture_internal_exceptions():
  125. add_http_request_source(span)
  126. return rv
  127. HTTPConnection.putrequest = putrequest # type: ignore[method-assign]
  128. HTTPConnection.getresponse = getresponse # type: ignore[method-assign]
  129. def _init_argument(
  130. args: "List[Any]",
  131. kwargs: "Dict[Any, Any]",
  132. name: str,
  133. position: int,
  134. setdefault_callback: "Optional[Callable[[Any], Any]]" = None,
  135. ) -> "Any":
  136. """
  137. given (*args, **kwargs) of a function call, retrieve (and optionally set a
  138. default for) an argument by either name or position.
  139. This is useful for wrapping functions with complex type signatures and
  140. extracting a few arguments without needing to redefine that function's
  141. entire type signature.
  142. """
  143. if name in kwargs:
  144. rv = kwargs[name]
  145. if setdefault_callback is not None:
  146. rv = setdefault_callback(rv)
  147. if rv is not None:
  148. kwargs[name] = rv
  149. elif position < len(args):
  150. rv = args[position]
  151. if setdefault_callback is not None:
  152. rv = setdefault_callback(rv)
  153. if rv is not None:
  154. args[position] = rv
  155. else:
  156. rv = setdefault_callback and setdefault_callback(None)
  157. if rv is not None:
  158. kwargs[name] = rv
  159. return rv
  160. def _install_subprocess() -> None:
  161. old_popen_init = subprocess.Popen.__init__
  162. @ensure_integration_enabled(StdlibIntegration, old_popen_init)
  163. def sentry_patched_popen_init(
  164. self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any"
  165. ) -> None:
  166. # Convert from tuple to list to be able to set values.
  167. a = list(a)
  168. args = _init_argument(a, kw, "args", 0) or []
  169. cwd = _init_argument(a, kw, "cwd", 9)
  170. # if args is not a list or tuple (and e.g. some iterator instead),
  171. # let's not use it at all. There are too many things that can go wrong
  172. # when trying to collect an iterator into a list and setting that list
  173. # into `a` again.
  174. #
  175. # Also invocations where `args` is not a sequence are not actually
  176. # legal. They just happen to work under CPython.
  177. description = None
  178. if isinstance(args, (list, tuple)) and len(args) < 100:
  179. with capture_internal_exceptions():
  180. description = " ".join(map(str, args))
  181. if description is None:
  182. description = safe_repr(args)
  183. env = None
  184. with sentry_sdk.start_span(
  185. op=OP.SUBPROCESS,
  186. name=description,
  187. origin="auto.subprocess.stdlib.subprocess",
  188. ) as span:
  189. for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
  190. span=span
  191. ):
  192. if env is None:
  193. env = _init_argument(
  194. a,
  195. kw,
  196. "env",
  197. 10,
  198. lambda x: dict(x if x is not None else os.environ),
  199. )
  200. env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
  201. if cwd:
  202. span.set_data("subprocess.cwd", cwd)
  203. rv = old_popen_init(self, *a, **kw)
  204. span.set_tag("subprocess.pid", self.pid)
  205. return rv
  206. subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore
  207. old_popen_wait = subprocess.Popen.wait
  208. @ensure_integration_enabled(StdlibIntegration, old_popen_wait)
  209. def sentry_patched_popen_wait(
  210. self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any"
  211. ) -> "Any":
  212. with sentry_sdk.start_span(
  213. op=OP.SUBPROCESS_WAIT,
  214. origin="auto.subprocess.stdlib.subprocess",
  215. ) as span:
  216. span.set_tag("subprocess.pid", self.pid)
  217. return old_popen_wait(self, *a, **kw)
  218. subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore
  219. old_popen_communicate = subprocess.Popen.communicate
  220. @ensure_integration_enabled(StdlibIntegration, old_popen_communicate)
  221. def sentry_patched_popen_communicate(
  222. self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any"
  223. ) -> "Any":
  224. with sentry_sdk.start_span(
  225. op=OP.SUBPROCESS_COMMUNICATE,
  226. origin="auto.subprocess.stdlib.subprocess",
  227. ) as span:
  228. span.set_tag("subprocess.pid", self.pid)
  229. return old_popen_communicate(self, *a, **kw)
  230. subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore
  231. def get_subprocess_traceparent_headers() -> "EnvironHeaders":
  232. return EnvironHeaders(os.environ, prefix="SUBPROCESS_")