You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hamilton.apache.org by sk...@apache.org on 2026/05/11 04:29:21 UTC

(hamilton) 01/02: Upgrade Datadog plugin to ddtrace 4.x public API

This is an automated email from the ASF dual-hosted git repository.

skrawcz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hamilton.git

commit e10f6b27ef4fc75e22b1e377399c880b09e743b5
Author: Dev-iL <65...@users.noreply.github.com>
AuthorDate: Sun Apr 26 15:05:32 2026 +0300

    Upgrade Datadog plugin to ddtrace 4.x public API
    
    ddtrace 3.0 removed `Span` and `context` from the top-level package.
    Migrate to the canonical `ddtrace.trace` module, bump the version constraint to `>=4.0,<5`, fix stale `sf-hamilton` package references, and add smoke tests for the plugin.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 hamilton/plugins/h_ddog.py   |  15 ++---
 pyproject.toml               |   4 +-
 tests/plugins/test_h_ddog.py | 152 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 161 insertions(+), 10 deletions(-)

diff --git a/hamilton/plugins/h_ddog.py b/hamilton/plugins/h_ddog.py
index 8d61639a..4345516e 100644
--- a/hamilton/plugins/h_ddog.py
+++ b/hamilton/plugins/h_ddog.py
@@ -25,14 +25,13 @@ from hamilton.lifecycle import base
 
 logger = logging.getLogger(__name__)
 try:
-    # TODO: this works for ddtrace < 3.0; Span was deprecated in 3.0
-    # See https://github.com/DataDog/dd-trace-py/pull/12186
-    from ddtrace import Span, context, tracer
+    from ddtrace import tracer
+    from ddtrace.trace import Context, Span
 except ImportError as e:
     logger.error("ImportError: %s", e)
     logger.error(
-        "To use the h_ddog plugin, please install sf-hamilton[datadog] using "
-        "`pip install sf-hamilton[datadog]` (or use your favorite package manager)."
+        "To use the h_ddog plugin, please install apache-hamilton[datadog] using "
+        "`pip install apache-hamilton[datadog]` (or use your favorite package manager). "
         "Remember to use quotes around the package name if using zsh!"
     )
     raise
@@ -75,14 +74,14 @@ class _DDOGTracerImpl:
         }
 
     @staticmethod
-    def _deserialize_span_dict(serialized_repr: dict[str, dict]) -> dict[str, context.Context]:
+    def _deserialize_span_dict(serialized_repr: dict[str, dict]) -> dict[str, Context]:
         """Note that we deserialize as contexts, as passing spans is not supported
         (the child should never terminate the parent span).
 
         :param span_dict: Dict of str -> dict params for contexts
         :return: A dictionary of contexts
         """
-        return {key: context.Context(**params) for key, params in serialized_repr.items()}
+        return {key: Context(**params) for key, params in serialized_repr.items()}
 
     def __getstate__(self):
         """Gets the state for serialization"""
