Skip to content

Commit 4bec4a4

Browse files
feat: introduce rust_tracing integration (#3717)
Introduce a new integration that allows traces to descend into code in Rust native extensions by hooking into Rust's popular `tracing` framework. it relies on the Rust native extension using [`pyo3-python-tracing-subscriber`](https://crates.io/crates/pyo3-python-tracing-subscriber), a crate i recently published under Sentry, to expose a way for the Python SDK to hook into `tracing`. in this screenshot, the transaction was started in Python but the rest of the span tree reflects the structure and performance of a naive fibonacci generator in Rust: https://github.com/user-attachments/assets/ae2caff6-1842-45d0-a604-2f3b6305f330 --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent c2361a3 commit 4bec4a4

File tree

3 files changed

+724
-0
lines changed

3 files changed

+724
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
"""
2+
This integration ingests tracing data from native extensions written in Rust.
3+
4+
Using it requires additional setup on the Rust side to accept a
5+
`RustTracingLayer` Python object and register it with the `tracing-subscriber`
6+
using an adapter from the `pyo3-python-tracing-subscriber` crate. For example:
7+
```rust
8+
#[pyfunction]
9+
pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) {
10+
tracing_subscriber::registry()
11+
.with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl))
12+
.init();
13+
}
14+
```
15+
16+
Usage in Python would then look like:
17+
```
18+
sentry_sdk.init(
19+
dsn=sentry_dsn,
20+
integrations=[
21+
RustTracingIntegration(
22+
"demo_rust_extension",
23+
demo_rust_extension.initialize_tracing,
24+
event_type_mapping=event_type_mapping,
25+
)
26+
],
27+
)
28+
```
29+
30+
Each native extension requires its own integration.
31+
"""
32+
33+
import json
34+
from enum import Enum, auto
35+
from typing import Any, Callable, Dict, Tuple, Optional
36+
37+
import sentry_sdk
38+
from sentry_sdk.integrations import Integration
39+
from sentry_sdk.scope import should_send_default_pii
40+
from sentry_sdk.tracing import Span as SentrySpan
41+
from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
42+
43+
TraceState = Optional[Tuple[Optional[SentrySpan], SentrySpan]]
44+
45+
46+
class RustTracingLevel(Enum):
47+
Trace: str = "TRACE"
48+
Debug: str = "DEBUG"
49+
Info: str = "INFO"
50+
Warn: str = "WARN"
51+
Error: str = "ERROR"
52+
53+
54+
class EventTypeMapping(Enum):
55+
Ignore = auto()
56+
Exc = auto()
57+
Breadcrumb = auto()
58+
Event = auto()
59+
60+
61+
def tracing_level_to_sentry_level(level):
62+
# type: (str) -> sentry_sdk._types.LogLevelStr
63+
level = RustTracingLevel(level)
64+
if level in (RustTracingLevel.Trace, RustTracingLevel.Debug):
65+
return "debug"
66+
elif level == RustTracingLevel.Info:
67+
return "info"
68+
elif level == RustTracingLevel.Warn:
69+
return "warning"
70+
elif level == RustTracingLevel.Error:
71+
return "error"
72+
else:
73+
# Better this than crashing
74+
return "info"
75+
76+
77+
def extract_contexts(event: Dict[str, Any]) -> Dict[str, Any]:
78+
metadata = event.get("metadata", {})
79+
contexts = {}
80+
81+
location = {}
82+
for field in ["module_path", "file", "line"]:
83+
if field in metadata:
84+
location[field] = metadata[field]
85+
if len(location) > 0:
86+
contexts["rust_tracing_location"] = location
87+
88+
fields = {}
89+
for field in metadata.get("fields", []):
90+
fields[field] = event.get(field)
91+
if len(fields) > 0:
92+
contexts["rust_tracing_fields"] = fields
93+
94+
return contexts
95+
96+
97+
def process_event(event: Dict[str, Any]) -> None:
98+
metadata = event.get("metadata", {})
99+
100+
logger = metadata.get("target")
101+
level = tracing_level_to_sentry_level(metadata.get("level"))
102+
message = event.get("message") # type: sentry_sdk._types.Any
103+
contexts = extract_contexts(event)
104+
105+
sentry_event = {
106+
"logger": logger,
107+
"level": level,
108+
"message": message,
109+
"contexts": contexts,
110+
} # type: sentry_sdk._types.Event
111+
112+
sentry_sdk.capture_event(sentry_event)
113+
114+
115+
def process_exception(event: Dict[str, Any]) -> None:
116+
process_event(event)
117+
118+
119+
def process_breadcrumb(event: Dict[str, Any]) -> None:
120+
level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level"))
121+
message = event.get("message")
122+
123+
sentry_sdk.add_breadcrumb(level=level, message=message)
124+
125+
126+
def default_span_filter(metadata: Dict[str, Any]) -> bool:
127+
return RustTracingLevel(metadata.get("level")) in (
128+
RustTracingLevel.Error,
129+
RustTracingLevel.Warn,
130+
RustTracingLevel.Info,
131+
)
132+
133+
134+
def default_event_type_mapping(metadata: Dict[str, Any]) -> EventTypeMapping:
135+
level = RustTracingLevel(metadata.get("level"))
136+
if level == RustTracingLevel.Error:
137+
return EventTypeMapping.Exc
138+
elif level in (RustTracingLevel.Warn, RustTracingLevel.Info):
139+
return EventTypeMapping.Breadcrumb
140+
elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace):
141+
return EventTypeMapping.Ignore
142+
else:
143+
return EventTypeMapping.Ignore
144+
145+
146+
class RustTracingLayer:
147+
def __init__(
148+
self,
149+
origin: str,
150+
event_type_mapping: Callable[
151+
[Dict[str, Any]], EventTypeMapping
152+
] = default_event_type_mapping,
153+
span_filter: Callable[[Dict[str, Any]], bool] = default_span_filter,
154+
send_sensitive_data: Optional[bool] = None,
155+
):
156+
self.origin = origin
157+
self.event_type_mapping = event_type_mapping
158+
self.span_filter = span_filter
159+
self.send_sensitive_data = send_sensitive_data
160+
161+
def on_event(self, event: str, _span_state: TraceState) -> None:
162+
deserialized_event = json.loads(event)
163+
metadata = deserialized_event.get("metadata", {})
164+
165+
event_type = self.event_type_mapping(metadata)
166+
if event_type == EventTypeMapping.Ignore:
167+
return
168+
elif event_type == EventTypeMapping.Exc:
169+
process_exception(deserialized_event)
170+
elif event_type == EventTypeMapping.Breadcrumb:
171+
process_breadcrumb(deserialized_event)
172+
elif event_type == EventTypeMapping.Event:
173+
process_event(deserialized_event)
174+
175+
def on_new_span(self, attrs: str, span_id: str) -> TraceState:
176+
attrs = json.loads(attrs)
177+
metadata = attrs.get("metadata", {})
178+
179+
if not self.span_filter(metadata):
180+
return None
181+
182+
module_path = metadata.get("module_path")
183+
name = metadata.get("name")
184+
message = attrs.get("message")
185+
186+
if message is not None:
187+
sentry_span_name = message
188+
elif module_path is not None and name is not None:
189+
sentry_span_name = f"{module_path}::{name}" # noqa: E231
190+
elif name is not None:
191+
sentry_span_name = name
192+
else:
193+
sentry_span_name = "<unknown>"
194+
195+
kwargs = {
196+
"op": "function",
197+
"name": sentry_span_name,
198+
"origin": self.origin,
199+
}
200+
201+
scope = sentry_sdk.get_current_scope()
202+
parent_sentry_span = scope.span
203+
if parent_sentry_span:
204+
sentry_span = parent_sentry_span.start_child(**kwargs)
205+
else:
206+
sentry_span = scope.start_span(**kwargs)
207+
208+
fields = metadata.get("fields", [])
209+
for field in fields:
210+
sentry_span.set_data(field, attrs.get(field))
211+
212+
scope.span = sentry_span
213+
return (parent_sentry_span, sentry_span)
214+
215+
def on_close(self, span_id: str, span_state: TraceState) -> None:
216+
if span_state is None:
217+
return
218+
219+
parent_sentry_span, sentry_span = span_state
220+
sentry_span.finish()
221+
sentry_sdk.get_current_scope().span = parent_sentry_span
222+
223+
def on_record(self, span_id: str, values: str, span_state: TraceState) -> None:
224+
if span_state is None:
225+
return
226+
_parent_sentry_span, sentry_span = span_state
227+
228+
send_sensitive_data = (
229+
should_send_default_pii()
230+
if self.send_sensitive_data is None
231+
else self.send_sensitive_data
232+
)
233+
234+
deserialized_values = json.loads(values)
235+
for key, value in deserialized_values.items():
236+
if send_sensitive_data:
237+
sentry_span.set_data(key, value)
238+
else:
239+
sentry_span.set_data(key, SENSITIVE_DATA_SUBSTITUTE)
240+
241+
242+
class RustTracingIntegration(Integration):
243+
"""
244+
Ingests tracing data from a Rust native extension's `tracing` instrumentation.
245+
246+
If a project uses more than one Rust native extension, each one will need
247+
its own instance of `RustTracingIntegration` with an initializer function
248+
specific to that extension.
249+
250+
Since all of the setup for this integration requires instance-specific state
251+
which is not available in `setup_once()`, setup instead happens in `__init__()`.
252+
"""
253+
254+
def __init__(
255+
self,
256+
identifier: str,
257+
initializer: Callable[[RustTracingLayer], None],
258+
event_type_mapping: Callable[
259+
[Dict[str, Any]], EventTypeMapping
260+
] = default_event_type_mapping,
261+
span_filter: Callable[[Dict[str, Any]], bool] = default_span_filter,
262+
send_sensitive_data: Optional[bool] = None,
263+
):
264+
self.identifier = identifier
265+
origin = f"auto.function.rust_tracing.{identifier}"
266+
self.tracing_layer = RustTracingLayer(
267+
origin, event_type_mapping, span_filter, send_sensitive_data
268+
)
269+
270+
initializer(self.tracing_layer)
271+
272+
@staticmethod
273+
def setup_once() -> None:
274+
pass

tests/integrations/rust_tracing/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)