stdlib.py 8.6 KB

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