Skip to content
Closed
62 changes: 62 additions & 0 deletions .github/workflows/test-integration-opentelemetry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Test opentelemetry

on:
push:
branches:
- master
- release/**

pull_request:

# Cancel in progress workflows on pull_requests.
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

env:
BUILD_CACHE_KEY: ${{ github.sha }}
CACHED_BUILD_PATHS: |
${{ github.workspace }}/dist-serverless

jobs:
test:
name: opentelemetry, python ${{ matrix.python-version }}, ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
continue-on-error: true

strategy:
matrix:
python-version: ["3.7","3.8","3.9","3.10"]
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Setup Test Env
env:
PGHOST: localhost
PGPASSWORD: sentry
run: |
pip install codecov tox

- name: Test opentelemetry
env:
CI_PYTHON_VERSION: ${{ matrix.python-version }}
timeout-minutes: 45
shell: bash
run: |
set -x # print commands that are executed
coverage erase

./scripts/runtox.sh "${{ matrix.python-version }}-opentelemetry" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
coverage combine .coverage*
coverage xml -i
codecov --file coverage.xml
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"python.pythonPath": ".venv/bin/python",
"python.formatting.provider": "black"
}
"python.formatting.provider": "black",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
3 changes: 2 additions & 1 deletion sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sentry_sdk.scope import Scope

from sentry_sdk._types import MYPY
from sentry_sdk.tracing import NoOpSpan

if MYPY:
from typing import Any
Expand Down Expand Up @@ -210,5 +211,5 @@ def start_transaction(
transaction=None, # type: Optional[Transaction]
**kwargs # type: Any
):
# type: (...) -> Transaction
# type: (...) -> Union[Transaction, NoOpSpan]
return Hub.current.start_transaction(transaction, **kwargs)
4 changes: 4 additions & 0 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sentry_sdk.transport import make_transport
from sentry_sdk.consts import (
DEFAULT_OPTIONS,
INSTRUMENTER,
VERSION,
ClientConstructor,
)
Expand Down Expand Up @@ -86,6 +87,9 @@ def _get_options(*args, **kwargs):
if rv["server_name"] is None and hasattr(socket, "gethostname"):
rv["server_name"] = socket.gethostname()

if rv["instrumenter"] is None:
rv["instrumenter"] = INSTRUMENTER.SENTRY

return rv


Expand Down
56 changes: 31 additions & 25 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@
DEFAULT_MAX_BREADCRUMBS = 100


class INSTRUMENTER:
SENTRY = "sentry"
OTEL = "otel"


class OP:
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
FUNCTION = "function"
FUNCTION_AWS = "function.aws"
FUNCTION_GCP = "function.gcp"
HTTP_CLIENT = "http.client"
HTTP_CLIENT_STREAM = "http.client.stream"
HTTP_SERVER = "http.server"
MIDDLEWARE_DJANGO = "middleware.django"
MIDDLEWARE_STARLETTE = "middleware.starlette"
MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive"
MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
QUEUE_TASK_CELERY = "queue.task.celery"
QUEUE_TASK_RQ = "queue.task.rq"
SUBPROCESS = "subprocess"
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_RENDER = "view.render"
WEBSOCKET_SERVER = "websocket.server"


# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
# take these arguments (even though they take opaque **kwargs)
class ClientConstructor(object):
Expand All @@ -57,6 +87,7 @@ def __init__(
server_name=None, # type: Optional[str]
shutdown_timeout=2, # type: float
integrations=[], # type: Sequence[Integration] # noqa: B006
instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str]
in_app_include=[], # type: List[str] # noqa: B006
in_app_exclude=[], # type: List[str] # noqa: B006
default_integrations=True, # type: bool
Expand Down Expand Up @@ -106,28 +137,3 @@ def _get_default_options():


VERSION = "1.11.1"


class OP:
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
FUNCTION = "function"
FUNCTION_AWS = "function.aws"
FUNCTION_GCP = "function.gcp"
HTTP_CLIENT = "http.client"
HTTP_CLIENT_STREAM = "http.client.stream"
HTTP_SERVER = "http.server"
MIDDLEWARE_DJANGO = "middleware.django"
MIDDLEWARE_STARLETTE = "middleware.starlette"
MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive"
MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
QUEUE_TASK_CELERY = "queue.task.celery"
QUEUE_TASK_RQ = "queue.task.rq"
SUBPROCESS = "subprocess"
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_RENDER = "view.render"
WEBSOCKET_SERVER = "websocket.server"
17 changes: 15 additions & 2 deletions sentry_sdk/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from contextlib import contextmanager

from sentry_sdk._compat import with_metaclass
from sentry_sdk.consts import INSTRUMENTER
from sentry_sdk.scope import Scope
from sentry_sdk.client import Client
from sentry_sdk.tracing import Span, Transaction
from sentry_sdk.tracing import NoOpSpan, Span, Transaction
from sentry_sdk.session import Session
from sentry_sdk.utils import (
exc_info_from_error,
Expand Down Expand Up @@ -464,6 +465,12 @@ def start_span(
for every incoming HTTP request. Use `start_transaction` to start a new
transaction when one is not already in progress.
"""
instrumenter = kwargs.get("instrumenter", INSTRUMENTER.SENTRY)
configuration_instrumenter = self.client and self.client.options["instrumenter"]

if instrumenter != configuration_instrumenter:
return NoOpSpan()

# TODO: consider removing this in a future release.
# This is for backwards compatibility with releases before
# start_transaction existed, to allow for a smoother transition.
Expand Down Expand Up @@ -496,7 +503,7 @@ def start_transaction(
transaction=None, # type: Optional[Transaction]
**kwargs # type: Any
):
# type: (...) -> Transaction
# type: (...) -> Union[Transaction, NoOpSpan]
"""
Start and return a transaction.

Expand All @@ -519,6 +526,12 @@ def start_transaction(
When the transaction is finished, it will be sent to Sentry with all its
finished child spans.
"""
instrumenter = kwargs.get("instrumenter", INSTRUMENTER.SENTRY)
configuration_instrumenter = self.client and self.client.options["instrumenter"]

if instrumenter != configuration_instrumenter:
return NoOpSpan()

custom_sampling_context = kwargs.pop("custom_sampling_context", {})

# if we haven't been given a transaction, make one
Expand Down
9 changes: 6 additions & 3 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import Scope
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing import SENTRY_TRACE_HEADER_NAME, SOURCE_FOR_STYLE
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand Down Expand Up @@ -101,8 +101,11 @@ def _add_sentry_trace(sender, template, context, **extra):
sentry_span = Hub.current.scope.span
context["sentry_trace"] = (
Markup(
'<meta name="sentry-trace" content="%s" />'
% (sentry_span.to_traceparent(),)
'<meta name="%s" content="%s" />'
% (
SENTRY_TRACE_HEADER_NAME,
sentry_span.to_traceparent(),
)
)
if sentry_span
else ""
Expand Down
3 changes: 3 additions & 0 deletions sentry_sdk/integrations/opentelemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401
SentrySpanProcessor,
)
113 changes: 113 additions & 0 deletions sentry_sdk/integrations/opentelemetry/propagator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from opentelemetry import trace # type: ignore
from opentelemetry.context import ( # type: ignore
Context,
create_key,
get_current,
set_value,
)
from opentelemetry.propagators.textmap import ( # type: ignore
CarrierT,
Getter,
Setter,
TextMapPropagator,
default_getter,
default_setter,
)
from opentelemetry.trace import ( # type: ignore
TraceFlags,
NonRecordingSpan,
SpanContext,
)

from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
)
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data
from sentry_sdk._types import MYPY

if MYPY:
from typing import Optional
from typing import Set


SENTRY_TRACE_KEY = create_key("sentry-trace")
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")


class SentryPropagator(TextMapPropagator): # type: ignore
def extract(self, carrier, context=None, getter=default_getter):
# type: (CarrierT, Optional[Context], Getter) -> 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

sentry_trace_data = (
sentrytrace.get("trace_id", "0"),
sentrytrace.get("parent_span_id", "0"),
sentrytrace.get("parent_sampled", None),
)

context = set_value(SENTRY_TRACE_KEY, sentry_trace_data, context)

trace_id, span_id, _ = sentry_trace_data

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) -> None
if context is None:
context = get_current()

current_span = trace.get_current_span(context)
span_id = trace.format_span_id(current_span.context.span_id)

from sentry_sdk.integrations.opentelemetry.span_processor import (
SentrySpanProcessor,
)

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())

baggage = hasattr(sentry_span, "get_baggage") and sentry_span.get_baggage()
if baggage:
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage.serialize())

@property
def fields(self):
# type: () -> Set[str]
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME}
Loading