Skip to content

Commit 23a1b35

Browse files
authored
feat(ray): add ignored actors configuration (#17673)
This PR adds the possibility to ignore actors. Ray by default will instrument all actors which can be super noisy. It is not possible to have the full list of ray actors we want to ignore while creating the integration. Therefore, we give a config for any user to add any actor to not instrument Co-authored-by: louis.tricot <louis.tricot@datadoghq.com>
1 parent 9fc7cbd commit 23a1b35

5 files changed

Lines changed: 112 additions & 1 deletion

File tree

ddtrace/contrib/internal/ray/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
Enable it when you need visibility into queuing/submission latency, fan-out patterns,
4141
or when work is submitted but delayed before execution.
4242
43+
- ``DD_TRACE_RAY_IGNORED_ACTORS``: JSON object mapping actor class names to actor methods
44+
to exclude from instrumentation, or ``"*"`` to exclude the full actor class (default:
45+
empty). For example, ``{"ActorA": ["method1"], "ActorB": "*"}``. Matching is based
46+
on class name only, not module.
47+
4348
- ``DD_TRACE_EXPERIMENTAL_LONG_RUNNING_FLUSH_INTERVAL``: Interval for resubmitting long-running
4449
spans (default: ``120.0`` seconds)
4550

ddtrace/contrib/internal/ray/core/actor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def inject_tracing_into_actor_class(wrapped, instance, args, kwargs):
208208
cls = wrapped(*args, **kwargs)
209209
module_name = str(cls.__module__)
210210
class_name = str(cls.__name__)
211+
ignored_actor_methods = _get_ray_integration_config().ignored_actors.get(class_name, frozenset())
211212

212213
# Skip tracing for certain ray modules
213214
if any(module_name.startswith(denied_module) for denied_module in RAY_ACTOR_MODULE_DENYLIST):
@@ -217,14 +218,18 @@ def inject_tracing_into_actor_class(wrapped, instance, args, kwargs):
217218
if class_name.startswith("_"):
218219
return cls
219220

221+
# Allow users to skip instrumentation for specific actor class names.
222+
if "*" in ignored_actor_methods:
223+
return cls
224+
220225
# Determine if the class is a JobSupervisor
221226
is_job_supervisor = f"{module_name}.{class_name}" == "ray.dashboard.modules.job.job_supervisor.JobSupervisor"
222227
# We do not want to instrument ping and polling to remove noise
223228
methods_to_ignore = {"ping", "_polling"} if is_job_supervisor else set()
224229

225230
methods = inspect.getmembers(cls, is_function_or_method)
226231
for name, method in methods:
227-
if name in methods_to_ignore:
232+
if name in methods_to_ignore or name in ignored_actor_methods:
228233
continue
229234

230235
if (

ddtrace/contrib/internal/ray/patch.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from collections.abc import Mapping
2+
import json
3+
from typing import Any
4+
15
import ray
26
from wrapt import wrap_function_wrapper as _w
37

@@ -31,6 +35,45 @@
3135
_job_contexts = {}
3236

3337

38+
def _parse_ignored_actors(value: Any) -> dict[str, frozenset[str]]:
39+
if not value:
40+
return {}
41+
42+
if isinstance(value, str):
43+
try:
44+
parsed_value = json.loads(value)
45+
except ValueError:
46+
log.warning("Invalid DD_TRACE_RAY_IGNORED_ACTORS value. Expected a JSON object.")
47+
return {}
48+
else:
49+
parsed_value = value
50+
51+
if not isinstance(parsed_value, Mapping):
52+
log.warning("Invalid DD_TRACE_RAY_IGNORED_ACTORS value. Expected a JSON object.")
53+
return {}
54+
55+
ignored_actors = {}
56+
for actor_name, ignored_methods in parsed_value.items():
57+
if not isinstance(actor_name, str) or not actor_name.strip():
58+
log.warning("Invalid DD_TRACE_RAY_IGNORED_ACTORS actor name. Expected a non-empty string.")
59+
continue
60+
61+
actor_name = actor_name.strip()
62+
if ignored_methods == "*":
63+
ignored_actors[actor_name] = frozenset({"*"})
64+
continue
65+
66+
if not isinstance(ignored_methods, (list, tuple, set, frozenset)):
67+
log.warning("Invalid DD_TRACE_RAY_IGNORED_ACTORS methods for actor %s. Expected a list.", actor_name)
68+
continue
69+
70+
methods = frozenset(method.strip() for method in ignored_methods if isinstance(method, str) and method.strip())
71+
if methods:
72+
ignored_actors[actor_name] = methods
73+
74+
return ignored_actors
75+
76+
3477
config._add(
3578
"ray",
3679
dict(
@@ -39,6 +82,7 @@
3982
trace_core_api=_get_config("DD_TRACE_RAY_CORE_API", default=False, modifier=asbool),
4083
trace_args_kwargs=_get_config("DD_TRACE_RAY_ARGS_KWARGS", default=False, modifier=asbool),
4184
submission_spans=_get_config("DD_TRACE_RAY_SUBMISSION_SPANS_ENABLED", default=False, modifier=asbool),
85+
ignored_actors=_get_config("DD_TRACE_RAY_IGNORED_ACTORS", default={}, modifier=_parse_ignored_actors),
4286
),
4387
)
4488

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
upgrade:
3+
- |
4+
ray: Adds ``DD_TRACE_RAY_IGNORED_ACTORS`` configuration to exclude specific Ray actor methods from instrumentation.
5+
Set ``DD_TRACE_RAY_IGNORED_ACTORS='{"ActorA": ["method1"], "ActorB": "*"}'`` to leave matching methods or actors uninstrumented while continuing to trace other Ray actor methods.
6+
Matching is based on actor class name only.

tests/contrib/ray/test_ray.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ray
88
from ray.util.tracing import tracing_helper
99

10+
from ddtrace.contrib.internal.ray.constants import DD_RAY_TRACE_CTX
1011
from tests.utils import TracerTestCase
1112
from tests.utils import override_config
1213

@@ -39,6 +40,16 @@
3940
]
4041

4142

43+
def test_parse_ignored_actors_returns_actor_method_mapping():
44+
from ddtrace.contrib.internal.ray.patch import _parse_ignored_actors
45+
46+
assert _parse_ignored_actors("") == {}
47+
assert _parse_ignored_actors('{"IgnoredCounter": ["increment"], "IgnoredActor": "*"}') == {
48+
"IgnoredCounter": frozenset({"increment"}),
49+
"IgnoredActor": frozenset({"*"}),
50+
}
51+
52+
4253
class TestRayIntegration(TracerTestCase):
4354
"""Test Ray integration with actual cluster setup and job submission"""
4455

@@ -129,6 +140,46 @@ def get_value(self):
129140
value = ray.get(denied_actor.get_value.remote())
130141
assert value == 42, f"Unexpected result: {value}"
131142

143+
def test_configurable_ignored_actor_methods(self):
144+
with override_config(
145+
"ray",
146+
dict(
147+
ignored_actors={"PartiallyIgnoredCounter": frozenset({"increment"}), "IgnoredCounter": frozenset({"*"})}
148+
),
149+
):
150+
151+
@ray.remote
152+
class PartiallyIgnoredCounter:
153+
def increment(self):
154+
return 1
155+
156+
def get_value(self):
157+
return 1
158+
159+
@ray.remote
160+
class IgnoredCounter:
161+
def increment(self):
162+
return 1
163+
164+
partially_ignored_actor = PartiallyIgnoredCounter.remote()
165+
ignored_actor = IgnoredCounter.remote()
166+
167+
partially_ignored_result = ray.get(partially_ignored_actor.increment.remote())
168+
traced_result = ray.get(partially_ignored_actor.get_value.remote())
169+
ignored_result = ray.get(ignored_actor.increment.remote())
170+
171+
assert partially_ignored_result == 1
172+
assert ignored_result == 1
173+
assert traced_result == 1
174+
175+
partially_ignored_params = [p.name for p in partially_ignored_actor._ray_method_signatures["increment"]]
176+
traced_params = [p.name for p in partially_ignored_actor._ray_method_signatures["get_value"]]
177+
ignored_params = [p.name for p in ignored_actor._ray_method_signatures["increment"]]
178+
179+
assert DD_RAY_TRACE_CTX not in partially_ignored_params
180+
assert DD_RAY_TRACE_CTX not in ignored_params
181+
assert DD_RAY_TRACE_CTX in traced_params
182+
132183
@pytest.mark.snapshot(token="tests.contrib.ray.test_ray.test_ignored_tasks", ignores=RAY_SNAPSHOT_IGNORES)
133184
def test_ignored_tasks(self):
134185
"""Test that tasks from modules in RAY_TASK_MODULE_DENYLIST are not traced."""

0 commit comments

Comments
 (0)