From 815a959ec0ede7aa6c60c0018ece7b54340a31fd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Apr 2019 11:11:26 +0200 Subject: [PATCH 01/10] feat(tracing): Initial tracing experiments --- sentry_sdk/_compat.py | 6 +++ sentry_sdk/consts.py | 2 + sentry_sdk/hub.py | 26 +++++++++- sentry_sdk/integrations/flask.py | 6 ++- sentry_sdk/integrations/stdlib.py | 8 ++- sentry_sdk/scope.py | 15 ++++++ sentry_sdk/tracing.py | 86 +++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 sentry_sdk/tracing.py diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index 871995e8c1..023724f2cc 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -83,3 +83,9 @@ def check_thread_support(): '(Enable the "enable-threads" flag).' ) ) + + +def to_text(value): + if isinstance(value, text_type): + return value + return value.decode('utf-8') diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3260fa3b35..9e62cb98a6 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -37,6 +37,7 @@ "debug": bool, "attach_stacktrace": bool, "ca_certs": Optional[str], + "propagate_traces": List[str], }, total=False, ) @@ -69,6 +70,7 @@ "debug": False, "attach_stacktrace": False, "ca_certs": None, + "propagate_traces": [], } diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index d81bf0171b..51ad14ff02 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from warnings import warn -from sentry_sdk._compat import with_metaclass +from sentry_sdk._compat import with_metaclass, urlparse from sentry_sdk.scope import Scope from sentry_sdk.client import Client from sentry_sdk.utils import ( @@ -415,6 +415,30 @@ def flush(self, timeout=None, callback=None): if client is not None: return client.flush(timeout=timeout, callback=callback) + def get_traceparent_for_propagation(self, url=None, host=None): + """Given a reference url or host this returns a traceparent header + if it should be propagated. + """ + client, scope = self._stack[-1] + if scope._span is None: + return + + propagate_traces = client and client.options["propagate_traces"] or [] + + if url is not None: + scheme, host = urlparse.urlsplit(url)[:2] + else: + scheme = None + + for target in propagate_traces: + target_scheme, target_host = urlparse.urlsplit(target)[:2] + if (scheme is None or scheme == target_scheme) and target_host == host: + break + else: + return None + + return scope._span.to_traceparent() + GLOBAL_HUB = Hub() _local.set(GLOBAL_HUB) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 47bdb5d6a8..8013aa3e31 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -4,6 +4,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.tracing import SpanContext from sentry_sdk.integrations import Integration from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -96,9 +97,12 @@ def _request_started(sender, **kwargs): if integration is None: return - weak_request = weakref.ref(_request_ctx_stack.top.request) app = _app_ctx_stack.top.app with hub.configure_scope() as scope: + request = _request_ctx_stack.top.request + scope.set_span_context(SpanContext.continue_from_headers(request.headers)) + + weak_request = weakref.ref(request) scope.add_event_processor( _make_request_event_processor( # type: ignore app, weak_request, integration diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 0d0b20cbf8..00d2fddb4c 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -1,5 +1,6 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration +from sentry_sdk._compat import to_text try: @@ -24,7 +25,8 @@ def install_httplib(): def putrequest(self, method, url, *args, **kwargs): rv = real_putrequest(self, method, url, *args, **kwargs) - if Hub.current.get_integration(StdlibIntegration) is None: + hub = Hub.current + if hub.get_integration(StdlibIntegration) is None: return rv self._sentrysdk_data_dict = data = {} @@ -42,6 +44,10 @@ def putrequest(self, method, url, *args, **kwargs): url, ) + traceparent = hub.get_traceparent_for_propagation(url=real_url) + if traceparent is not None: + self.putheader('traceparent', traceparent) + data["url"] = real_url data["method"] = method return rv diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 1762046058..5d3319b37a 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -4,6 +4,7 @@ from itertools import chain from sentry_sdk.utils import logger, capture_internal_exceptions, object_to_json +from sentry_sdk.tracing import SpanContext if False: from typing import Any @@ -59,6 +60,7 @@ class Scope(object): "_event_processors", "_error_processors", "_should_capture", + "_span", ) def __init__(self): @@ -88,6 +90,10 @@ def user(self, value): """When set a specific user is bound to the scope.""" self._user = value + def set_span_context(self, span_context): + """Sets the span context.""" + self._span = span_context + def set_tag(self, key, value): """Sets a tag for a key to a specific value.""" self._tags[key] = value @@ -127,6 +133,8 @@ def clear(self): self.clear_breadcrumbs() self._should_capture = True + self._span = None + def clear_breadcrumbs(self): # type: () -> None """Clears breadcrumb buffer.""" @@ -193,6 +201,12 @@ def _drop(event, cause, ty): if self._contexts: event.setdefault("contexts", {}).update(self._contexts) + if self._span is not None: + event.setdefault("contexts", {})['trace'] = { + 'trace_id': self._span.trace_id, + 'span_id': self._span.span_id, + } + exc_info = hint.get("exc_info") if hint is not None else None if exc_info is not None: for processor in self._error_processors: @@ -230,6 +244,7 @@ def __copy__(self): rv._error_processors = list(self._error_processors) rv._should_capture = self._should_capture + rv._span = self._span return rv diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py new file mode 100644 index 0000000000..19c87976b1 --- /dev/null +++ b/sentry_sdk/tracing.py @@ -0,0 +1,86 @@ +import re +import uuid + +_traceparent_header_format_re = re.compile( + '^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})' + \ + '(-.*)?[ \t]*$') + + +class SpanContext(object): + + def __init__(self, trace_id, span_id, recorded=False, parent=None): + self.trace_id = trace_id + self.span_id = span_id + self.recorded = recorded + self.parent = None + + def __repr__(self): + return '%s(trace_id=%r, span_id=%r, recorded=%r)' % ( + self.__class__.__name__, + self.trace_id, + self.span_id, + self.recorded, + ) + + @classmethod + def start_trace(cls, recorded=False): + trace_id = uuid.uuid4() + return cls( + trace_id=uuid.uuid4().hex, + span_id=uuid.uuid4().hex[16:], + recorded=recorded, + ) + + def new_span(self): + if self.trace_id is None: + return SpanContext.start_trace() + return SpanContext( + trace_id=self.trace_id, + span_id=uuid.uuid4().hex[16:], + parent=self, + recorded=self.recorded, + ) + + @classmethod + def continue_from_headers(cls, headers): + parent = cls.from_traceparent(headers.get('traceparent')) + if parent is None: + return cls.start_trace() + return parent.new_span() + + @classmethod + def from_traceparent(cls, traceparent): + if not traceparent: + return None + + match = _traceparent_header_format_re.match(traceparent) + if match is None: + return None + + version, trace_id, span_id, trace_options, extra = match.groups() + + if int(trace_id, 16) == 0 or int(span_id, 16) == 0: + return None + + version = int(version, 16) + if version == 0: + if extra: + return None + elif version == 255: + return None + + options = int(trace_options, 16) + + return cls( + trace_id=trace_id, + span_id=span_id, + recorded=options & 1 != 0 + ) + + def to_traceparent(self): + return '%02x-%s-%s-%02x' % ( + 0, + self.trace_id, + self.span_id, + self.recorded and 1 or 0, + ) From c88ee110ec3341cb23e4df96752e9c2be91f8638 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Apr 2019 11:44:28 +0200 Subject: [PATCH 02/10] ref: Hide traceparent header in public api --- sentry_sdk/hub.py | 19 ++++++++----------- sentry_sdk/integrations/stdlib.py | 5 ++--- sentry_sdk/tracing.py | 4 +++- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 51ad14ff02..309dfb8dae 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -415,29 +415,26 @@ def flush(self, timeout=None, callback=None): if client is not None: return client.flush(timeout=timeout, callback=callback) - def get_traceparent_for_propagation(self, url=None, host=None): - """Given a reference url or host this returns a traceparent header - if it should be propagated. + def iter_trace_propagation_headers(self, url): + """Given a reference url or host returns an iterator of all trace + propagation headers that should be added to the request. If no + propagation is enabled for this URL the iterator will be empty. """ client, scope = self._stack[-1] if scope._span is None: return propagate_traces = client and client.options["propagate_traces"] or [] - - if url is not None: - scheme, host = urlparse.urlsplit(url)[:2] - else: - scheme = None - + scheme, host = urlparse.urlsplit(url)[:2] for target in propagate_traces: target_scheme, target_host = urlparse.urlsplit(target)[:2] if (scheme is None or scheme == target_scheme) and target_host == host: break else: - return None + return - return scope._span.to_traceparent() + for item in scope._span.iter_headers(): + yield item GLOBAL_HUB = Hub() diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 00d2fddb4c..feb68c4b39 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -44,9 +44,8 @@ def putrequest(self, method, url, *args, **kwargs): url, ) - traceparent = hub.get_traceparent_for_propagation(url=real_url) - if traceparent is not None: - self.putheader('traceparent', traceparent) + for key, value in hub.iter_trace_propagation_headers(real_url): + self.putheader(key, value) data["url"] = real_url data["method"] = method diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 19c87976b1..3de72e0f13 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -24,7 +24,6 @@ def __repr__(self): @classmethod def start_trace(cls, recorded=False): - trace_id = uuid.uuid4() return cls( trace_id=uuid.uuid4().hex, span_id=uuid.uuid4().hex[16:], @@ -48,6 +47,9 @@ def continue_from_headers(cls, headers): return cls.start_trace() return parent.new_span() + def iter_headers(self): + yield 'traceparent', self.to_traceparent() + @classmethod def from_traceparent(cls, traceparent): if not traceparent: From 45bf1679e3f648f611c7ef8edc584eb5dd76f462 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Apr 2019 14:02:57 +0200 Subject: [PATCH 03/10] ref: Moved tracing code to wsgi integration --- sentry_sdk/_compat.py | 6 ------ sentry_sdk/integrations/flask.py | 3 --- sentry_sdk/integrations/stdlib.py | 1 - sentry_sdk/integrations/wsgi.py | 2 ++ sentry_sdk/tracing.py | 13 +++++++++++++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index 023724f2cc..871995e8c1 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -83,9 +83,3 @@ def check_thread_support(): '(Enable the "enable-threads" flag).' ) ) - - -def to_text(value): - if isinstance(value, text_type): - return value - return value.decode('utf-8') diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 8013aa3e31..437e9fed0b 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -4,7 +4,6 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk.tracing import SpanContext from sentry_sdk.integrations import Integration from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -100,8 +99,6 @@ def _request_started(sender, **kwargs): app = _app_ctx_stack.top.app with hub.configure_scope() as scope: request = _request_ctx_stack.top.request - scope.set_span_context(SpanContext.continue_from_headers(request.headers)) - weak_request = weakref.ref(request) scope.add_event_processor( _make_request_event_processor( # type: ignore diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index feb68c4b39..8d3d2f0d8c 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -1,6 +1,5 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration -from sentry_sdk._compat import to_text try: diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 05587c60d2..a9f2c615eb 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -3,6 +3,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk._compat import PY2, reraise +from sentry_sdk.tracing import SpanContext from sentry_sdk.integrations._wsgi_common import _filter_headers if False: @@ -81,6 +82,7 @@ def __call__(self, environ, start_response): with hub.configure_scope() as scope: scope.clear_breadcrumbs() scope._name = "wsgi" + scope.set_span_context(SpanContext.continue_from_environ(environ)) scope.add_event_processor(_make_wsgi_event_processor(environ)) try: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3de72e0f13..3fa9d82414 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -6,6 +6,15 @@ '(-.*)?[ \t]*$') +class _EnvironHeaders(object): + + def __init__(self, environ): + self.environ = environ + + def get(self, key): + return self.environ.get('HTTP_' + key.replace('-', '_').upper()) + + class SpanContext(object): def __init__(self, trace_id, span_id, recorded=False, parent=None): @@ -40,6 +49,10 @@ def new_span(self): recorded=self.recorded, ) + @classmethod + def continue_from_environ(cls, environ): + return cls.continue_from_headers(_EnvironHeaders(environ)) + @classmethod def continue_from_headers(cls, headers): parent = cls.from_traceparent(headers.get('traceparent')) From 8cd5eb614c7de3b4dd225a8c6f54b71ff16d4cb7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Apr 2019 21:35:22 +0200 Subject: [PATCH 04/10] ref: lint --- sentry_sdk/scope.py | 7 +++---- sentry_sdk/tracing.py | 26 +++++++++----------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5d3319b37a..ce4cb2c501 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -4,7 +4,6 @@ from itertools import chain from sentry_sdk.utils import logger, capture_internal_exceptions, object_to_json -from sentry_sdk.tracing import SpanContext if False: from typing import Any @@ -202,9 +201,9 @@ def _drop(event, cause, ty): event.setdefault("contexts", {}).update(self._contexts) if self._span is not None: - event.setdefault("contexts", {})['trace'] = { - 'trace_id': self._span.trace_id, - 'span_id': self._span.span_id, + event.setdefault("contexts", {})["trace"] = { + "trace_id": self._span.trace_id, + "span_id": self._span.span_id, } exc_info = hint.get("exc_info") if hint is not None else None diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3fa9d82414..2d81382839 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -2,21 +2,19 @@ import uuid _traceparent_header_format_re = re.compile( - '^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})' + \ - '(-.*)?[ \t]*$') + "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" "(-.*)?[ \t]*$" +) class _EnvironHeaders(object): - def __init__(self, environ): self.environ = environ def get(self, key): - return self.environ.get('HTTP_' + key.replace('-', '_').upper()) + return self.environ.get("HTTP_" + key.replace("-", "_").upper()) class SpanContext(object): - def __init__(self, trace_id, span_id, recorded=False, parent=None): self.trace_id = trace_id self.span_id = span_id @@ -24,7 +22,7 @@ def __init__(self, trace_id, span_id, recorded=False, parent=None): self.parent = None def __repr__(self): - return '%s(trace_id=%r, span_id=%r, recorded=%r)' % ( + return "%s(trace_id=%r, span_id=%r, recorded=%r)" % ( self.__class__.__name__, self.trace_id, self.span_id, @@ -34,9 +32,7 @@ def __repr__(self): @classmethod def start_trace(cls, recorded=False): return cls( - trace_id=uuid.uuid4().hex, - span_id=uuid.uuid4().hex[16:], - recorded=recorded, + trace_id=uuid.uuid4().hex, span_id=uuid.uuid4().hex[16:], recorded=recorded ) def new_span(self): @@ -55,13 +51,13 @@ def continue_from_environ(cls, environ): @classmethod def continue_from_headers(cls, headers): - parent = cls.from_traceparent(headers.get('traceparent')) + parent = cls.from_traceparent(headers.get("traceparent")) if parent is None: return cls.start_trace() return parent.new_span() def iter_headers(self): - yield 'traceparent', self.to_traceparent() + yield "traceparent", self.to_traceparent() @classmethod def from_traceparent(cls, traceparent): @@ -86,14 +82,10 @@ def from_traceparent(cls, traceparent): options = int(trace_options, 16) - return cls( - trace_id=trace_id, - span_id=span_id, - recorded=options & 1 != 0 - ) + return cls(trace_id=trace_id, span_id=span_id, recorded=options & 1 != 0) def to_traceparent(self): - return '%02x-%s-%s-%02x' % ( + return "%02x-%s-%s-%02x" % ( 0, self.trace_id, self.span_id, From 82cf7733dae10eb4c1902c26dab21415ff34602b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 17 Apr 2019 00:15:58 +0200 Subject: [PATCH 05/10] feat: Added basic celery propagation --- sentry_sdk/hub.py | 22 ++++++++++++-------- sentry_sdk/integrations/celery.py | 26 ++++++++++++++++++++++++ tests/integrations/celery/test_celery.py | 9 +++++++- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 309dfb8dae..dac3193550 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -415,23 +415,27 @@ def flush(self, timeout=None, callback=None): if client is not None: return client.flush(timeout=timeout, callback=callback) - def iter_trace_propagation_headers(self, url): + def iter_trace_propagation_headers(self, url=None): """Given a reference url or host returns an iterator of all trace propagation headers that should be added to the request. If no propagation is enabled for this URL the iterator will be empty. + + If the URL is set to `None` then the `propagate_traces` flag is not + checked. """ client, scope = self._stack[-1] if scope._span is None: return - propagate_traces = client and client.options["propagate_traces"] or [] - scheme, host = urlparse.urlsplit(url)[:2] - for target in propagate_traces: - target_scheme, target_host = urlparse.urlsplit(target)[:2] - if (scheme is None or scheme == target_scheme) and target_host == host: - break - else: - return + if url is not None: + propagate_traces = client and client.options["propagate_traces"] or [] + scheme, host = urlparse.urlsplit(url)[:2] + for target in propagate_traces: + target_scheme, target_host = urlparse.urlsplit(target)[:2] + if (scheme is None or scheme == target_scheme) and target_host == host: + break + else: + return for item in scope._span.iter_headers(): yield item diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 5b7646d429..8967ce5107 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -6,6 +6,7 @@ from sentry_sdk.hub import Hub from sentry_sdk.utils import capture_internal_exceptions, event_from_exception +from sentry_sdk.tracing import SpanContext from sentry_sdk._compat import reraise from sentry_sdk.integrations import Integration from sentry_sdk.integrations.logging import ignore_logger @@ -25,6 +26,7 @@ def sentry_build_tracer(name, task, *args, **kwargs): # short-circuits to task.run if it thinks it's safe. task.__call__ = _wrap_task_call(task, task.__call__) task.run = _wrap_task_call(task, task.run) + task.apply_async = _wrap_apply_async(task, task.apply_async) return _wrap_tracer(task, old_build_tracer(name, task, *args, **kwargs)) trace.build_tracer = sentry_build_tracer @@ -37,6 +39,21 @@ def sentry_build_tracer(name, task, *args, **kwargs): ignore_logger("celery.worker.job") +def _wrap_apply_async(task, f): + def apply_async(self, *args, **kwargs): + hub = Hub.current + headers = None + for key, value in hub.iter_trace_propagation_headers(): + if headers is None: + headers = dict(kwargs.get("headers") or {}) + headers[key] = value + if headers is not None: + kwargs["headers"] = headers + return f(self, *args, **kwargs) + + return apply_async + + def _wrap_tracer(task, f): # Need to wrap tracer for pushing the scope before prerun is sent, and # popping it after postrun is sent. @@ -52,6 +69,7 @@ def _inner(*args, **kwargs): with hub.push_scope() as scope: scope._name = "celery" scope.clear_breadcrumbs() + _continue_trace(args[3]["headers"], scope) scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) return f(*args, **kwargs) @@ -59,6 +77,14 @@ def _inner(*args, **kwargs): return _inner +def _continue_trace(headers, scope): + if headers: + span_context = SpanContext.continue_from_headers(headers) + else: + span_context = SpanContext.start_trace() + scope.set_span_context(span_context) + + def _wrap_task_call(task, f): # Need to wrap task call because the exception is caught before we get to # see it. Also celery's reported stacktrace is untrustworthy. diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index dc5e1f3e91..888a1bf10f 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -4,8 +4,9 @@ pytest.importorskip("celery") -from sentry_sdk import Hub +from sentry_sdk import Hub, configure_scope from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.tracing import SpanContext from celery import Celery, VERSION from celery.bin import worker @@ -47,9 +48,15 @@ def dummy_task(x, y): foo = 42 # noqa return x / y + span_context = SpanContext.start_trace() + with configure_scope() as scope: + scope.set_span_context(span_context) dummy_task.delay(1, 2) dummy_task.delay(1, 0) + event, = events + assert event["contexts"]["trace"]["trace_id"] == span_context.trace_id + assert event["contexts"]["trace"]["span_id"] != span_context.span_id assert event["transaction"] == "dummy_task" assert event["extra"]["celery-job"] == { "args": [1, 0], From 6936d0367206eca1f646f72d456758eaae66e9a3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 17 Apr 2019 00:24:11 +0200 Subject: [PATCH 06/10] feat: Added a way to disable trace propagation for celery --- sentry_sdk/integrations/celery.py | 19 ++++++++++++------- tests/integrations/celery/test_celery.py | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 8967ce5107..acfa3c5a60 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -15,6 +15,9 @@ class CeleryIntegration(Integration): identifier = "celery" + def __init__(self, propagate_traces=True): + self.propagate_traces = propagate_traces + @staticmethod def setup_once(): import celery.app.trace as trace # type: ignore @@ -42,13 +45,15 @@ def sentry_build_tracer(name, task, *args, **kwargs): def _wrap_apply_async(task, f): def apply_async(self, *args, **kwargs): hub = Hub.current - headers = None - for key, value in hub.iter_trace_propagation_headers(): - if headers is None: - headers = dict(kwargs.get("headers") or {}) - headers[key] = value - if headers is not None: - kwargs["headers"] = headers + integration = hub.get_integration(CeleryIntegration) + if integration is not None and integration.propagate_traces: + headers = None + for key, value in hub.iter_trace_propagation_headers(): + if headers is None: + headers = dict(kwargs.get("headers") or {}) + headers[key] = value + if headers is not None: + kwargs["headers"] = headers return f(self, *args, **kwargs) return apply_async diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 888a1bf10f..758bb04783 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -23,8 +23,8 @@ def inner(signal, f): @pytest.fixture def init_celery(sentry_init): - def inner(): - sentry_init(integrations=[CeleryIntegration()]) + def inner(propagate_traces=True): + sentry_init(integrations=[CeleryIntegration(propagate_traces=propagate_traces)]) celery = Celery(__name__) if VERSION < (4,): celery.conf.CELERY_ALWAYS_EAGER = True @@ -70,6 +70,26 @@ def dummy_task(x, y): assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42" +def test_simple_no_propagation(capture_events, init_celery): + celery = init_celery(propagate_traces=False) + events = capture_events() + + @celery.task(name="dummy_task") + def dummy_task(): + 1 / 0 + + span_context = SpanContext.start_trace() + with configure_scope() as scope: + scope.set_span_context(span_context) + dummy_task.delay() + + event, = events + assert event["contexts"]["trace"]["trace_id"] != span_context.trace_id + assert event["transaction"] == "dummy_task" + exception, = event["exception"]["values"] + assert exception["type"] == "ZeroDivisionError" + + def test_ignore_expected(capture_events, celery): events = capture_events() From 94438ec7df4204a2b6186b20efe89a043a11a0e7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 17 Apr 2019 10:51:17 +0200 Subject: [PATCH 07/10] fix: Optional headers --- sentry_sdk/integrations/celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index acfa3c5a60..75b0c442af 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -74,7 +74,7 @@ def _inner(*args, **kwargs): with hub.push_scope() as scope: scope._name = "celery" scope.clear_breadcrumbs() - _continue_trace(args[3]["headers"], scope) + _continue_trace(args[3].get("headers") or {}, scope) scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) return f(*args, **kwargs) From e818437d75b1ffa31b6e45674345fd485a334590 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 17 Apr 2019 11:04:44 +0200 Subject: [PATCH 08/10] ref: Always populate traces if enabled --- sentry_sdk/consts.py | 4 ++-- sentry_sdk/hub.py | 21 ++++----------------- sentry_sdk/integrations/stdlib.py | 2 +- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 9e62cb98a6..cd8e576a26 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -37,7 +37,7 @@ "debug": bool, "attach_stacktrace": bool, "ca_certs": Optional[str], - "propagate_traces": List[str], + "propagate_traces": bool, }, total=False, ) @@ -70,7 +70,7 @@ "debug": False, "attach_stacktrace": False, "ca_certs": None, - "propagate_traces": [], + "propagate_traces": True, } diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index dac3193550..23a90f783c 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -415,27 +415,14 @@ def flush(self, timeout=None, callback=None): if client is not None: return client.flush(timeout=timeout, callback=callback) - def iter_trace_propagation_headers(self, url=None): - """Given a reference url or host returns an iterator of all trace - propagation headers that should be added to the request. If no - propagation is enabled for this URL the iterator will be empty. - - If the URL is set to `None` then the `propagate_traces` flag is not - checked. - """ + def iter_trace_propagation_headers(self): client, scope = self._stack[-1] if scope._span is None: return - if url is not None: - propagate_traces = client and client.options["propagate_traces"] or [] - scheme, host = urlparse.urlsplit(url)[:2] - for target in propagate_traces: - target_scheme, target_host = urlparse.urlsplit(target)[:2] - if (scheme is None or scheme == target_scheme) and target_host == host: - break - else: - return + propagate_traces = client and client.options["propagate_traces"] + if not propagate_traces: + return for item in scope._span.iter_headers(): yield item diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 8d3d2f0d8c..8b16c73ce4 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -43,7 +43,7 @@ def putrequest(self, method, url, *args, **kwargs): url, ) - for key, value in hub.iter_trace_propagation_headers(real_url): + for key, value in hub.iter_trace_propagation_headers(): self.putheader(key, value) data["url"] = real_url From e2d406bb5b619e890754c5c5170a53ad626350a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 23 Apr 2019 10:08:33 +0200 Subject: [PATCH 09/10] ref: traceparent -> sentry-trace --- sentry_sdk/tracing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 2d81382839..37c1ee356d 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -51,13 +51,13 @@ def continue_from_environ(cls, environ): @classmethod def continue_from_headers(cls, headers): - parent = cls.from_traceparent(headers.get("traceparent")) + parent = cls.from_traceparent(headers.get("sentry-trace")) if parent is None: return cls.start_trace() return parent.new_span() def iter_headers(self): - yield "traceparent", self.to_traceparent() + yield "sentry-trace", self.to_traceparent() @classmethod def from_traceparent(cls, traceparent): From 2cded9b0b4357733b2fa10055560a72050b45883 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 29 Apr 2019 14:04:27 +0200 Subject: [PATCH 10/10] fix: Linters --- sentry_sdk/hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 23a90f783c..cfaa97349c 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from warnings import warn -from sentry_sdk._compat import with_metaclass, urlparse +from sentry_sdk._compat import with_metaclass from sentry_sdk.scope import Scope from sentry_sdk.client import Client from sentry_sdk.utils import (