Skip to content

Commit f898f0f

Browse files
authored
Merge branch 'master' into antonpirker/2289-response-status-code
2 parents fcfb80d + 3d2517d commit f898f0f

File tree

11 files changed

+314
-57
lines changed

11 files changed

+314
-57
lines changed

linter-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mypy==1.4.1
1+
mypy==1.5.1
22
black==23.7.0
33
flake8==5.0.4
44
types-certifi

sentry_sdk/client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
VERSION,
2828
ClientConstructor,
2929
)
30-
from sentry_sdk.integrations import setup_integrations
30+
from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
3131
from sentry_sdk.utils import ContextVar
3232
from sentry_sdk.sessions import SessionFlusher
3333
from sentry_sdk.envelope import Envelope
@@ -237,6 +237,15 @@ def _capture_envelope(envelope):
237237
)
238238
)
239239

240+
if self.options["_experiments"].get("otel_powered_performance", False):
241+
logger.debug(
242+
"[OTel] Enabling experimental OTel-powered performance monitoring."
243+
)
244+
self.options["instrumenter"] = INSTRUMENTER.OTEL
245+
_DEFAULT_INTEGRATIONS.append(
246+
"sentry_sdk.integrations.opentelemetry.OpenTelemetryIntegration",
247+
)
248+
240249
self.integrations = setup_integrations(
241250
self.options["integrations"],
242251
with_defaults=self.options["default_integrations"],

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
# TODO: Remove these 2 profiling related experiments
4040
"profiles_sample_rate": Optional[float],
4141
"profiler_mode": Optional[ProfilerMode],
42+
"otel_powered_performance": Optional[bool],
4243
},
4344
total=False,
4445
)

sentry_sdk/integrations/__init__.py

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
"""This package"""
21
from __future__ import absolute_import
3-
42
from threading import Lock
53

64
from sentry_sdk._compat import iteritems
5+
from sentry_sdk._types import TYPE_CHECKING
76
from sentry_sdk.utils import logger
87

9-
from sentry_sdk._types import TYPE_CHECKING
108

119
if TYPE_CHECKING:
1210
from typing import Callable
1311
from typing import Dict
1412
from typing import Iterator
1513
from typing import List
1614
from typing import Set
17-
from typing import Tuple
1815
from typing import Type
1916

2017

2118
_installer_lock = Lock()
2219
_installed_integrations = set() # type: Set[str]
2320

2421

25-
def _generate_default_integrations_iterator(integrations, auto_enabling_integrations):
26-
# type: (Tuple[str, ...], Tuple[str, ...]) -> Callable[[bool], Iterator[Type[Integration]]]
22+
def _generate_default_integrations_iterator(
23+
integrations, # type: List[str]
24+
auto_enabling_integrations, # type: List[str]
25+
):
26+
# type: (...) -> Callable[[bool], Iterator[Type[Integration]]]
2727

2828
def iter_default_integrations(with_auto_enabling_integrations):
2929
# type: (bool) -> Iterator[Type[Integration]]
@@ -51,38 +51,40 @@ def iter_default_integrations(with_auto_enabling_integrations):
5151
return iter_default_integrations
5252

5353

54-
_AUTO_ENABLING_INTEGRATIONS = (
55-
"sentry_sdk.integrations.django.DjangoIntegration",
56-
"sentry_sdk.integrations.flask.FlaskIntegration",
57-
"sentry_sdk.integrations.starlette.StarletteIntegration",
58-
"sentry_sdk.integrations.fastapi.FastApiIntegration",
54+
_DEFAULT_INTEGRATIONS = [
55+
# stdlib/base runtime integrations
56+
"sentry_sdk.integrations.argv.ArgvIntegration",
57+
"sentry_sdk.integrations.atexit.AtexitIntegration",
58+
"sentry_sdk.integrations.dedupe.DedupeIntegration",
59+
"sentry_sdk.integrations.excepthook.ExcepthookIntegration",
60+
"sentry_sdk.integrations.logging.LoggingIntegration",
61+
"sentry_sdk.integrations.modules.ModulesIntegration",
62+
"sentry_sdk.integrations.stdlib.StdlibIntegration",
63+
"sentry_sdk.integrations.threading.ThreadingIntegration",
64+
]
65+
66+
_AUTO_ENABLING_INTEGRATIONS = [
67+
"sentry_sdk.integrations.aiohttp.AioHttpIntegration",
68+
"sentry_sdk.integrations.boto3.Boto3Integration",
5969
"sentry_sdk.integrations.bottle.BottleIntegration",
60-
"sentry_sdk.integrations.falcon.FalconIntegration",
61-
"sentry_sdk.integrations.sanic.SanicIntegration",
6270
"sentry_sdk.integrations.celery.CeleryIntegration",
71+
"sentry_sdk.integrations.django.DjangoIntegration",
72+
"sentry_sdk.integrations.falcon.FalconIntegration",
73+
"sentry_sdk.integrations.fastapi.FastApiIntegration",
74+
"sentry_sdk.integrations.flask.FlaskIntegration",
75+
"sentry_sdk.integrations.httpx.HttpxIntegration",
76+
"sentry_sdk.integrations.pyramid.PyramidIntegration",
77+
"sentry_sdk.integrations.redis.RedisIntegration",
6378
"sentry_sdk.integrations.rq.RqIntegration",
64-
"sentry_sdk.integrations.aiohttp.AioHttpIntegration",
65-
"sentry_sdk.integrations.tornado.TornadoIntegration",
79+
"sentry_sdk.integrations.sanic.SanicIntegration",
6680
"sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration",
67-
"sentry_sdk.integrations.redis.RedisIntegration",
68-
"sentry_sdk.integrations.pyramid.PyramidIntegration",
69-
"sentry_sdk.integrations.boto3.Boto3Integration",
70-
"sentry_sdk.integrations.httpx.HttpxIntegration",
71-
)
81+
"sentry_sdk.integrations.starlette.StarletteIntegration",
82+
"sentry_sdk.integrations.tornado.TornadoIntegration",
83+
]
7284

