Changed code to support older Python versions
This commit is contained in:
parent
eb92d2d36f
commit
582458cdd0
5027 changed files with 794942 additions and 4 deletions
|
|
@ -0,0 +1,7 @@
|
|||
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
|
||||
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
|
||||
|
||||
__all__ = [
|
||||
"SentryPropagator",
|
||||
"SentrySpanProcessor",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
|||
from opentelemetry.context import create_key
|
||||
|
||||
|
||||
SENTRY_TRACE_KEY = create_key("sentry-trace")
|
||||
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
"""
|
||||
IMPORTANT: The contents of this file are part of a proof of concept and as such
|
||||
are experimental and not suitable for production use. They may be changed or
|
||||
removed at any time without prior notice.
|
||||
"""
|
||||
|
||||
from sentry_sdk.integrations import DidNotEnable, Integration
|
||||
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
|
||||
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
|
||||
from sentry_sdk.utils import logger
|
||||
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.propagate import set_global_textmap
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
except ImportError:
|
||||
raise DidNotEnable("opentelemetry not installed")
|
||||
|
||||
try:
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
DjangoInstrumentor = None
|
||||
|
||||
|
||||
CONFIGURABLE_INSTRUMENTATIONS = {
|
||||
DjangoInstrumentor: {"is_sql_commentor_enabled": True},
|
||||
}
|
||||
|
||||
|
||||
class OpenTelemetryIntegration(Integration):
|
||||
identifier = "opentelemetry"
|
||||
|
||||
@staticmethod
|
||||
def setup_once():
|
||||
# type: () -> None
|
||||
logger.warning(
|
||||
"[OTel] Initializing highly experimental OpenTelemetry support. "
|
||||
"Use at your own risk."
|
||||
)
|
||||
|
||||
_setup_sentry_tracing()
|
||||
# _setup_instrumentors()
|
||||
|
||||
logger.debug("[OTel] Finished setting up OpenTelemetry integration")
|
||||
|
||||
|
||||
def _setup_sentry_tracing():
|
||||
# type: () -> None
|
||||
provider = TracerProvider()
|
||||
provider.add_span_processor(SentrySpanProcessor())
|
||||
trace.set_tracer_provider(provider)
|
||||
set_global_textmap(SentryPropagator())
|
||||
|
||||
|
||||
def _setup_instrumentors():
|
||||
# type: () -> None
|
||||
for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items():
|
||||
instrumentor().instrument(**kwargs)
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
from opentelemetry import trace
|
||||
from opentelemetry.context import (
|
||||
Context,
|
||||
get_current,
|
||||
set_value,
|
||||
)
|
||||
from opentelemetry.propagators.textmap import (
|
||||
CarrierT,
|
||||
Getter,
|
||||
Setter,
|
||||
TextMapPropagator,
|
||||
default_getter,
|
||||
default_setter,
|
||||
)
|
||||
from opentelemetry.trace import (
|
||||
NonRecordingSpan,
|
||||
SpanContext,
|
||||
TraceFlags,
|
||||
)
|
||||
|
||||
from sentry_sdk.integrations.opentelemetry.consts import (
|
||||
SENTRY_BAGGAGE_KEY,
|
||||
SENTRY_TRACE_KEY,
|
||||
)
|
||||
from sentry_sdk.integrations.opentelemetry.span_processor import (
|
||||
SentrySpanProcessor,
|
||||
)
|
||||
from sentry_sdk.tracing import (
|
||||
BAGGAGE_HEADER_NAME,
|
||||
SENTRY_TRACE_HEADER_NAME,
|
||||
)
|
||||
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, Set
|
||||
|
||||
|
||||
class SentryPropagator(TextMapPropagator):
|
||||
"""
|
||||
Propagates tracing headers for Sentry's tracing system in a way OTel understands.
|
||||
"""
|
||||
|
||||
def extract(self, carrier, context=None, getter=default_getter):
|
||||
# type: (CarrierT, Optional[Context], Getter[CarrierT]) -> Context
|
||||
if context is None:
|
||||
context = get_current()
|
||||
|
||||
sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME)
|
||||
if not sentry_trace:
|
||||
return context
|
||||
|
||||
sentrytrace = extract_sentrytrace_data(sentry_trace[0])
|
||||
if not sentrytrace:
|
||||
return context
|
||||
|
||||
context = set_value(SENTRY_TRACE_KEY, sentrytrace, context)
|
||||
|
||||
trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"]
|
||||
|
||||
span_context = SpanContext(
|
||||
trace_id=int(trace_id, 16), # type: ignore
|
||||
span_id=int(span_id, 16), # type: ignore
|
||||
# we simulate a sampled trace on the otel side and leave the sampling to sentry
|
||||
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
||||
is_remote=True,
|
||||
)
|
||||
|
||||
baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME)
|
||||
|
||||
if baggage_header:
|
||||
baggage = Baggage.from_incoming_header(baggage_header[0])
|
||||
else:
|
||||
# If there's an incoming sentry-trace but no incoming baggage header,
|
||||
# for instance in traces coming from older SDKs,
|
||||
# baggage will be empty and frozen and won't be populated as head SDK.
|
||||
baggage = Baggage(sentry_items={})
|
||||
|
||||
baggage.freeze()
|
||||
context = set_value(SENTRY_BAGGAGE_KEY, baggage, context)
|
||||
|
||||
span = NonRecordingSpan(span_context)
|
||||
modified_context = trace.set_span_in_context(span, context)
|
||||
return modified_context
|
||||
|
||||
def inject(self, carrier, context=None, setter=default_setter):
|
||||
# type: (CarrierT, Optional[Context], Setter[CarrierT]) -> None
|
||||
if context is None:
|
||||
context = get_current()
|
||||
|
||||
current_span = trace.get_current_span(context)
|
||||
current_span_context = current_span.get_span_context()
|
||||
|
||||
if not current_span_context.is_valid:
|
||||
return
|
||||
|
||||
span_id = trace.format_span_id(current_span_context.span_id)
|
||||
|
||||
span_map = SentrySpanProcessor().otel_span_map
|
||||
sentry_span = span_map.get(span_id, None)
|
||||
if not sentry_span:
|
||||
return
|
||||
|
||||
setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent())
|
||||
|
||||
if sentry_span.containing_transaction:
|
||||
baggage = sentry_span.containing_transaction.get_baggage()
|
||||
if baggage:
|
||||
baggage_data = baggage.serialize()
|
||||
if baggage_data:
|
||||
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
# type: () -> Set[str]
|
||||
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME}
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from opentelemetry.context import get_value
|
||||
from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan as OTelSpan
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace import (
|
||||
format_span_id,
|
||||
format_trace_id,
|
||||
get_current_span,
|
||||
SpanKind,
|
||||
)
|
||||
from opentelemetry.trace.span import (
|
||||
INVALID_SPAN_ID,
|
||||
INVALID_TRACE_ID,
|
||||
)
|
||||
from sentry_sdk import get_client, start_transaction
|
||||
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS
|
||||
from sentry_sdk.integrations.opentelemetry.consts import (
|
||||
SENTRY_BAGGAGE_KEY,
|
||||
SENTRY_TRACE_KEY,
|
||||
)
|
||||
from sentry_sdk.scope import add_global_event_processor
|
||||
from sentry_sdk.tracing import Transaction, Span as SentrySpan
|
||||
from sentry_sdk.utils import Dsn
|
||||
|
||||
from urllib3.util import parse_url as urlparse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Optional, Union
|
||||
from opentelemetry import context as context_api
|
||||
from sentry_sdk._types import Event, Hint
|
||||
|
||||
OPEN_TELEMETRY_CONTEXT = "otel"
|
||||
SPAN_MAX_TIME_OPEN_MINUTES = 10
|
||||
SPAN_ORIGIN = "auto.otel"
|
||||
|
||||
|
||||
def link_trace_context_to_error_event(event, otel_span_map):
|
||||
# type: (Event, dict[str, Union[Transaction, SentrySpan]]) -> Event
|
||||
client = get_client()
|
||||
|
||||
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
||||
return event
|
||||
|
||||
if hasattr(event, "type") and event["type"] == "transaction":
|
||||
return event
|
||||
|
||||
otel_span = get_current_span()
|
||||
if not otel_span:
|
||||
return event
|
||||
|
||||
ctx = otel_span.get_span_context()
|
||||
|
||||
if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID:
|
||||
return event
|
||||
|
||||
sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None)
|
||||
if not sentry_span:
|
||||
return event
|
||||
|
||||
contexts = event.setdefault("contexts", {})
|
||||
contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())
|
||||
|
||||
return event
|
||||
|
||||
|
||||
class SentrySpanProcessor(SpanProcessor):
|
||||
"""
|
||||
Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
|
||||
"""
|
||||
|
||||
# The mapping from otel span ids to sentry spans
|
||||
otel_span_map = {} # type: dict[str, Union[Transaction, SentrySpan]]
|
||||
|
||||
# The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES
|
||||
open_spans = {} # type: dict[int, set[str]]
|
||||
|
||||
def __new__(cls):
|
||||
# type: () -> SentrySpanProcessor
|
||||
if not hasattr(cls, "instance"):
|
||||
cls.instance = super().__new__(cls)
|
||||
|
||||
return cls.instance
|
||||
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
@add_global_event_processor
|
||||
def global_event_processor(event, hint):
|
||||
# type: (Event, Hint) -> Event
|
||||
return link_trace_context_to_error_event(event, self.otel_span_map)
|
||||
|
||||
def _prune_old_spans(self):
|
||||
# type: (SentrySpanProcessor) -> None
|
||||
"""
|
||||
Prune spans that have been open for too long.
|
||||
"""
|
||||
current_time_minutes = int(time() / 60)
|
||||
for span_start_minutes in list(
|
||||
self.open_spans.keys()
|
||||
): # making a list because we change the dict
|
||||
# prune empty open spans buckets
|
||||
if self.open_spans[span_start_minutes] == set():
|
||||
self.open_spans.pop(span_start_minutes)
|
||||
|
||||
# prune old buckets
|
||||
elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES:
|
||||
for span_id in self.open_spans.pop(span_start_minutes):
|
||||
self.otel_span_map.pop(span_id, None)
|
||||
|
||||
def on_start(self, otel_span, parent_context=None):
|
||||
# type: (OTelSpan, Optional[context_api.Context]) -> None
|
||||
client = get_client()
|
||||
|
||||
if not client.dsn:
|
||||
return
|
||||
|
||||
try:
|
||||
_ = Dsn(client.dsn)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
||||
return
|
||||
|
||||
if not otel_span.get_span_context().is_valid:
|
||||
return
|
||||
|
||||
if self._is_sentry_span(otel_span):
|
||||
return
|
||||
|
||||
trace_data = self._get_trace_data(otel_span, parent_context)
|
||||
|
||||
parent_span_id = trace_data["parent_span_id"]
|
||||
sentry_parent_span = (
|
||||
self.otel_span_map.get(parent_span_id) if parent_span_id else None
|
||||
)
|
||||
|
||||
start_timestamp = None
|
||||
if otel_span.start_time is not None:
|
||||
start_timestamp = datetime.fromtimestamp(
|
||||
otel_span.start_time / 1e9, timezone.utc
|
||||
) # OTel spans have nanosecond precision
|
||||
|
||||
sentry_span = None
|
||||
if sentry_parent_span:
|
||||
sentry_span = sentry_parent_span.start_child(
|
||||
span_id=trace_data["span_id"],
|
||||
name=otel_span.name,
|
||||
start_timestamp=start_timestamp,
|
||||
instrumenter=INSTRUMENTER.OTEL,
|
||||
origin=SPAN_ORIGIN,
|
||||
)
|
||||
else:
|
||||
sentry_span = start_transaction(
|
||||
name=otel_span.name,
|
||||
span_id=trace_data["span_id"],
|
||||
parent_span_id=parent_span_id,
|
||||
trace_id=trace_data["trace_id"],
|
||||
baggage=trace_data["baggage"],
|
||||
start_timestamp=start_timestamp,
|
||||
instrumenter=INSTRUMENTER.OTEL,
|
||||
origin=SPAN_ORIGIN,
|
||||
)
|
||||
|
||||
self.otel_span_map[trace_data["span_id"]] = sentry_span
|
||||
|
||||
if otel_span.start_time is not None:
|
||||
span_start_in_minutes = int(
|
||||
otel_span.start_time / 1e9 / 60
|
||||
) # OTel spans have nanosecond precision
|
||||
self.open_spans.setdefault(span_start_in_minutes, set()).add(
|
||||
trace_data["span_id"]
|
||||
)
|
||||
|
||||
self._prune_old_spans()
|
||||
|
||||
def on_end(self, otel_span):
|
||||
# type: (OTelSpan) -> None
|
||||
client = get_client()
|
||||
|
||||
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
|
||||
return
|
||||
|
||||
span_context = otel_span.get_span_context()
|
||||
if not span_context.is_valid:
|
||||
return
|
||||
|
||||
span_id = format_span_id(span_context.span_id)
|
||||
sentry_span = self.otel_span_map.pop(span_id, None)
|
||||
if not sentry_span:
|
||||
return
|
||||
|
||||
sentry_span.op = otel_span.name
|
||||
|
||||
self._update_span_with_otel_status(sentry_span, otel_span)
|
||||
|
||||
if isinstance(sentry_span, Transaction):
|
||||
sentry_span.name = otel_span.name
|
||||
sentry_span.set_context(
|
||||
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
|
||||
)
|
||||
self._update_transaction_with_otel_data(sentry_span, otel_span)
|
||||
|
||||
else:
|
||||
self._update_span_with_otel_data(sentry_span, otel_span)
|
||||
|
||||
end_timestamp = None
|
||||
if otel_span.end_time is not None:
|
||||
end_timestamp = datetime.fromtimestamp(
|
||||
otel_span.end_time / 1e9, timezone.utc
|
||||
) # OTel spans have nanosecond precision
|
||||
|
||||
sentry_span.finish(end_timestamp=end_timestamp)
|
||||
|
||||
if otel_span.start_time is not None:
|
||||
span_start_in_minutes = int(
|
||||
otel_span.start_time / 1e9 / 60
|
||||
) # OTel spans have nanosecond precision
|
||||
self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id)
|
||||
|
||||
self._prune_old_spans()
|
||||
|
||||
def _is_sentry_span(self, otel_span):
|
||||
# type: (OTelSpan) -> bool
|
||||
"""
|
||||
Break infinite loop:
|
||||
HTTP requests to Sentry are caught by OTel and send again to Sentry.
|
||||
"""
|
||||
otel_span_url = None
|
||||
if otel_span.attributes is not None:
|
||||
otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
|
||||
otel_span_url = cast("Optional[str]", otel_span_url)
|
||||
|
||||
dsn_url = None
|
||||
client = get_client()
|
||||
if client.dsn:
|
||||
dsn_url = Dsn(client.dsn).netloc
|
||||
|
||||
if otel_span_url and dsn_url and dsn_url in otel_span_url:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_otel_context(self, otel_span):
|
||||
# type: (OTelSpan) -> dict[str, Any]
|
||||
"""
|
||||
Returns the OTel context for Sentry.
|
||||
See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context
|
||||
"""
|
||||
ctx = {}
|
||||
|
||||
if otel_span.attributes:
|
||||
ctx["attributes"] = dict(otel_span.attributes)
|
||||
|
||||
if otel_span.resource.attributes:
|
||||
ctx["resource"] = dict(otel_span.resource.attributes)
|
||||
|
||||
return ctx
|
||||
|
||||
def _get_trace_data(self, otel_span, parent_context):
|
||||
# type: (OTelSpan, Optional[context_api.Context]) -> dict[str, Any]
|
||||
"""
|
||||
Extracts tracing information from one OTel span and its parent OTel context.
|
||||
"""
|
||||
trace_data = {} # type: dict[str, Any]
|
||||
span_context = otel_span.get_span_context()
|
||||
|
||||
span_id = format_span_id(span_context.span_id)
|
||||
trace_data["span_id"] = span_id
|
||||
|
||||
trace_id = format_trace_id(span_context.trace_id)
|
||||
trace_data["trace_id"] = trace_id
|
||||
|
||||
parent_span_id = (
|
||||
format_span_id(otel_span.parent.span_id) if otel_span.parent else None
|
||||
)
|
||||
trace_data["parent_span_id"] = parent_span_id
|
||||
|
||||
sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context)
|
||||
sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data)
|
||||
trace_data["parent_sampled"] = (
|
||||
sentry_trace_data["parent_sampled"] if sentry_trace_data else None
|
||||
)
|
||||
|
||||
baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context)
|
||||
trace_data["baggage"] = baggage
|
||||
|
||||
return trace_data
|
||||
|
||||
def _update_span_with_otel_status(self, sentry_span, otel_span):
|
||||
# type: (SentrySpan, OTelSpan) -> None
|
||||
"""
|
||||
Set the Sentry span status from the OTel span
|
||||
"""
|
||||
if otel_span.status.is_unset:
|
||||
return
|
||||
|
||||
if otel_span.status.is_ok:
|
||||
sentry_span.set_status(SPANSTATUS.OK)
|
||||
return
|
||||
|
||||
sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR)
|
||||
|
||||
def _update_span_with_otel_data(self, sentry_span, otel_span):
|
||||
# type: (SentrySpan, OTelSpan) -> None
|
||||
"""
|
||||
Convert OTel span data and update the Sentry span with it.
|
||||
This should eventually happen on the server when ingesting the spans.
|
||||
"""
|
||||
sentry_span.set_data("otel.kind", otel_span.kind)
|
||||
|
||||
op = otel_span.name
|
||||
description = otel_span.name
|
||||
|
||||
if otel_span.attributes is not None:
|
||||
for key, val in otel_span.attributes.items():
|
||||
sentry_span.set_data(key, val)
|
||||
|
||||
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
|
||||
http_method = cast("Optional[str]", http_method)
|
||||
|
||||
db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM)
|
||||
|
||||
if http_method:
|
||||
op = "http"
|
||||
|
||||
if otel_span.kind == SpanKind.SERVER:
|
||||
op += ".server"
|
||||
elif otel_span.kind == SpanKind.CLIENT:
|
||||
op += ".client"
|
||||
|
||||
description = http_method
|
||||
|
||||
peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None)
|
||||
if peer_name:
|
||||
description += " {}".format(peer_name)
|
||||
|
||||
target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None)
|
||||
if target:
|
||||
description += " {}".format(target)
|
||||
|
||||
if not peer_name and not target:
|
||||
url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None)
|
||||
url = cast("Optional[str]", url)
|
||||
if url:
|
||||
parsed_url = urlparse(url)
|
||||
url = "{}://{}{}".format(
|
||||
parsed_url.scheme, parsed_url.netloc, parsed_url.path
|
||||
)
|
||||
description += " {}".format(url)
|
||||
|
||||
status_code = otel_span.attributes.get(
|
||||
SpanAttributes.HTTP_STATUS_CODE, None
|
||||
)
|
||||
status_code = cast("Optional[int]", status_code)
|
||||
if status_code:
|
||||
sentry_span.set_http_status(status_code)
|
||||
|
||||
elif db_query:
|
||||
op = "db"
|
||||
statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None)
|
||||
statement = cast("Optional[str]", statement)
|
||||
if statement:
|
||||
description = statement
|
||||
|
||||
sentry_span.op = op
|
||||
sentry_span.description = description
|
||||
|
||||
def _update_transaction_with_otel_data(self, sentry_span, otel_span):
|
||||
# type: (SentrySpan, OTelSpan) -> None
|
||||
if otel_span.attributes is None:
|
||||
return
|
||||
|
||||
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
|
||||
|
||||
if http_method:
|
||||
status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
|
||||
status_code = cast("Optional[int]", status_code)
|
||||
if status_code:
|
||||
sentry_span.set_http_status(status_code)
|
||||
|
||||
op = "http"
|
||||
|
||||
if otel_span.kind == SpanKind.SERVER:
|
||||
op += ".server"
|
||||
elif otel_span.kind == SpanKind.CLIENT:
|
||||
op += ".client"
|
||||
|
||||
sentry_span.op = op
|
||||
Loading…
Add table
Add a link
Reference in a new issue