@@ -293,7 +292,7 @@ class DDOGTracer(
     This tracer bypasses context management so we can more accurately track relationships between nodes/tags. Also, we plan to
     get this working with OpenTelemetry, and use that for datadog integration.
 
-    To use this, you'll want to run `pip install sf-hamilton[ddog]` (or `pip install "sf-hamilton[ddog]"` if using zsh)
+    To use this, you'll want to run `pip install apache-hamilton[datadog]` (or `pip install "apache-hamilton[datadog]"` if using zsh)
     """
 
     def __init__(self, root_name: str, include_causal_links: bool = False, service: str = None):
diff --git a/pyproject.toml b/pyproject.toml
index 109e703d..f6d059bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,7 +59,7 @@ dask-array = ["dask[array]"]
 dask-dataframe = ["dask[dataframe]"]
 dask-diagnostics = ["dask[diagnostics]"]
 dask-distributed = ["dask[distributed]"]
-datadog = ["ddtrace<3.0"]  # Temporary pin until h_ddog.py import is fixed for >3.0 version
+datadog = ["ddtrace>=4.0,<5.0"]
 diskcache = ["diskcache"]
 experiments = [
   "fastapi",
@@ -140,7 +140,7 @@ docs = [
   "commonmark",
   "dask-expr>=1.1.14",
   "dask[distributed]",
-  "ddtrace<3.0",
+  "ddtrace>=4.0,<5",
   "diskcache",
   # required for all the plugins
   "dlt",
diff --git a/tests/plugins/test_h_ddog.py b/tests/plugins/test_h_ddog.py
new file mode 100644
index 00000000..4ff03fd4
--- /dev/null
+++ b/tests/plugins/test_h_ddog.py
@@ -0,0 +1,152 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+pytest.importorskip("ddtrace")
+
+from hamilton.plugins.h_ddog import AsyncDDOGTracer, DDOGTracer
+
+
+@pytest.fixture()
+def mock_tracer():
+    with patch("hamilton.plugins.h_ddog.tracer") as t:
+        mock_span = MagicMock()
+        mock_span.context = MagicMock(trace_id=1, span_id=2)
+        mock_span.__exit__ = MagicMock(return_value=False)
+        t.start_span.return_value = mock_span
+        t.current_trace_context.return_value = None
+        yield t
+
+
+class TestDDOGTracerLifecycle:
+    def test_span_lifecycle(self, mock_tracer):
+        tracer_inst = DDOGTracer(root_name="test-root", service="test-svc")
+        run_id = "run-1"
+
+        tracer_inst.run_before_graph_execution(run_id=run_id)
+        assert mock_tracer.start_span.call_count == 1
+
+        tracer_inst.run_before_node_execution(
+            node_name="node_a",
+            node_kwargs={},
+            node_tags={"module": "test"},
+            task_id=None,
+            run_id=run_id,
+        )
+        assert mock_tracer.start_span.call_count == 2
+
+        tracer_inst.run_after_node_execution(
+            node_name="node_a", error=None, task_id=None, run_id=run_id
+        )
+        node_span = mock_tracer.start_span.return_value
+        node_span.__exit__.assert_called_with(None, None, None)
+
+        tracer_inst.run_after_graph_execution(error=None, run_id=run_id)
+
+    def test_span_lifecycle_with_error(self, mock_tracer):
+        tracer_inst = DDOGTracer(root_name="test-root")
+        run_id = "run-err"
+
+        tracer_inst.run_before_graph_execution(run_id=run_id)
+        tracer_inst.run_before_node_execution(
+            node_name="node_b",
+            node_kwargs={},
+            node_tags={},
+            task_id=None,
+            run_id=run_id,
+        )
+
+        err = ValueError("boom")
+        tracer_inst.run_after_node_execution(
+            node_name="node_b", error=err, task_id=None, run_id=run_id
+        )
+        node_span = mock_tracer.start_span.return_value
+        node_span.__exit__.assert_called_with(ValueError, err, err.__traceback__)
+
+        tracer_inst.run_after_graph_execution(error=err, run_id=run_id)
+
+    def test_task_span_lifecycle(self, mock_tracer):
+        tracer_inst = DDOGTracer(root_name="test-root")
+        run_id = "run-task"
+
+        tracer_inst.run_before_graph_execution(run_id=run_id)
+        tracer_inst.run_before_task_execution(task_id="task-1", run_id=run_id)
+        tracer_inst.run_before_node_execution(
+            node_name="node_c",
+            node_kwargs={},
+            node_tags={},
+            task_id="task-1",
+            run_id=run_id,
+        )
+        tracer_inst.run_after_node_execution(
+            node_name="node_c", error=None, task_id="task-1", run_id=run_id
+        )
+        tracer_inst.run_after_task_execution(task_id="task-1", run_id=run_id, error=None)
+        tracer_inst.run_after_graph_execution(error=None, run_id=run_id)
+
+
+class TestAsyncDDOGTracer:
+    def test_instantiation(self):
+        inst = AsyncDDOGTracer(root_name="async-root", service="async-svc")
+        assert inst._impl.root_name == "async-root"
+        assert inst._impl.service == "async-svc"
+
+    def test_instantiation_with_causal_links(self):
+        inst = AsyncDDOGTracer(root_name="async-root", include_causal_links=True, service=None)
+        assert inst._impl.include_causal_links is True
+
+
+class TestSerialization:
+    def test_getstate_setstate_round_trip(self, mock_tracer):
+        tracer_inst = DDOGTracer(root_name="ser-root", service="ser-svc")
+        run_id = "run-ser"
+
+        tracer_inst.run_before_graph_execution(run_id=run_id)
+
+        state = tracer_inst._impl.__getstate__()
+        assert "root_trace_name" in state
+        assert state["root_trace_name"] == "ser-root"
+        assert state["service"] == "ser-svc"
+
+        new_impl = object.__new__(type(tracer_inst._impl))
+        new_impl.__setstate__(state)
+        assert new_impl.root_name == "ser-root"
+        assert new_impl.service == "ser-svc"
+        assert new_impl.include_causal_links is False
+        assert isinstance(new_impl.run_span_cache, dict)
+
+        tracer_inst.run_after_graph_execution(error=None, run_id=run_id)
+
+    def test_serialize_deserialize_span_dict(self, mock_tracer):
+        from ddtrace.trace import Context, Span
+
+        from hamilton.plugins.h_ddog import _DDOGTracerImpl
+
+        mock_span = MagicMock(spec=Span)
+        mock_span.context = MagicMock(trace_id=123, span_id=456)
+        span_dict = {"key1": mock_span}
+
+        serialized = _DDOGTracerImpl._serialize_span_dict(span_dict)
+        assert serialized["key1"]["trace_id"] == 123
+        assert serialized["key1"]["span_id"] == 456
+
+        deserialized = _DDOGTracerImpl._deserialize_span_dict(serialized)
+        assert "key1" in deserialized
+        assert isinstance(deserialized["key1"], Context)