7385

7486
iter_default_integrations = _generate_default_integrations_iterator(
75-
integrations=(
76-
# stdlib/base runtime integrations
77-
"sentry_sdk.integrations.logging.LoggingIntegration",
78-
"sentry_sdk.integrations.stdlib.StdlibIntegration",
79-
"sentry_sdk.integrations.excepthook.ExcepthookIntegration",
80-
"sentry_sdk.integrations.dedupe.DedupeIntegration",
81-
"sentry_sdk.integrations.atexit.AtexitIntegration",
82-
"sentry_sdk.integrations.modules.ModulesIntegration",
83-
"sentry_sdk.integrations.argv.ArgvIntegration",
84-
"sentry_sdk.integrations.threading.ThreadingIntegration",
85-
),
87+
integrations=_DEFAULT_INTEGRATIONS,
8688
auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS,
8789
)
8890

@@ -93,8 +95,10 @@ def setup_integrations(
9395
integrations, with_defaults=True, with_auto_enabling_integrations=False
9496
):
9597
# type: (List[Integration], bool, bool) -> Dict[str, Integration]
96-
"""Given a list of integration instances this installs them all. When
97-
`with_defaults` is set to `True` then all default integrations are added
98+
"""
99+
Given a list of integration instances, this installs them all.
100+
101+
When `with_defaults` is set to `True` all default integrations are added
98102
unless they were already provided before.
99103
"""
100104
integrations = dict(

sentry_sdk/integrations/opentelemetry/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from sentry_sdk.integrations.opentelemetry.integration import ( # noqa: F401
2+
OpenTelemetryIntegration,
3+
)
4+
15
from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401
26
SentrySpanProcessor,
37
)
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
IMPORTANT: The contents of this file are part of a proof of concept and as such
3+
are experimental and not suitable for production use. They may be changed or
4+
removed at any time without prior notice.
5+
"""
6+
import sys
7+
from importlib import import_module
8+
9+
from sentry_sdk.integrations import DidNotEnable, Integration
10+
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
11+
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
12+
from sentry_sdk.integrations.modules import _get_installed_modules
13+
from sentry_sdk.utils import logger
14+
from sentry_sdk._types import TYPE_CHECKING
15+
16+
try:
17+
from opentelemetry import trace # type: ignore
18+
from opentelemetry.instrumentation.auto_instrumentation._load import ( # type: ignore
19+
_load_distro,
20+
_load_instrumentors,
21+
)
22+
from opentelemetry.propagate import set_global_textmap # type: ignore
23+
from opentelemetry.sdk.trace import TracerProvider # type: ignore
24+
except ImportError:
25+
raise DidNotEnable("opentelemetry not installed")
26+
27+
if TYPE_CHECKING:
28+
from typing import Dict
29+
30+
31+
CLASSES_TO_INSTRUMENT = {
32+
# A mapping of packages to their entry point class that will be instrumented.
33+
# This is used to post-instrument any classes that were imported before OTel
34+
# instrumentation took place.
35+
"fastapi": "fastapi.FastAPI",
36+
"flask": "flask.Flask",
37+
}
38+
39+
40+
class OpenTelemetryIntegration(Integration):
41+
identifier = "opentelemetry"
42+
43+
@staticmethod
44+
def setup_once():
45+
# type: () -> None
46+
logger.warning(
47+
"[OTel] Initializing highly experimental OpenTelemetry support. "
48+
"Use at your own risk."
49+
)
50+
51+
original_classes = _record_unpatched_classes()
52+
53+
try:
54+
distro = _load_distro()
55+
distro.configure()
56+
_load_instrumentors(distro)
57+
except Exception:
58+
logger.exception("[OTel] Failed to auto-initialize OpenTelemetry")
59+
60+
try:
61+
_patch_remaining_classes(original_classes)
62+
except Exception:
63+
logger.exception(
64+
"[OTel] Failed to post-patch instrumented classes. "
65+
"You might have to make sure sentry_sdk.init() is called before importing anything else."
66+
)
67+
68+
_setup_sentry_tracing()
69+
70+
logger.debug("[OTel] Finished setting up OpenTelemetry integration")
71+
72+
73+
def _record_unpatched_classes():
74+
# type: () -> Dict[str, type]
75+
"""
76+
Keep references to classes that are about to be instrumented.
77+
78+
Used to search for unpatched classes after the instrumentation has run so
79+
that they can be patched manually.
80+
"""
81+
installed_packages = _get_installed_modules()
82+
83+
original_classes = {}
84+
85+
for package, orig_path in CLASSES_TO_INSTRUMENT.items():
86+
if package in installed_packages:
87+
try:
88+
original_cls = _import_by_path(orig_path)
89+
except (AttributeError, ImportError):
90+
logger.debug("[OTel] Failed to import %s", orig_path)
91+
continue
92+
93+
original_classes[package] = original_cls
94+
95+
return original_classes
96+
97+
98+
def _patch_remaining_classes(original_classes):
99+
# type: (Dict[str, type]) -> None
100+
"""
101+
Best-effort attempt to patch any uninstrumented classes in sys.modules.
102+
103+
This enables us to not care about the order of imports and sentry_sdk.init()
104+
in user code. If e.g. the Flask class had been imported before sentry_sdk
105+
was init()ed (and therefore before the OTel instrumentation ran), it would
106+
not be instrumented. This function goes over remaining uninstrumented
107+
occurrences of the class in sys.modules and replaces them with the
108+
instrumented class.
109+
110+
Since this is looking for exact matches, it will not work in some scenarios
111+
(e.g. if someone is not using the specific class explicitly, but rather
112+
inheriting from it). In those cases it's still necessary to sentry_sdk.init()
113+
before importing anything that's supposed to be instrumented.
114+
"""
115+
# check which classes have actually been instrumented
116+
instrumented_classes = {}
117+
118+
for package in list(original_classes.keys()):
119+
original_path = CLASSES_TO_INSTRUMENT[package]
120+
121+
try:
122+
cls = _import_by_path(original_path)
123+
except (AttributeError, ImportError):
124+
logger.debug(
125+
"[OTel] Failed to check if class has been instrumented: %s",
126+
original_path,
127+
)
128+
del original_classes[package]
129+
continue
130+
131+
if not cls.__module__.startswith("opentelemetry."):
132+
del original_classes[package]
133+
continue
134+
135+
instrumented_classes[package] = cls
136+
137+
if not instrumented_classes:
138+
return
139+
140+
# replace occurrences of the original unpatched class in sys.modules
141+
for module_name, module in sys.modules.copy().items():
142+
if (
143+
module_name.startswith("sentry_sdk")
144+
or module_name in sys.builtin_module_names
145+
):
146+
continue
147+
148+
for package, original_cls in original_classes.items():
149+
for var_name, var in vars(module).copy().items():
150+
if var == original_cls:
151+
logger.debug(
152+
"[OTel] Additionally patching %s from %s",
153+
original_cls,
154+
module_name,
155+
)
156+
157+
setattr(module, var_name, instrumented_classes[package])
158+
159+
160+
def _import_by_path(path):
161+
# type: (str) -> type
162+
parts = path.rsplit(".", maxsplit=1)
163+
return getattr(import_module(parts[0]), parts[-1])
164+
165+
166+
def _setup_sentry_tracing():
167+
# type: () -> None
168+
provider = TracerProvider()
169+
170+
provider.add_span_processor(SentrySpanProcessor())
171+
172+
trace.set_tracer_provider(provider)
173+
174+
set_global_textmap(SentryPropagator())

