10000 Basic OTel support (#1772) · getsentry/sentry-python@d0eed0e · GitHub
[go: up one dir, main page]

Skip to content

Commit d0eed0e

Browse files
authored
Basic OTel support (#1772)
Adding basic OpenTelementry (OTel) support to the Sentry SDK: - Adding a OTel SpanProcessor that can receive spans form OTel and then convert them into Sentry Spans and send them to Sentry. - Adding a OTel Propagator that can receive and propagate trace headers (Baggage) to keep distributed tracing intact.
1 parent eb0db0a commit d0eed0e

File tree

12 files changed

+1154
-11
lines changed

12 files changed

+1154
-11
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Test opentelemetry
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- release/**
8+
9+
pull_request:
10+
11+
# Cancel in progress workflows on pull_requests.
12+
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
BUILD_CACHE_KEY: ${{ github.sha }}
22+
CACHED_BUILD_PATHS: |
23+
${{ github.workspace }}/dist-serverless
24+
25+
jobs:
26+
test:
27+
name: opentelemetry, python ${{ matrix.python-version }}, ${{ matrix.os }}
28+
runs-on: ${{ matrix.os }}
29+
timeout-minutes: 45
30+
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
python-version: ["3.7","3.8","3.9","3.10"]
35+
# python3.6 reached EOL and is no longer being supported on
36+
# new versions of hosted runners on Github Actions
37+
# ubuntu-20.04 is the last version that supported python3.6
38+
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877
39+
os: [ubuntu-20.04]
40+
41+
steps:
42+
- uses: actions/checkout@v3
43+
- uses: actions/setup-python@v4
44+
with:
45+
python-version: ${{ matrix.python-version }}
46+
47+
- name: Setup Test Env
48+
run: |
49+
pip install codecov "tox>=3,<4"
50+
51+
- name: Test opentelemetry
52+
timeout-minutes: 45
53+
shell: bash
54+
run: |
55+
set -x # print commands that are executed
56+
coverage erase
57+
58+
./scripts/runtox.sh "${{ matrix.python-version }}-opentelemetry" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
59+
coverage combine .coverage*
60+
coverage xml -i
61+
codecov --file coverage.xml
62+
63+
check_required_tests:
64+
name: All opentelemetry tests passed or skipped
65+
needs: test
66+
# Always run this, even if a dependent job failed
67+
if: always()
68+
runs-on: ubuntu-20.04
69+
steps:
70+
- name: Check for failures
71+
if: contains(needs.test.result, 'failure')
72+
run: |
73+
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401
2+
SentrySpanProcessor,
3+
)
4+
5+
from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401
6+
SentryPropagator,
7+
)
Lines changed: 6 additions & 0 deletions
B428
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from opentelemetry.context import ( # type: ignore
2+
create_key,
3+
)
4+
5+
SENTRY_TRACE_KEY = create_key("sentry-trace")
6+
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from opentelemetry import trace # type: ignore
2+
from opentelemetry.context import ( # type: ignore
3+
Context,
4+
get_current,
5+
set_value,
6+
)
7+
from opentelemetry.propagators.textmap import ( # type: ignore
8+
CarrierT,
9+
Getter,
10+
Setter,
11+
TextMapPropagator,
12+
default_getter,
13+
default_setter,
14+
)
15+
from opentelemetry.trace import ( # type: ignore
16+
TraceFlags,
17+
NonRecordingSpan,
18+
SpanContext,
19+
)
20+
from sentry_sdk.integrations.opentelemetry.consts import (
21+
SENTRY_BAGGAGE_KEY,
22+
SENTRY_TRACE_KEY,
23+
)
24+
from sentry_sdk.integrations.opentelemetry.span_processor import (
25+
SentrySpanProcessor,
26+
)
27+
28+
from sentry_sdk.tracing import (
29+
BAGGAGE_HEADER_NAME,
30+
SENTRY_TRACE_HEADER_NAME,
31+
)
32+
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data
33+
from sentry_sdk._types import MYPY
34+
35+
if MYPY:
36+
from typing import Optional
37+
from typing import Set
38+
39+
40+
class SentryPropagator(TextMapPropagator): # type: ignore
41+
"""
42+
Propagates tracing headers for Sentry's tracing system in a way OTel understands.
43+
"""
44+
45+
def extract(self, carrier, context=None, getter=default_getter):
46+
# type: (CarrierT, Optional[Context], Getter) -> Context
47+
if context is None:
48+
context = get_current()
49+
50+
sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME)
51+
if not sentry_trace:
52+
return context
53+
54+
sentrytrace = extract_sentrytrace_data(sentry_trace[0])
55+
if not sentrytrace:
56+
return context
57+
58+
context = set_value(SENTRY_TRACE_KEY, sentrytrace, context)
59+
60+
trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"]
61+
62+
span_context = SpanContext(
63+
trace_id=int(trace_id, 16), # type: ignore
64+
span_id=int(span_id, 16), # type: ignore
65+
# we simulate a sampled trace on the otel side and leave the sampling to sentry
66+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
67+
is_remote=True,
68+
)
69+
70+
baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME)
71+
72+
if baggage_header:
73+
baggage = Baggage.from_incoming_header(baggage_header[0])
74+
else:
75+
# If there's an incoming sentry-trace but no incoming baggage header,
76+
# for instance in traces coming from older SDKs,
77+
# baggage will be empty and frozen and won't be populated as head SDK.
78+
baggage = Baggage(sentry_items={})
79+
80+
baggage.freeze()
81+
context = set_value(SENTRY_BAGGAGE_KEY, baggage, context)
82+
83+
span = NonRecordingSpan(span_context)
84+
modified_context = trace.set_span_in_context(span, context)
85+
return modified_context
86+
87+
def inject(self, carrier, context=None, setter=default_setter):
88+
# type: (CarrierT, Optional[Context], Setter) -> None
89+
if context is None:
90+
context = get_current()
91+
92+
current_span = trace.get_current_span(context)
93+
94+
if not current_span.context.is_valid:
95+
return
96+
97+
span_id = trace.format_span_id(current_span.context.span_id)
98+
99+
span_map = SentrySpanProcessor().otel_span_map
100+
sentry_span = span_map.get(span_id, None)
101+
if not sentry_span:
102+
return
103+
104+
setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent())
105+
106+
baggage = sentry_span.containing_transaction.get_baggage()
107+
if baggage:
108+
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage.serialize())
109+
110+
@property
111+
def fields(self):
112+
# type: () -> Set[str]
113+
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME}

0 commit comments

Comments
 (0)
0