diff --git a/.riot/requirements/10e65d1.txt b/.riot/requirements/10e65d1.txt new file mode 100644 index 00000000000..f4eb9d21b0b --- /dev/null +++ b/.riot/requirements/10e65d1.txt @@ -0,0 +1,27 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/10e65d1.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.10 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +exceptiongroup==1.2.2 +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-mock==3.14.0 +pyyaml==6.0.2 +requests==2.32.3 +sortedcontainers==2.4.0 +tomli==2.2.1 +urllib3==2.3.0 diff --git a/.riot/requirements/1261872.txt b/.riot/requirements/1261872.txt new file mode 100644 index 00000000000..b474929a916 --- /dev/null +++ b/.riot/requirements/1261872.txt @@ -0,0 +1,24 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/1261872.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.10 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-mock==3.14.0 +requests==2.32.3 +sortedcontainers==2.4.0 +urllib3==2.3.0 diff --git a/.riot/requirements/14d1688.txt b/.riot/requirements/14d1688.txt new file mode 100644 index 00000000000..7956336155d --- /dev/null +++ b/.riot/requirements/14d1688.txt @@ -0,0 +1,27 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/14d1688.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.1 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +exceptiongroup==1.2.2 +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==5.0.0 +pytest-mock==3.14.0 +pyyaml==6.0.2 +requests==2.32.3 +sortedcontainers==2.4.0 +tomli==2.2.1 +urllib3==2.2.3 diff --git a/.riot/requirements/668f2f5.txt b/.riot/requirements/668f2f5.txt new file mode 100644 index 00000000000..644799441d7 --- /dev/null +++ b/.riot/requirements/668f2f5.txt @@ -0,0 +1,27 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/668f2f5.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.10 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +exceptiongroup==1.2.2 +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-mock==3.14.0 +pyyaml==6.0.2 +requests==2.32.3 +sortedcontainers==2.4.0 +tomli==2.2.1 +urllib3==2.3.0 diff --git a/.riot/requirements/d5f777e.txt b/.riot/requirements/d5f777e.txt new file mode 100644 index 00000000000..661f34852c0 --- /dev/null +++ b/.riot/requirements/d5f777e.txt @@ -0,0 +1,25 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/d5f777e.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.10 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-mock==3.14.0 +pyyaml==6.0.2 +requests==2.32.3 +sortedcontainers==2.4.0 +urllib3==2.3.0 diff --git a/.riot/requirements/e49670c.txt b/.riot/requirements/e49670c.txt new file mode 100644 index 00000000000..4c6bb99ba9e --- /dev/null +++ b/.riot/requirements/e49670c.txt @@ -0,0 +1,25 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --no-annotate .riot/requirements/e49670c.in +# +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +coverage[toml]==7.6.10 +dd-trace-api @ git+https://github.com/DataDog/dd-trace-api-py +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.0.0 +mock==5.1.0 +opentracing==2.4.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-mock==3.14.0 +pyyaml==6.0.2 +requests==2.32.3 +sortedcontainers==2.4.0 +urllib3==2.3.0 diff --git a/ddtrace/_monkey.py b/ddtrace/_monkey.py index 8ede9f49ca4..4fb61ea5d8c 100644 --- a/ddtrace/_monkey.py +++ b/ddtrace/_monkey.py @@ -37,6 +37,7 @@ "cassandra": True, "celery": True, "consul": True, + "dd_trace_api": True, "django": True, "dramatiq": True, "elasticsearch": True, diff --git a/ddtrace/contrib/internal/dd_trace_api/__init__.py b/ddtrace/contrib/internal/dd_trace_api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/contrib/internal/dd_trace_api/patch.py b/ddtrace/contrib/internal/dd_trace_api/patch.py new file mode 100644 index 00000000000..965b8c4f318 --- /dev/null +++ b/ddtrace/contrib/internal/dd_trace_api/patch.py @@ -0,0 +1,79 @@ +from sys import addaudithook +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple +import weakref + +import dd_trace_api + +import ddtrace + + +_DD_HOOK_NAME = "dd.hook" +_TRACER_KEY = "Tracer" +_STUB_TO_REAL = weakref.WeakKeyDictionary() +_STUB_TO_REAL[dd_trace_api.tracer] = ddtrace.tracer + + +def _proxy_span_arguments(args: List, kwargs: Dict) -> Tuple[List, Dict]: + """Convert all dd_trace_api.Span objects in the args/kwargs collections to their held ddtrace.Span objects""" + proxied_args = [] + for arg in args: + if isinstance(arg, dd_trace_api.Span): + proxied_args.append(_STUB_TO_REAL[arg]) + else: + proxied_args.append(arg) + proxied_kwargs = {} + for name, kwarg in kwargs.items(): + if isinstance(kwarg, dd_trace_api.Span): + proxied_kwargs[name] = _STUB_TO_REAL[kwarg] + else: + proxied_kwargs[name] = kwarg + return proxied_args, proxied_kwargs + + +def _call_on_real_instance( + operand_stub: dd_trace_api._Stub, method_name: str, retval_from_api: Optional[Any], *args: List, **kwargs: Dict +) -> None: + """ + Call `method_name` on the real object corresponding to `operand_stub` with `args` and `kwargs` as arguments. + + Store the value that will be returned from the API call we're in the middle of, for the purpose + of mapping from those Stub objects to their real counterparts. + """ + args, kwargs = _proxy_span_arguments(args, kwargs) + retval_from_impl = getattr(_STUB_TO_REAL[operand_stub], method_name)(*args, **kwargs) + if retval_from_api is not None: + _STUB_TO_REAL[retval_from_api] = retval_from_impl + + +def _hook(name, hook_args): + """Called in response to `sys.audit` events""" + if not dd_trace_api.__datadog_patch or name != _DD_HOOK_NAME: + return + args = hook_args[0][0] + api_return_value, stub_self, event_name = args[0:3] + _call_on_real_instance(stub_self, event_name, api_return_value, *args[3:], **hook_args[0][1]) + + +def get_version() -> str: + return getattr(dd_trace_api, "__version__", "") + + +def patch(tracer=None): + if getattr(dd_trace_api, "__datadog_patch", False): + return + dd_trace_api.__datadog_patch = True + _STUB_TO_REAL[dd_trace_api.tracer] = tracer + if not getattr(dd_trace_api, "__dd_has_audit_hook", False): + addaudithook(_hook) + dd_trace_api.__dd_has_audit_hook = True + + +def unpatch(): + if not getattr(dd_trace_api, "__datadog_patch", False): + return + dd_trace_api.__datadog_patch = False + # NB sys.addaudithook's cannot be removed diff --git a/lib-injection/sources/min_compatible_versions.csv b/lib-injection/sources/min_compatible_versions.csv index 4537863f24c..77abca4d8cb 100644 --- a/lib-injection/sources/min_compatible_versions.csv +++ b/lib-injection/sources/min_compatible_versions.csv @@ -17,14 +17,15 @@ algoliasearch,~=2.5 anthropic,==0.26.0 anyio,>=3.4.0 aredis,0 -asgiref,~=3.0 +asgiref,~=3.0.0 astunparse,0 async_generator,~=1.10 -asyncpg,~=0.23 +asyncpg,~=0.23.0 asynctest,==0.13.0 austin-python,~=1.0 avro,0 azure.functions,0 +bcrypt,==4.2.1 blinker,0 boto3,==1.34.49 bottle,>=0.12 @@ -64,19 +65,20 @@ elasticsearch[async],0 envier,==0.5.2 exceptiongroup,0 faiss-cpu,==1.8.0 -falcon,~=3.0 +falcon,~=3.0.0 fastapi,~=0.64.0 flask,~=0.12.0 flask-caching,~=1.10.0 flask-openapi3,0 gevent,~=20.12.0 +git+https://github.com/DataDog/dd-trace-api-py,0 google-ai-generativelanguage,0 google-generativeai,0 googleapis-common-protos,0 graphene,~=3.0.0 graphql-core,~=3.2.0 graphql-relay,0 -greenlet,~=1.0.0 +greenlet,~=1.0 grpcio,~=1.34.0 gunicorn,==20.0.4 gunicorn[gevent],0 @@ -97,13 +99,13 @@ langchain-community,==0.0.14 langchain-core,==0.1.52 langchain-openai,==0.1.6 langchain-pinecone,==0.1.0 -langchain_experimental,==0.0.47 +langgraph,~=0.2.60 logbook,~=1.0.0 loguru,~=0.4.0 lxml,0 lz4,0 mako,~=1.1.0 -mariadb,~=1.0.0 +mariadb,~=1.0 markupsafe,<2.0 mock,0 molten,>=1.0 diff --git a/min_compatible_versions.csv b/min_compatible_versions.csv index 4537863f24c..77abca4d8cb 100644 --- a/min_compatible_versions.csv +++ b/min_compatible_versions.csv @@ -17,14 +17,15 @@ algoliasearch,~=2.5 anthropic,==0.26.0 anyio,>=3.4.0 aredis,0 -asgiref,~=3.0 +asgiref,~=3.0.0 astunparse,0 async_generator,~=1.10 -asyncpg,~=0.23 +asyncpg,~=0.23.0 asynctest,==0.13.0 austin-python,~=1.0 avro,0 azure.functions,0 +bcrypt,==4.2.1 blinker,0 boto3,==1.34.49 bottle,>=0.12 @@ -64,19 +65,20 @@ elasticsearch[async],0 envier,==0.5.2 exceptiongroup,0 faiss-cpu,==1.8.0 -falcon,~=3.0 +falcon,~=3.0.0 fastapi,~=0.64.0 flask,~=0.12.0 flask-caching,~=1.10.0 flask-openapi3,0 gevent,~=20.12.0 +git+https://github.com/DataDog/dd-trace-api-py,0 google-ai-generativelanguage,0 google-generativeai,0 googleapis-common-protos,0 graphene,~=3.0.0 graphql-core,~=3.2.0 graphql-relay,0 -greenlet,~=1.0.0 +greenlet,~=1.0 grpcio,~=1.34.0 gunicorn,==20.0.4 gunicorn[gevent],0 @@ -97,13 +99,13 @@ langchain-community,==0.0.14 langchain-core,==0.1.52 langchain-openai,==0.1.6 langchain-pinecone,==0.1.0 -langchain_experimental,==0.0.47 +langgraph,~=0.2.60 logbook,~=1.0.0 loguru,~=0.4.0 lxml,0 lz4,0 mako,~=1.1.0 -mariadb,~=1.0.0 +mariadb,~=1.0 markupsafe,<2.0 mock,0 molten,>=1.0 diff --git a/releasenotes/notes/dd-trace-api-integration-0fa7ea051a4d6ce1.yaml b/releasenotes/notes/dd-trace-api-integration-0fa7ea051a4d6ce1.yaml new file mode 100644 index 00000000000..9b5d2efaaf2 --- /dev/null +++ b/releasenotes/notes/dd-trace-api-integration-0fa7ea051a4d6ce1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + dd-trace-api: adds a simple and minimal instrumentation for the dd-trace-api-py package diff --git a/riotfile.py b/riotfile.py index bf3e0eede96..27ad4b86541 100644 --- a/riotfile.py +++ b/riotfile.py @@ -756,6 +756,12 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT ), ], ), + Venv( + name="dd_trace_api", + command="pytest {cmdargs} tests/contrib/dd_trace_api", + pkgs={"git+https://github.com/DataDog/dd-trace-api-py": latest, "requests": latest}, + pys=select_pys(min_version="3.8"), + ), # Django Python version support # 2.2 3.5, 3.6, 3.7, 3.8 3.9 # 3.2 3.6, 3.7, 3.8, 3.9, 3.10 diff --git a/tests/contrib/dd_trace_api/__init__.py b/tests/contrib/dd_trace_api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/contrib/dd_trace_api/test_dd_trace_api_patch.py b/tests/contrib/dd_trace_api/test_dd_trace_api_patch.py new file mode 100644 index 00000000000..4f2afd87494 --- /dev/null +++ b/tests/contrib/dd_trace_api/test_dd_trace_api_patch.py @@ -0,0 +1,31 @@ +# This test script was automatically generated by the contrib-patch-tests.py +# script. If you want to make changes to it, you should make sure that you have +# removed the ``_generated`` suffix from the file name, to prevent the content +# from being overwritten by future re-generations. + +from ddtrace.contrib.internal.dd_trace_api.patch import get_version +from ddtrace.contrib.internal.dd_trace_api.patch import patch + + +try: + from ddtrace.contrib.internal.dd_trace_api.patch import unpatch +except ImportError: + unpatch = None +from tests.contrib.patch import PatchTestCase + + +class TestDDTraceAPIPatch(PatchTestCase.Base): + __integration_name__ = "dd_trace_api" + __module_name__ = "dd_trace_api" + __patch_func__ = patch + __unpatch_func__ = unpatch + __get_version__ = get_version + + def assert_module_patched(self, requests): + pass + + def assert_not_module_patched(self, requests): + pass + + def assert_not_module_double_patched(self, requests): + pass diff --git a/tests/contrib/dd_trace_api/test_integration.py b/tests/contrib/dd_trace_api/test_integration.py new file mode 100644 index 00000000000..560d198191e --- /dev/null +++ b/tests/contrib/dd_trace_api/test_integration.py @@ -0,0 +1,107 @@ +import sys +from typing import Any + +import dd_trace_api + +from ddtrace import Span as dd_span_class +from ddtrace.contrib.internal.dd_trace_api.patch import patch +from ddtrace.contrib.internal.dd_trace_api.patch import unpatch +from tests.utils import TracerTestCase + + +class DDTraceAPITestCase(TracerTestCase): + def setUp(self): + super(DDTraceAPITestCase, self).setUp() + patch(tracer=self.tracer) + + def tearDown(self): + self.pop_spans() + super(DDTraceAPITestCase, self).tearDown() + unpatch() + + def _assert_span_stub(self, stub: Any): + assert not isinstance(stub, dd_span_class), "Returned span object should be a stub" + + def _assert_real_spans(self, count=1): + spans = self.pop_spans() + assert len(spans) == count + generated_span = spans[0] + assert isinstance(generated_span, dd_span_class), "Generated span is a real span" + assert hasattr(generated_span, "span_id"), "Generated span should support read operations" + return spans + + def test_tracer_singleton(self): + assert isinstance(dd_trace_api.tracer, dd_trace_api.Tracer), "Tracer stub should be exposed as a singleton" + + def test_start_span(self): + with dd_trace_api.tracer.start_span("web.request") as span: + self._assert_span_stub(span) + self._assert_real_spans() + + def test_span_finish(self): + span = dd_trace_api.tracer.start_span("web.request") + self._assert_span_stub(span) + span.finish() + self._assert_real_spans() + + def test_span_finish_with_ancestors(self): + span = dd_trace_api.tracer.start_span("web.request") + child_span = dd_trace_api.tracer.start_span("web.request", child_of=span) + child_span.finish_with_ancestors() + self._assert_real_spans(2) + + def test_trace(self): + with dd_trace_api.tracer.trace("web.request") as span: + self._assert_span_stub(span) + self._assert_real_spans() + + def test_current_span(self): + with dd_trace_api.tracer.trace("web.request"): + span = dd_trace_api.tracer.current_span() + self._assert_span_stub(span) + self._assert_real_spans() + + def test_current_root_span(self): + with dd_trace_api.tracer.trace("web.request"): + span = dd_trace_api.tracer.current_root_span() + self._assert_span_stub(span) + with dd_trace_api.tracer.trace("web.other.request"): + root_from_nested = dd_trace_api.tracer.current_root_span() + self._assert_span_stub(root_from_nested) + self._assert_real_spans(2) + + def test_wrap(self): + @dd_trace_api.tracer.wrap() + def foo(): + return 1 + 1 + + result = foo() + assert result == 2 + self._assert_real_spans() + + def test_set_traceback(self): + with dd_trace_api.tracer.trace("web.request") as span: + try: + raise Exception + except Exception: # noqa + span.set_traceback() + spans = self._assert_real_spans() + assert "error.stack" in spans[0]._meta + + def test_set_exc_info(self): + with dd_trace_api.tracer.trace("web.request") as span: + try: + raise Exception + except Exception: # noqa + span.set_exc_info(*sys.exc_info()) + spans = self._assert_real_spans() + assert "error.message" in spans[0]._meta + assert "error.stack" in spans[0]._meta + assert "error.type" in spans[0]._meta + + def test_set_tags(self): + with dd_trace_api.tracer.trace("web.request") as span: + span.set_tags({"tag1": "value1", "tag2": "value2"}) + spans = self._assert_real_spans() + assert spans[0]._meta["tag1"] == "value1", "Tag set via API should be applied to the real spans" + assert spans[0]._meta["tag2"] == "value2", "Tag set via API should be applied to the real spans" diff --git a/tests/contrib/suitespec.yml b/tests/contrib/suitespec.yml index 8c5fbc72da2..222f25899a0 100644 --- a/tests/contrib/suitespec.yml +++ b/tests/contrib/suitespec.yml @@ -75,6 +75,9 @@ components: - ddtrace/contrib/dbapi/* - ddtrace/contrib/dbapi_async/* - ddtrace/ext/db.py + dd_trace_api: + - ddtrace/contrib/dd_trace_api/* + - ddtrace/contrib/internal/dd_trace_api/* django: - ddtrace/contrib/django/* - ddtrace/contrib/internal/django/* @@ -471,6 +474,15 @@ suites: - '@datastreams' - tests/datastreams/* runner: riot + dd_trace_api: + paths: + - '@bootstrap' + - '@core' + - '@tracing' + - '@dd_trace_api' + - tests/contrib/dd_trace_api/* + runner: riot + snapshot: true django: env: TEST_MEMCACHED_HOST: memcached