8000 ref: Integration options now live on the client (#101) · etherscan-io/sentry-python@68b8533 · GitHub
[go: up one dir, main page]

Skip to content

Commit 68b8533

Browse files
authored
ref: Integration options now live on the client (getsentry#101)
* ref: Integration options now live on the client * ref: Use an indirection for working with integrations * ref: Refactored integration code again * fix: Fixed some tests * fix: Fixed the celery integration * tests: Added test for integration separation * fix: Restore old `install` for backwards compat * fix: styling * ref: No classmethod for setup_once * fix: Fix missing transaction in celery * fix: Fix sanic crash * feat: Added warning to disabled integrations due to lack of fall through * feat: Emit a warning into the debug log when an integration does not fall through * fix: linters
1 parent 8aa0446 commit 68b8533

21 files changed

+474
-324
lines changed

sentry_sdk/_compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
string_types = (str, text_type)
1313
number_types = (int, long, float) # noqa
1414
int_types = (int, long) # noqa
15+
iteritems = lambda x: x.iteritems()
1516

1617
def implements_str(cls):
1718
cls.__unicode__ = cls.__str__
@@ -34,6 +35,7 @@ def implements_iterator(cls):
3435
string_types = (text_type,)
3536
number_types = (int, float)
3637
int_types = (int,) # noqa
38+
iteritems = lambda x: x.items()
3739

3840
def _identity(x):
3941
return x

sentry_sdk/api.py

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import inspect
22
from contextlib import contextmanager
33

4-
from sentry_sdk.hub import Hub
4+
from sentry_sdk.hub import Hub, init
55
from sentry_sdk.scope import Scope
66
from sentry_sdk.transport import Transport, HttpTransport
7-
from sentry_sdk.client import Client, get_options
8-
from sentry_sdk.integrations import setup_integrations
7+
from sentry_sdk.client import Client
98

109

11-
__all__ = ["Hub", "Scope", "Client", "Transport", "HttpTransport"]
10+
__all__ = ["Hub", "Scope", "Client", "Transport", "HttpTransport", "init"]
11+
12+
13+
_initial_client = None
1214

1315

1416
def public(f):
@@ -24,45 +26,6 @@ def hubmethod(f):
2426
return public(f)
2527

2628

27-
class _InitGuard(object):
28-
def __init__(self, client):
29-
self._client = client
30-
31-
def __enter__(self):
32-
return self
33-
34-
def __exit__(self, exc_type, exc_value, tb):
35-
c = self._client
36-
if c is not None:
37-
c.close()
38-
39-
40-
def _init_on_hub(hub, args, kwargs):
41-
options = get_options(*args, **kwargs)
42-
client = Client(options)
43-
hub.bind_client(client)
44-
setup_integrations(
45-
options["integrations"] or [], with_defaults=options["default_integrations"]
46-
)
47-
return _InitGuard(client)
48-
49-
50-
@public
51-
def init(*args, **kwargs):
52-
"""Initializes the SDK and optionally integrations.
53-
54-
This takes the same arguments as the client constructor.
55-
"""
56-
return _init_on_hub(Hub.main, args, kwargs)
57-
58-
59-
def _init_on_current(*args, **kwargs):
60-
# This function only exists to support unittests. Do not call it as
61-
# initializing integrations on anything but the main hub is not going
62-
# to yield the results you expect.
63-
return _init_on_hub(Hub.current, args, kwargs)
64-
65-
6629
@hubmethod
6730
def capture_event(event, hint=None):
6831
hub = Hub.current

sentry_sdk/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from sentry_sdk.transport import make_transport
1818
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO
19+
from sentry_sdk.integrations import setup_integrations
1920

2021

2122
def get_options(*args, **kwargs):
@@ -60,6 +61,10 @@ def __init__(self, *args, **kwargs):
6061
)
6162
)
6263

64+
self.integrations = setup_integrations(
65+
options["integrations"], with_defaults=options["default_integrations"]
66+
)
67+
6368
@property
6469
def dsn(self):
6570
"""Returns the configured DSN as string."""
@@ -93,7 +98,9 @@ def _prepare_event(self, event, hint, scope):
9398
if event.get(key) is None:
9499
event[key] = self.options[key]
95100
if event.get("sdk") is None:
96-
event["sdk"] = SDK_INFO
101+
sdk_info = dict(SDK_INFO)
102+
sdk_info["integrations"] = sorted(self.integrations.keys())
103+
event["sdk"] = sdk_info
97104

