10000 Basic OpenTelemetry (OTel) Support by antonpirker · Pull Request #1692 · getsentry/sentry-python · GitHub
[go: up one dir, main page]

Skip to content

Basic OpenTelemetry (OTel) Support #1692

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
60c53b7
Added boilerplate for otel.
antonpirker Oct 19, 2022
0832886
Merge branch 'master' into antonpirker/1687-basic-otel-support
antonpirker Oct 20, 2022
b575bfc
Added first version of a span processor
antonpirker Oct 21, 2022
4de5aaa
Trying to make a centralized noop for performance capturing functions.
antonpirker Oct 21, 2022
03c4c20
Committing some otel changes from last week.
antonpirker Nov 2, 2022
e28d836
Cleanup
antonpirker Nov 10, 2022
e4bb1ff
Added boilerplate for otel.
antonpirker Oct 19, 2022
6148b59
Added first version of a span processor
antonpirker Oct 21, 2022
b3c537e
Trying to make a centralized noop for performance capturing functions.
antonpirker Nov 10, 2022
fb05f20
Committing some otel changes from last week.
antonpirker Nov 2, 2022
7cf1c94
Cleanup
antonpirker Nov 10, 2022
972636d
Merge branch 'antonpirker/1687-basic-otel-support' of github.com:gets…
antonpirker Nov 10, 2022
0977c0a
Setting otel context in spans
antonpirker Nov 10, 2022
5d9ed16
Setting the contexts on span.
antonpirker Nov 15, 2022
e055964
Merge branch 'master' into antonpirker/1687-basic-otel-support
antonpirker Nov 16, 2022
35b3d88
Wiring up spans correctly
antonpirker Nov 17, 2022
94e0be8
First version of sentry otel propagator.
antonpirker Nov 18, 2022
993dc33
Moved SENTRY_TRACE_HEADER_NAME to tracing.py and using it everywhere
antonpirker Nov 18, 2022
485d776
Fixed extracting tracing information from incoming request
antonpirker Nov 23, 2022
8cf16cb
Get baggabe from the otel propagator and add it to the Sentry transac…
antonpirker Nov 23, 2022
f14c214
Code formatting
antonpirker Nov 23, 2022
c2607a4
Use start and end time from otel span
antonpirker Nov 23, 2022
ff45019
Fixed typing
antonpirker Nov 24, 2022
e403c6d
Made typing Py2.7 compatible
antonpirker Nov 24, 2022
ff6dbe7
Test outline
antonpirker Nov 24, 2022
73307ec
Test extract_sentry_trace
antonpirker Nov 24, 2022
236ce04
Tests for OTel propagator
antonpirker Nov 25, 2022
de399b9
Fixed one test
antonpirker Nov 25, 2022
b3b5453
Added some tests for span processor
antonpirker Nov 25, 2022
1fe8a97
Merge branch 'master' into antonpirker/1687-basic-otel-support
antonpirker Nov 25, 2022
5da4ffe
Made variable name actually pythonic.
antonpirker Nov 25, 2022
edca5b0
Added opentelementry test config
antonpirker Nov 25, 2022
1f5285e
Made variable name actually pythonic.
antonpirker Nov 25, 2022
1eb7295
Tests for on_end
antonpirker Nov 28, 2022
268f8d0
Fixed typo
antonpirker Nov 28, 2022
17f0346
Updated assert commands
antonpirker Nov 28, 2022
d1c1942
Fixed typo
antonpirker Nov 28, 2022
4b662b5
Trigger CI again.
antonpirker Nov 28, 2022
0c67bb8
Make some ifs to work same as in other SDKs for consistency
antonpirker Nov 28, 2022
bc316e0
Fixed typing
antonpirker Nov 28, 2022
4d85ecf
Fixed typing again
antonpirker Nov 28, 2022
63b477e
Fixed instrumenter default values.
antonpirker Nov 28, 2022
0239264
Fixed tests
antonpirker Nov 28, 2022
3ae5e04
Fixed test
antonpirker Nov 28, 2022
88f446b
Fixed infinite loop when otel catches sentry http requests.
antonpirker Nov 29, 2022
1181a63
Fixed HTTP request span description.
antonpirker Nov 29, 2022
3d36914
Fixed typing
antonpirker Nov 29, 2022
d29d78e
Fixed tests
antonpirker Nov 29, 2022
b538f6c
Improved tests
antonpirker Nov 29, 2022
08d0311
Fixed tests
antonpirker Nov 29, 2022
a643fb4
Merge branch 'master' into antonpirker/1687-basic-otel-support
antonpirker Nov 30, 2022
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
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,
)
105 changes: 105 additions & 0 deletions sentry_sdk/integrations/opentelemetry/propagator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 SENTRY_TRACE_HEADER_NAME, Transaction
from sentry_sdk.tracing_utils import Baggage
from sentry_sdk._types import MYPY

if MYPY:
from typing import Optional
from typing import Set


BAGGAGE_HEADER_NAME = "baggage"


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

sentry_trace_data = Transaction.extract_sentry_trace(sentry_trace[0])

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),
span_id=int(span_id, 16),
# 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
0