Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic OTel support #1772

Merged
merged 19 commits into from Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/test-integration-opentelemetry.yml
@@ -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: 6 additions & 0 deletions sentry_sdk/integrations/opentelemetry/__init__.py
@@ -0,0 +1,6 @@
from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401
SentrySpanProcessor,
)
from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401
SentryPropagator,
)
117 changes: 117 additions & 0 deletions sentry_sdk/integrations/opentelemetry/propagator.py
@@ -0,0 +1,117 @@
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
"""
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

sentry_trace_data = (
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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])
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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)
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
span_id = trace.format_span_id(current_span.context.span_id)

from sentry_sdk.integrations.opentelemetry.span_processor import (
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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()
antonpirker marked this conversation as resolved.
Show resolved Hide resolved
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}