From e68161c8ed29e47809addc6a249fb5cab5733c68 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 22 Jun 2023 10:23:01 +0200 Subject: [PATCH] Load tracing information from environment (#2176) It should be able to continue a trace with trace information that was given to the python process over environment variables. See this RFC for the spec: https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md --------- Co-authored-by: Ivana Kellyerova --- Makefile | 2 +- sentry_sdk/_compat.py | 5 ++- sentry_sdk/consts.py | 8 ++++ sentry_sdk/scope.py | 40 +++++++++++++++++- tests/test_scope.py | 95 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a4d07279da..2011b1b63e 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ lint: .venv apidocs: .venv @$(VENV_PATH)/bin/pip install --editable . @$(VENV_PATH)/bin/pip install -U -r ./docs-requirements.txt - @$(VENV_PATH)/bin/sphinx-build -W -b html docs/ docs/_build + @$(VENV_PATH)/bin/sphinx-build -vv -W -b html docs/ docs/_build .PHONY: apidocs apidocs-hotfix: apidocs diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index 4fa489569b..0e56608d13 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -82,7 +82,10 @@ def check_thread_support(): if "threads" in opt: return - if str(opt.get("enable-threads", "0")).lower() in ("false", "off", "no", "0"): + # put here because of circular import + from sentry_sdk.consts import FALSE_VALUES + + if str(opt.get("enable-threads", "0")).lower() in FALSE_VALUES: from warnings import warn warn( diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index ebe5719471..0f276e05df 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -45,6 +45,14 @@ MATCH_ALL = r".*" +FALSE_VALUES = [ + "false", + "no", + "off", + "n", + "0", +] + class INSTRUMENTER: SENTRY = "sentry" diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c7ff150064..3ad61d31d5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,6 +1,7 @@ from copy import copy from collections import deque from itertools import chain +import os import uuid from sentry_sdk.attachments import Attachment @@ -19,6 +20,8 @@ from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import logger, capture_internal_exceptions +from sentry_sdk.consts import FALSE_VALUES + if TYPE_CHECKING: from typing import Any @@ -122,7 +125,36 @@ def __init__(self): self._propagation_context = None # type: Optional[Dict[str, Any]] self.clear() - self.generate_propagation_context() + + incoming_trace_information = self._load_trace_data_from_env() + self.generate_propagation_context(incoming_data=incoming_trace_information) + + def _load_trace_data_from_env(self): + # type: () -> Optional[Dict[str, str]] + """ + Load Sentry trace id and baggage from environment variables. + Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". + """ + incoming_trace_information = None + + sentry_use_environment = ( + os.environ.get("SENTRY_USE_ENVIRONMENT") or "" + ).lower() + use_environment = sentry_use_environment not in FALSE_VALUES + if use_environment: + incoming_trace_information = {} + + if os.environ.get("SENTRY_TRACE"): + incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( + os.environ.get("SENTRY_TRACE") or "" + ) + + if os.environ.get("SENTRY_BAGGAGE"): + incoming_trace_information[BAGGAGE_HEADER_NAME] = ( + os.environ.get("SENTRY_BAGGAGE") or "" + ) + + return incoming_trace_information or None def _extract_propagation_context(self, data): # type: (Dict[str, Any]) -> Optional[Dict[str, Any]] @@ -141,6 +173,12 @@ def _extract_propagation_context(self, data): if sentrytrace_data is not None: context.update(sentrytrace_data) + only_baggage_no_sentry_trace = ( + "dynamic_sampling_context" in context and "trace_id" not in context + ) + if only_baggage_no_sentry_trace: + context.update(self._create_new_propagation_context()) + if context: if not context.get("span_id"): context["span_id"] = uuid.uuid4().hex[16:] diff --git a/tests/test_scope.py b/tests/test_scope.py index d90a89f490..8bdd46e02f 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1,7 +1,14 @@ import copy +import os +import pytest from sentry_sdk import capture_exception from sentry_sdk.scope import Scope +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + def test_copying(): s1 = Scope() @@ -62,3 +69,91 @@ def test_common_args(): assert s2._extras == {"k": "v", "foo": "bar"} assert s2._tags == {"a": "b", "x": "y"} assert s2._contexts == {"os": {"name": "Blafasel"}, "device": {"a": "b"}} + + +BAGGAGE_VALUE = ( + "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, " + "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, " + "sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;" +) + +SENTRY_TRACE_VALUE = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1" + + +@pytest.mark.parametrize( + "env,excepted_value", + [ + ( + { + "SENTRY_TRACE": SENTRY_TRACE_VALUE, + }, + { + "sentry-trace": SENTRY_TRACE_VALUE, + }, + ), + ( + { + "SENTRY_BAGGAGE": BAGGAGE_VALUE, + }, + { + "baggage": BAGGAGE_VALUE, + }, + ), + ( + { + "SENTRY_TRACE": SENTRY_TRACE_VALUE, + "SENTRY_BAGGAGE": BAGGAGE_VALUE, + }, + { + "sentry-trace": SENTRY_TRACE_VALUE, + "baggage": BAGGAGE_VALUE, + }, + ), + ( + { + "SENTRY_USE_ENVIRONMENT": "", + "SENTRY_TRACE": SENTRY_TRACE_VALUE, + "SENTRY_BAGGAGE": BAGGAGE_VALUE, + }, + { + "sentry-trace": SENTRY_TRACE_VALUE, + "baggage": BAGGAGE_VALUE, + }, + ), + ( + { + "SENTRY_USE_ENVIRONMENT": "True", + "SENTRY_TRACE": SENTRY_TRACE_VALUE, + "SENTRY_BAGGAGE": BAGGAGE_VALUE, + }, + { + "sentry-trace": SENTRY_TRACE_VALUE, + "baggage": BAGGAGE_VALUE, + }, + ), + ( + { + "SENTRY_USE_ENVIRONMENT": "no", + "SENTRY_TRACE": SENTRY_TRACE_VALUE, + "SENTRY_BAGGAGE": BAGGAGE_VALUE, + }, + None, + ), + ( + { + "SENTRY_USE_ENVIRONMENT": "True", + "MY_OTHER_VALUE": "asdf", + "SENTRY_RELEASE": "1.0.0", + }, + None, + ), + ], +) +def test_load_trace_data_from_env(env, excepted_value): + new_env = os.environ.copy() + new_env.update(env) + + with mock.patch.dict(os.environ, new_env): + s = Scope() + incoming_trace_data = s._load_trace_data_from_env() + assert incoming_trace_data == excepted_value