Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pip-wheel-metadata
.mypy_cache
.vscode/
.claude/
.tool-versions

# for running AWS Lambda tests using AWS SAM
sam.template.yaml
1 change: 1 addition & 0 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ def _capture_envelope(envelope):
"auto_enabling_integrations"
],
disabled_integrations=self.options["disabled_integrations"],
options=self.options,
)

spotlight_config = self.options.get("spotlight")
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class EndpointType(Enum):
"""

ENVELOPE = "envelope"
OTLP_TRACES = "integration/otlp/v1/traces"


class CompressionAlgo(Enum):
Expand Down
12 changes: 11 additions & 1 deletion sentry_sdk/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Set
from typing import Type
from typing import Union
from typing import Any


_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600))
Expand Down Expand Up @@ -175,8 +176,9 @@ def setup_integrations(
with_defaults=True,
with_auto_enabling_integrations=False,
disabled_integrations=None,
options=None,
):
# type: (Sequence[Integration], bool, bool, Optional[Sequence[Union[type[Integration], Integration]]]) -> Dict[str, Integration]
# type: (Sequence[Integration], bool, bool, Optional[Sequence[Union[type[Integration], Integration]]], Optional[Dict[str, Any]]) -> Dict[str, Integration]
"""
Given a list of integration instances, this installs them all.

Expand Down Expand Up @@ -221,6 +223,7 @@ def setup_integrations(
)
try:
type(integration).setup_once()
integration.setup_once_with_options(options)
except DidNotEnable as e:
if identifier not in used_as_default_integration:
raise
Expand Down Expand Up @@ -300,3 +303,10 @@ def setup_once():
instance again.
"""
pass

def setup_once_with_options(self, options=None):
# type: (Optional[Dict[str, Any]]) -> None
"""
Called after setup_once in rare cases on the instance and options since we don't have those available above..
"""
pass
101 changes: 101 additions & 0 deletions sentry_sdk/integrations/otlp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.utils import logger, Dsn
from sentry_sdk.consts import VERSION, EndpointType

try:
from opentelemetry import trace
from opentelemetry.propagate import set_global_textmap
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
except ImportError:
raise DidNotEnable("opentelemetry-distro[otlp] is not installed")

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional, Dict, Any
from sentry_sdk._types import Event, Hint


def link_event_to_trace(event, _hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
"""
Get the trace info from the running otel span and
update the trace context on sentry events of other types
(errors, logs, checkins, etc).
"""
if hasattr(event, "type") and event["type"] == "transaction":
return event

otel_span = trace.get_current_span()
if not otel_span:
return event

ctx = otel_span.get_span_context()

if ctx.trace_id == trace.INVALID_TRACE_ID or ctx.span_id == trace.INVALID_SPAN_ID:
return event

contexts = event.setdefault("contexts", {})
contexts.setdefault("trace", {}).update(
{
"trace_id": trace.format_trace_id(ctx.trace_id),
"span_id": trace.format_span_id(ctx.span_id),
"status": "ok", # TODO
}
)

return event


def setup_otlp_exporter(dsn=None):
# type: (Optional[str]) -> None
tracer_provider = trace.get_tracer_provider()

if not isinstance(tracer_provider, TracerProvider):
logger.debug("[OTLP] No TracerProvider configured by user, creating a new one")
tracer_provider = TracerProvider()
trace.set_tracer_provider(tracer_provider)

endpoint = None
headers = None
if dsn:
auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}")
endpoint = auth.get_api_url(EndpointType.OTLP_TRACES)
headers = {"X-Sentry-Auth": auth.to_header()}
logger.debug(f"[OTLP] Sending traces to {endpoint}")

otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
span_processor = BatchSpanProcessor(otlp_exporter)
tracer_provider.add_span_processor(span_processor)


class OTLPIntegration(Integration):
identifier = "otlp"

def __init__(self, setup_otlp_exporter=True, setup_propagator=True):
# type: (bool, bool) -> None
self.setup_otlp_exporter = setup_otlp_exporter
self.setup_propagator = setup_propagator

@staticmethod
def setup_once():
# type: () -> None
logger.debug("[OTLP] Setting up trace linking for all events")
add_global_event_processor(link_event_to_trace)

def setup_once_with_options(self, options=None):
# type: (Optional[Dict[str, Any]]) -> None
if self.setup_otlp_exporter:
logger.debug("[OTLP] Setting up OTLP exporter")
dsn = options.get("dsn") if options else None # type: Optional[str]
setup_otlp_exporter(dsn)

if self.setup_propagator:
logger.debug("[OTLP] Setting up propagator for distributed tracing")
# TODO-neel better propagator support, chain with existing ones if possible instead of replacing
set_global_textmap(SentryPropagator())
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def get_file_text(file_name):
"openai": ["openai>=1.0.0", "tiktoken>=0.3.0"],
"openfeature": ["openfeature-sdk>=0.7.1"],
"opentelemetry": ["opentelemetry-distro>=0.35b0"],
"opentelemetry-experimental": ["opentelemetry-distro"],
"opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"],
"pure-eval": ["pure_eval", "executing", "asttokens"],
"pydantic_ai": ["pydantic-ai>=1.0.0"],
"pymongo": ["pymongo>=3.1"],
Expand Down
7 changes: 7 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ envlist =
# OpenTelemetry (OTel)
{py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry

# OpenTelemetry with OTLP
{py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-otlp

# OpenTelemetry Experimental (POTel)
{py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel

Expand Down Expand Up @@ -342,6 +345,9 @@ deps =
# OpenTelemetry (OTel)
opentelemetry: opentelemetry-distro

# OpenTelemetry with OTLP
otlp: opentelemetry-distro[otlp]

# OpenTelemetry Experimental (POTel)
potel: -e .[opentelemetry-experimental]

Expand Down Expand Up @@ -765,6 +771,7 @@ setenv =
cloud_resource_context: TESTPATH=tests/integrations/cloud_resource_context
gcp: TESTPATH=tests/integrations/gcp
opentelemetry: TESTPATH=tests/integrations/opentelemetry
otlp: TESTPATH=tests/integrations/otlp
potel: TESTPATH=tests/integrations/opentelemetry
socket: TESTPATH=tests/integrations/socket

Expand Down
Loading