httpx.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import sentry_sdk
  2. from sentry_sdk.consts import OP, SPANDATA
  3. from sentry_sdk.integrations import Integration, DidNotEnable
  4. from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
  5. from sentry_sdk.tracing_utils import Baggage, should_propagate_trace
  6. from sentry_sdk.utils import (
  7. SENSITIVE_DATA_SUBSTITUTE,
  8. capture_internal_exceptions,
  9. ensure_integration_enabled,
  10. logger,
  11. parse_url,
  12. )
  13. from typing import TYPE_CHECKING
  14. if TYPE_CHECKING:
  15. from collections.abc import MutableMapping
  16. from typing import Any
  17. try:
  18. from httpx import AsyncClient, Client, Request, Response # type: ignore
  19. except ImportError:
  20. raise DidNotEnable("httpx is not installed")
  21. __all__ = ["HttpxIntegration"]
  22. class HttpxIntegration(Integration):
  23. identifier = "httpx"
  24. origin = f"auto.http.{identifier}"
  25. @staticmethod
  26. def setup_once():
  27. # type: () -> None
  28. """
  29. httpx has its own transport layer and can be customized when needed,
  30. so patch Client.send and AsyncClient.send to support both synchronous and async interfaces.
  31. """
  32. _install_httpx_client()
  33. _install_httpx_async_client()
  34. def _install_httpx_client():
  35. # type: () -> None
  36. real_send = Client.send
  37. @ensure_integration_enabled(HttpxIntegration, real_send)
  38. def send(self, request, **kwargs):
  39. # type: (Client, Request, **Any) -> Response
  40. parsed_url = None
  41. with capture_internal_exceptions():
  42. parsed_url = parse_url(str(request.url), sanitize=False)
  43. with sentry_sdk.start_span(
  44. op=OP.HTTP_CLIENT,
  45. name="%s %s"
  46. % (
  47. request.method,
  48. parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE,
  49. ),
  50. origin=HttpxIntegration.origin,
  51. ) as span:
  52. span.set_data(SPANDATA.HTTP_METHOD, request.method)
  53. if parsed_url is not None:
  54. span.set_data("url", parsed_url.url)
  55. span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
  56. span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
  57. if should_propagate_trace(sentry_sdk.get_client(), str(request.url)):
  58. for (
  59. key,
  60. value,
  61. ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers():
  62. logger.debug(
  63. "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
  64. key=key, value=value, url=request.url
  65. )
  66. )
  67. if key == BAGGAGE_HEADER_NAME:
  68. _add_sentry_baggage_to_headers(request.headers, value)
  69. else:
  70. request.headers[key] = value
  71. rv = real_send(self, request, **kwargs)
  72. span.set_http_status(rv.status_code)
  73. span.set_data("reason", rv.reason_phrase)
  74. return rv
  75. Client.send = send
  76. def _install_httpx_async_client():
  77. # type: () -> None
  78. real_send = AsyncClient.send
  79. async def send(self, request, **kwargs):
  80. # type: (AsyncClient, Request, **Any) -> Response
  81. if sentry_sdk.get_client().get_integration(HttpxIntegration) is None:
  82. return await real_send(self, request, **kwargs)
  83. parsed_url = None
  84. with capture_internal_exceptions():
  85. parsed_url = parse_url(str(request.url), sanitize=False)
  86. with sentry_sdk.start_span(
  87. op=OP.HTTP_CLIENT,
  88. name="%s %s"
  89. % (
  90. request.method,
  91. parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE,
  92. ),
  93. origin=HttpxIntegration.origin,
  94. ) as span:
  95. span.set_data(SPANDATA.HTTP_METHOD, request.method)
  96. if parsed_url is not None:
  97. span.set_data("url", parsed_url.url)
  98. span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
  99. span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
  100. if should_propagate_trace(sentry_sdk.get_client(), str(request.url)):
  101. for (
  102. key,
  103. value,
  104. ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers():
  105. logger.debug(
  106. "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
  107. key=key, value=value, url=request.url
  108. )
  109. )
  110. if key == BAGGAGE_HEADER_NAME and request.headers.get(
  111. BAGGAGE_HEADER_NAME
  112. ):
  113. # do not overwrite any existing baggage, just append to it
  114. request.headers[key] += "," + value
  115. else:
  116. request.headers[key] = value
  117. rv = await real_send(self, request, **kwargs)
  118. span.set_http_status(rv.status_code)
  119. span.set_data("reason", rv.reason_phrase)
  120. return rv
  121. AsyncClient.send = send
  122. def _add_sentry_baggage_to_headers(headers, sentry_baggage):
  123. # type: (MutableMapping[str, str], str) -> None
  124. """Add the Sentry baggage to the headers.
  125. This function directly mutates the provided headers. The provided sentry_baggage
  126. is appended to the existing baggage. If the baggage already contains Sentry items,
  127. they are stripped out first.
  128. """
  129. existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "")
  130. stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage)
  131. separator = "," if len(stripped_existing_baggage) > 0 else ""
  132. headers[BAGGAGE_HEADER_NAME] = (
  133. stripped_existing_baggage + separator + sentry_baggage
  134. )