Skip to content

Commit

Permalink
Basic OTel support (#1772)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
antonpirker authored Dec 14, 2022
1 parent eb0db0a commit d0eed0e
Show file tree
Hide file tree
Showing 12 changed files with 1,154 additions and 11 deletions.
73 changes: 73 additions & 0 deletions .github/workflows/test-integration-opentelemetry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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

strategy:
fail-fast: false
matrix:
python-version: ["3.7","3.8","3.9","3.10"]
# python3.6 reached EOL and is no longer being supported on
# new versions of hosted runners on Github Actions
# ubuntu-20.04 is the last version that supported python3.6
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877
os: [ubuntu-20.04]

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

- name: Setup Test Env
run: |
pip install codecov "tox>=3,<4"
- name: Test opentelemetry
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
check_required_tests:
name: All opentelemetry tests passed or skipped
needs: test
# Always run this, even if a dependent job failed
if: always()
runs-on: ubuntu-20.04
steps:
- name: Check for failures
if: contains(needs.test.result, 'failure')
run: |
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1
7 changes: 7 additions & 0 deletions sentry_sdk/integrations/opentelemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401
SentrySpanProcessor,
)

from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401
SentryPropagator,
)
6 changes: 6 additions & 0 deletions sentry_sdk/integrations/opentelemetry/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from opentelemetry.context import ( # type: ignore
create_key,
)

SENTRY_TRACE_KEY = create_key("sentry-trace")
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
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,
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.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 sentry_sdk._types import MYPY

if MYPY:
from typing import Optional
from typing import Set


class SentryPropagator(TextMapPropagator): # type: ignore
"""
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) -> 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) -> None
if context is None:
context = get_current()

current_span = trace.get_current_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())

baggage = sentry_span.containing_transaction.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

0 comments on commit d0eed0e

Please sign in to comment.