98105
if event.get("platform") is None:
99106
event["platform"] = "python"

sentry_sdk/consts.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,8 @@
3030
}
3131

3232

33-
# Modified by sentry_sdk.integrations
34-
INTEGRATIONS = []
35-
3633
SDK_INFO = {
3734
"name": "sentry.python",
3835
"version": VERSION,
3936
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
40-
"integrations": INTEGRATIONS,
4137
}

sentry_sdk/hub.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import sys
22
import copy
3+
import weakref
34
from datetime import datetime
45
from contextlib import contextmanager
6+
from warnings import warn
57

6-
from sentry_sdk._compat import with_metaclass
8+
from sentry_sdk._compat import with_metaclass, string_types
79
from sentry_sdk.scope import Scope
10+
from sentry_sdk.client import Client
811
from sentry_sdk.utils import (
912
exc_info_from_error,
1013
event_from_exception,
@@ -14,12 +17,7 @@
1417

1518

1619
_local = ContextVar("sentry_current_hub")
17-
18-
19-
def _get_client_options():
20-
hub = Hub.current
21-
if hub and hub.client:
22-
return hub.client.options
20+
_initial_client = None
2321

2422

2523
def _should_send_default_pii():
@@ -29,6 +27,33 @@ def _should_send_default_pii():
2927
return client.options["send_default_pii"]
3028

3129

30+
class _InitGuard(object):
31+
def __init__(self, client):
32+
self._client = client
33+
34+
def __enter__(self):
35+
return self
36+
37+
def __exit__(self, exc_type, exc_value, tb):
38+
c = self._client
39+
if c is not None:
40+
c.close()
41+
42+
43+
def init(*args, **kwargs):
44+
"""Initializes the SDK and optionally integrations.
45+
46+
This takes the same arguments as the client constructor.
47+
"""
48+
global _initial_client
49+
client = Client(*args, **kwargs)
50+
Hub.main.bind_client(client)
51+
rv = _InitGuard(client)
52+
if client is not None:
53+
_initial_client = weakref.ref(client)
54+
return rv
55+
56 F438 +
3257
class HubMeta(type):
3358
@property
3459
def current(self):
@@ -107,6 +132,41 @@ def run(self, callback):
107132
with self:
108133
return callback()
109134

135+
def get_integration(self, name_or_class):
136+
"""Returns the integration for this hub by name or class. If there
137+
is no client bound or the client does not have that integration
138+
then `None` is returned.
139+
140+
If the return value is not `None` the hub is guaranteed to have a
141+
client attached.
142+
"""
143+
if not isinstance(name_or_class, string_types):
144+
name_or_class = name_or_class.identifier
145+
client = self._stack[-1][0]
146+
if client is not None:
147+
rv = client.integrations.get(name_or_class)
148+
if rv is not None:
149+
return rv
150+
151+
initial_client = _initial_client
152+
if initial_client is not None:
153+
initial_client = initial_client()
154+
155+
if (
156+
initial_client is not None
157+
and initial_client is not client
158+
and initial_client.integrations.get(name_or_class) is not None
159+
):
160+
warning = (
161+
"Integration %r attempted to run but it was only "
162+
"enabled on init() but not the client that "
163+
"was bound to the current flow. Earlier versions of "
164+
"the SDK would consider these integrations enabled but "
165+
"this is no longer the case." % (name_or_class,)
166+
)
167+
warn(Warning(warning), stacklevel=3)
168+
logger.warning(warning)
169+
110170
@property
111171
def client(self):
112172
"""Returns the current client on the hub."""

sentry_sdk/integrations/__init__.py

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"""This package"""
22
from threading import Lock
3+
from collections import namedtuple
34

5+
from sentry_sdk._compat import iteritems
46
from sentry_sdk.utils import logger
5-
from sentry_sdk.consts import INTEGRATIONS as _installed_integrations
67

78

89
_installer_lock = Lock()
10+
_installed_integrations = set()
911

1012

11-
def get_default_integrations():
12-
"""Returns an iterator of default integration instances.
13+
def iter_default_integrations():
14+
"""Returns an iterator of default integration classes.
1315
1416
This returns the following default integration:
1517
@@ -25,54 +27,74 @@ def get_default_integrations():
2527
from sentry_sdk.integrations.atexit import AtexitIntegration
2628
from sentry_sdk.integrations.modules import ModulesIntegration
2729

28-
yield LoggingIntegration()
29-
yield StdlibIntegration()
30-
yield ExcepthookIntegration()
31-
yield DedupeIntegration()
32-
yield AtexitIntegration()
33-
yield ModulesIntegration()
30+
yield LoggingIntegration
31+
yield StdlibIntegration
32+
yield ExcepthookIntegration
33+
yield DedupeIntegration
34+
yield AtexitIntegration
35+
yield ModulesIntegration
3436

3537

3638
def setup_integrations(integrations, with_defaults=True):
3739
"""Given a list of integration instances this installs them all. When
3840
`with_defaults` is set to `True` then all default integrations are added
3941
unless they were already provided before.
4042
"""
41-
integrations = list(integrations)
43+
integrations = dict(
44+
(integration.identifier, integration) for integration in integrations or ()
45+
)
46+
4247
if with_defaults:
43-
for instance in get_default_integrations():
44-
if not any(isinstance(x, type(instance)) for x in integrations):
45-
integrations.append(instance)
48+
for integration_cls in iter_default_integrations():
49+
if integration_cls.identifier not in integrations:
50+
instance = integration_cls()
51+
integrations[instance.identifier] = instance
52+
53+
for identifier, integration in iteritems(integrations):
54+
with _installer_lock:
55+
if identifier not in _installed_integrations:
56+
try:
57+
type(integration).setup_once()
58+
except NotImplementedError:
59+
if getattr(integration, "install", None) is not None:
60+
logger.warn(
61+
"Integration %s: The install method is "
62+
"deprecated. Use `setup_once`.",
63+
identifier,
64+
)
65+
integration.install()
66+
else:
67+
raise
68+
_installed_integrations.add(identifier)
4669

47-
for integration in integrations:
48-
integration()
70+
return integrations
71+
72+
73+
IntegrationAttachment = namedtuple(
74+
"IntegrationAttachment", ["integration", "client", "hub"]
75+
)
4976

5077

5178
class Integration(object):
52-
"""Baseclass for all integrations."""
79+
"""Baseclass for all integrations.
5380
54-
identifier = None
55-
"""A unique identifying string for the integration. Integrations must
56-
set this as a class attribute.
81+
To accept options for an integration, implement your own constructor that
82+
saves those options on `self`.
5783
"""
5884

59-
def install(self):
60-
"""An integration must implement all its code here. When the
61-
`setup_integrations` function runs it will invoke this unless the
62-
integration was already activated elsewhere.
85+
install = None
86+
"""Legacy method, do not implement."""
87+
88+
@staticmethod
89+
def setup_once():
6390
"""
64-
raise NotImplementedError()
91+
Initialize the integration.
6592
66-
def __call__(self):
67-
assert self.identifier
68-
with _installer_lock:
69-
if self.identifier in _installed_integrations:
70-
logger.warning(
71-
"%s integration for Sentry is already "
72-
"configured. Will ignore second configuration.",
73-
self.identifier,
74-
)
75-
return
76-
77-
self.install()
78-
_installed_integrations.append(self.identifier)
93+
This function is only called once, ever. Configuration is not available
94+
at this point, so the only thing to do here is to hook into exception
95+
handlers, and perhaps do monkeypatches.
96+
97+
Inside those hooks `Integration.current` can be used to access the
98+
instance again.
99+
"""
100+
raise NotImplementedError()

sentry_sdk/integrations/_wsgi.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import sys
33

4-
from sentry_sdk.hub import Hub, _should_send_default_pii, _get_client_options
4+
from sentry_sdk.hub import Hub, _should_send_default_pii
55
from sentry_sdk.utils import (
66
AnnotatedValue,
77
capture_internal_exceptions,
@@ -44,8 +44,8 @@ def __init__(self, request):
4444
self.request = request
4545

4646
def extract_into_event(self, event):
47-
client_options = _get_client_options()
48-
if client_options is None:
47+
client = Hub.current.client
48+
if client is None:
4949
return
5050

5151
content_length = self.content_length()
@@ -55,7 +55,7 @@ def extract_into_event(self, event):
5555
if _should_send_default_pii():
5656
request_info["cookies"] = dict(self.cookies())
5757

58-
bodies = client_options["request_bodies"]
58+
bodies = client.options["request_bodies"]
5959
if (
6060
bodies == "never"
6161
or (bodies == "small" and content_length > 10 ** 3)

0 commit comments

Comments
 (0)
0