sentry_sdk/integrations/opentelemetry/span_processor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def on_end(self, otel_span):
169169
sentry_span.set_context(
170170
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
171171
)
172+
self._update_transaction_with_otel_data(sentry_span, otel_span)
172173

173174
else:
174175
self._update_span_with_otel_data(sentry_span, otel_span)
@@ -306,3 +307,21 @@ def _update_span_with_otel_data(self, sentry_span, otel_span):
306307

307308
sentry_span.op = op
308309
sentry_span.description = description
310+
311+
def _update_transaction_with_otel_data(self, sentry_span, otel_span):
312+
# type: (SentrySpan, OTelSpan) -> None
313+
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
314+
315+
if http_method:
316+
status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
317+
if status_code:
318+
sentry_span.set_http_status(status_code)
319+
320+
op = "http"
321+
322+
if otel_span.kind == SpanKind.SERVER:
323+
op += ".server"
324+
elif otel_span.kind == SpanKind.CLIENT:
325+
op += ".client"
326+
327+
sentry_span.op = op

sentry_sdk/integrations/starlite.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None:
8181
]
8282
)
8383

84-
SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3
84+
SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3 # type: ignore
8585
middleware = kwargs.pop("middleware", None) or []
8686
kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware]
8787
old__init__(self, *args, **kwargs)

0 commit comments

Comments
 (0)