otlp.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. from sentry_sdk import get_client, capture_event
  2. from sentry_sdk.integrations import Integration, DidNotEnable
  3. from sentry_sdk.scope import register_external_propagation_context
  4. from sentry_sdk.utils import (
  5. Dsn,
  6. logger,
  7. event_from_exception,
  8. capture_internal_exceptions,
  9. )
  10. from sentry_sdk.consts import VERSION, EndpointType
  11. from sentry_sdk.tracing_utils import Baggage
  12. from sentry_sdk.tracing import (
  13. BAGGAGE_HEADER_NAME,
  14. SENTRY_TRACE_HEADER_NAME,
  15. )
  16. try:
  17. from opentelemetry.propagate import set_global_textmap
  18. from opentelemetry.sdk.trace import TracerProvider, Span
  19. from opentelemetry.sdk.trace.export import BatchSpanProcessor
  20. from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
  21. from opentelemetry.trace import (
  22. get_current_span,
  23. get_tracer_provider,
  24. set_tracer_provider,
  25. format_trace_id,
  26. format_span_id,
  27. SpanContext,
  28. INVALID_SPAN_ID,
  29. INVALID_TRACE_ID,
  30. )
  31. from opentelemetry.context import (
  32. Context,
  33. get_current,
  34. get_value,
  35. )
  36. from opentelemetry.propagators.textmap import (
  37. CarrierT,
  38. Setter,
  39. default_setter,
  40. )
  41. from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
  42. from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY
  43. except ImportError:
  44. raise DidNotEnable("opentelemetry-distro[otlp] is not installed")
  45. from typing import TYPE_CHECKING
  46. if TYPE_CHECKING:
  47. from typing import Optional, Dict, Any, Tuple
  48. def otel_propagation_context() -> "Optional[Tuple[str, str]]":
  49. """
  50. Get the (trace_id, span_id) from opentelemetry if exists.
  51. """
  52. ctx = get_current_span().get_span_context()
  53. if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID:
  54. return None
  55. return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id))
  56. def setup_otlp_traces_exporter(
  57. dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None
  58. ) -> None:
  59. tracer_provider = get_tracer_provider()
  60. if not isinstance(tracer_provider, TracerProvider):
  61. logger.debug("[OTLP] No TracerProvider configured by user, creating a new one")
  62. tracer_provider = TracerProvider()
  63. set_tracer_provider(tracer_provider)
  64. endpoint = None
  65. headers = None
  66. if collector_url:
  67. endpoint = collector_url
  68. logger.debug(f"[OTLP] Sending traces to collector at {endpoint}")
  69. elif dsn:
  70. auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}")
  71. endpoint = auth.get_api_url(EndpointType.OTLP_TRACES)
  72. headers = {"X-Sentry-Auth": auth.to_header()}
  73. logger.debug(f"[OTLP] Sending traces to {endpoint}")
  74. otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
  75. span_processor = BatchSpanProcessor(otlp_exporter)
  76. tracer_provider.add_span_processor(span_processor)
  77. _sentry_patched_exception = False
  78. def setup_capture_exceptions() -> None:
  79. """
  80. Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry.
  81. """
  82. global _sentry_patched_exception
  83. _original_record_exception = Span.record_exception
  84. if _sentry_patched_exception:
  85. return
  86. def _sentry_patched_record_exception(
  87. self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any"
  88. ) -> None:
  89. otlp_integration = get_client().get_integration(OTLPIntegration)
  90. if otlp_integration and otlp_integration.capture_exceptions:
  91. with capture_internal_exceptions():
  92. event, hint = event_from_exception(
  93. exception,
  94. client_options=get_client().options,
  95. mechanism={"type": OTLPIntegration.identifier, "handled": False},
  96. )
  97. capture_event(event, hint=hint)
  98. _original_record_exception(self, exception, *args, **kwargs)
  99. Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign]
  100. _sentry_patched_exception = True
  101. class SentryOTLPPropagator(SentryPropagator):
  102. """
  103. We need to override the inject of the older propagator since that
  104. is SpanProcessor based.
  105. !!! Note regarding baggage:
  106. We cannot meaningfully populate a new baggage as a head SDK
  107. when we are using OTLP since we don't have any sort of transaction semantic to
  108. track state across a group of spans.
  109. For incoming baggage, we just pass it on as is so that case is correctly handled.
  110. """
  111. def inject(
  112. self,
  113. carrier: "CarrierT",
  114. context: "Optional[Context]" = None,
  115. setter: "Setter[CarrierT]" = default_setter,
  116. ) -> None:
  117. otlp_integration = get_client().get_integration(OTLPIntegration)
  118. if otlp_integration is None:
  119. return
  120. if context is None:
  121. context = get_current()
  122. current_span = get_current_span(context)
  123. current_span_context = current_span.get_span_context()
  124. if not current_span_context.is_valid:
  125. return
  126. sentry_trace = _to_traceparent(current_span_context)
  127. setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace)
  128. baggage = get_value(SENTRY_BAGGAGE_KEY, context)
  129. if baggage is not None and isinstance(baggage, Baggage):
  130. baggage_data = baggage.serialize()
  131. if baggage_data:
  132. setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data)
  133. def _to_traceparent(span_context: "SpanContext") -> str:
  134. """
  135. Helper method to generate the sentry-trace header.
  136. """
  137. span_id = format_span_id(span_context.span_id)
  138. trace_id = format_trace_id(span_context.trace_id)
  139. sampled = span_context.trace_flags.sampled
  140. return f"{trace_id}-{span_id}-{'1' if sampled else '0'}"
  141. class OTLPIntegration(Integration):
  142. """
  143. Automatically setup OTLP ingestion from the DSN.
  144. :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True.
  145. Set to False to setup the TracerProvider manually.
  146. :param collector_url: URL of your own OpenTelemetry collector, defaults to None.
  147. When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN.
  148. :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True.
  149. Set to False to configure propagators manually or to disable propagation.
  150. :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False.
  151. Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases.
  152. """
  153. identifier = "otlp"
  154. def __init__(
  155. self,
  156. setup_otlp_traces_exporter: bool = True,
  157. collector_url: "Optional[str]" = None,
  158. setup_propagator: bool = True,
  159. capture_exceptions: bool = False,
  160. ) -> None:
  161. self.setup_otlp_traces_exporter = setup_otlp_traces_exporter
  162. self.collector_url = collector_url
  163. self.setup_propagator = setup_propagator
  164. self.capture_exceptions = capture_exceptions
  165. @staticmethod
  166. def setup_once() -> None:
  167. logger.debug("[OTLP] Setting up trace linking for all events")
  168. register_external_propagation_context(otel_propagation_context)
  169. def setup_once_with_options(
  170. self, options: "Optional[Dict[str, Any]]" = None
  171. ) -> None:
  172. if self.setup_otlp_traces_exporter:
  173. logger.debug("[OTLP] Setting up OTLP exporter")
  174. dsn: "Optional[str]" = options.get("dsn") if options else None
  175. setup_otlp_traces_exporter(dsn, collector_url=self.collector_url)
  176. if self.setup_propagator:
  177. logger.debug("[OTLP] Setting up propagator for distributed tracing")
  178. # TODO-neel better propagator support, chain with existing ones if possible instead of replacing
  179. set_global_textmap(SentryOTLPPropagator())
  180. setup_capture_exceptions()