Skip to content

Commit 44e3d24

Browse files
glowskirszokeasaurusrex
authored andcommitted
feat(ray): Create Ray integration
The integration includes performance support. Also, add tests for the integration. Closes #2400
1 parent b24c1e4 commit 44e3d24

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

scripts/split-tox-gh-actions/split-tox-gh-actions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"langchain",
7575
"openai",
7676
"huggingface_hub",
77+
"ray",
7778
"rq",
7879
],
7980
"Databases": [

sentry_sdk/integrations/ray.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from sentry_sdk.integrations import DidNotEnable, Integration
2+
3+
try:
4+
import ray # type: ignore[import-not-found]
5+
except ImportError:
6+
raise DidNotEnable("Ray not installed.")
7+
import functools
8+
9+
from sentry_sdk.tracing import TRANSACTION_SOURCE_TASK
10+
import logging
11+
import sentry_sdk
12+
from importlib.metadata import version
13+
14+
from typing import TYPE_CHECKING
15+
16+
if TYPE_CHECKING:
17+
from collections.abc import Callable
18+
from typing import Any, Optional
19+
20+
21+
def _check_sentry_initialized():
22+
# type: () -> None
23+
if sentry_sdk.Hub.current.client:
24+
return
25+
# we cannot use sentry sdk logging facilities because it wasn't initialized
26+
logger = logging.getLogger("sentry_sdk.errors")
27+
logger.warning(
28+
"[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded."
29+
)
30+
31+
32+
def _patch_ray_remote():
33+
# type: () -> None
34+
old_remote = ray.remote
35+
36+
@functools.wraps(old_remote)
37+
def new_remote(f, *args, **kwargs):
38+
# type: (Callable[..., Any], *Any, **Any) -> Callable[..., Any]
39+
def _f(*f_args, _tracing=None, **f_kwargs):
40+
# type: (Any, Optional[dict[str, Any]], Any) -> Any
41+
_check_sentry_initialized()
42+
transaction = None
43+
if _tracing is not None:
44+
transaction = sentry_sdk.continue_trace(
45+
_tracing,
46+
op="ray.remote.receive",
47+
source=TRANSACTION_SOURCE_TASK,
48+
name="Ray worker transaction",
49+
)
50+
with sentry_sdk.start_transaction(transaction) as tx:
51+
result = f(*f_args, **f_kwargs)
52+
tx.set_status("ok")
53+
return result
54+
55+
rv = old_remote(_f, *args, *kwargs)
56+
old_remote_method = rv.remote
57+
58+
def _remote_method_with_header_propagation(*args, **kwargs):
59+
# type: (*Any, **Any) -> Any
60+
with sentry_sdk.start_span(
61+
op="ray.remote.send", description="Sending task to ray cluster."
62+
):
63+
tracing = {
64+
k: v
65+
for k, v in sentry_sdk.Hub.current.iter_trace_propagation_headers()
66+
}
67+
return old_remote_method(*args, **kwargs, _tracing=tracing)
68+
69+
rv.remote = _remote_method_with_header_propagation
70+
71+
return rv
72+
73+
ray.remote = new_remote
74+
return
75+
76+
77+
class RayIntegration(Integration):
78+
identifier = "ray"
79+
80+
@staticmethod
81+
def setup_once():
82+
# type: () -> None
83+
if tuple(int(x) for x in version("ray").split(".")) < (2, 7, 0):
84+
raise DidNotEnable("Ray 2.7.0 or newer required")
85+
_patch_ray_remote()

tests/integrations/ray/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pytest.importorskip("ray")

tests/integrations/ray/test_ray.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import time
2+
3+
import ray
4+
5+
import sentry_sdk
6+
from sentry_sdk.envelope import Envelope
7+
from sentry_sdk.integrations.ray import RayIntegration
8+
from tests.conftest import TestTransport
9+
10+
11+
class RayTestTransport(TestTransport):
12+
def __init__(self):
13+
self.events = []
14+
self.envelopes = []
15+
super().__init__(self.events.append, self.envelopes.append)
16+
17+
18+
def _setup_ray_sentry():
19+
sentry_sdk.init(
20+
traces_sample_rate=1.0,
21+
integrations=[RayIntegration()],
22+
transport=RayTestTransport(),
23+
)
24+
25+
26+
def test_ray():
27+
_setup_ray_sentry()
28+
29+
@ray.remote
30+
def _task():
31+
with sentry_sdk.start_span(op="task", description="example task step"):
32+
time.sleep(0.1)
33+
return sentry_sdk.Hub.current.client.transport.envelopes
34+
35+
ray.init(
36+
runtime_env=dict(worker_process_setup_hook=_setup_ray_sentry, working_dir="./")
37+
)
38+
39+
with sentry_sdk.start_transaction(op="task", name="ray test transaction"):
40+
worker_envelopes = ray.get(_task.remote())
41+
42+
_assert_envelopes_are_associated_with_same_trace_id(
43+
sentry_sdk.Hub.current.client.transport.envelopes[0], worker_envelopes[0]
44+
)
45+
46+
47+
def _assert_envelopes_are_associated_with_same_trace_id(
48+
client_side_envelope: Envelope, worker_envelope: Envelope
49+
):
50+
client_side_envelope_dict = client_side_envelope.get_transaction_event()
51+
worker_envelope_dict = worker_envelope.get_transaction_event()
52+
trace_id = client_side_envelope_dict["contexts"]["trace"]["trace_id"]
53+
for span in client_side_envelope_dict["spans"]:
54+
assert span["trace_id"] == trace_id
55+
for span in worker_envelope_dict["spans"]:
56+
assert span["trace_id"] == trace_id
57+
assert worker_envelope_dict["contexts"]["trace"]["trace_id"] == trace_id

tox.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ envlist =
185185
{py3.8,py3.11,py3.12}-quart-v{0.19}
186186
{py3.8,py3.11,py3.12}-quart-latest
187187

188+
# Ray
189+
{py3.10,py3.11}-ray
190+
188191
# Redis
189192
{py3.6,py3.8}-redis-v{3}
190193
{py3.7,py3.8,py3.11}-redis-v{4}
@@ -494,6 +497,9 @@ deps =
494497
pyramid-v2.0: pyramid~=2.0.0
495498
pyramid-latest: pyramid
496499

500+
# Ray
501+
ray: ray>=2.7.0
502+
497503
# Quart
498504
quart: quart-auth
499505
quart: pytest-asyncio
@@ -638,6 +644,7 @@ setenv =
638644
pymongo: TESTPATH=tests/integrations/pymongo
639645
pyramid: TESTPATH=tests/integrations/pyramid
640646
quart: TESTPATH=tests/integrations/quart
647+
ray: TESTPATH=tests/integrations/ray
641648
redis: TESTPATH=tests/integrations/redis
642649
rediscluster: TESTPATH=tests/integrations/rediscluster
643650
requests: TESTPATH=tests/integrations/requests

0 commit comments

Comments
 (0)