Skip to content

Commit 5e4c6a7

Browse files
authored
Merge branch 'master' into antonpirker/missing-stack-trames
2 parents 7189be1 + da0b086 commit 5e4c6a7

File tree

7 files changed

+938
-40
lines changed

7 files changed

+938
-40
lines changed

sentry_sdk/_init_implementation.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
from typing import TYPE_CHECKING
24

35
import sentry_sdk
@@ -9,16 +11,35 @@
911

1012

1113
class _InitGuard:
14+
_CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = (
15+
"Using the return value of sentry_sdk.init as a context manager "
16+
"and manually calling the __enter__ and __exit__ methods on the "
17+
"return value are deprecated. We are no longer maintaining this "
18+
"functionality, and we will remove it in the next major release."
19+
)
20+
1221
def __init__(self, client):
1322
# type: (sentry_sdk.Client) -> None
1423
self._client = client
1524

1625
def __enter__(self):
1726
# type: () -> _InitGuard
27+
warnings.warn(
28+
self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE,
29+
stacklevel=2,
30+
category=DeprecationWarning,
31+
)
32+
1833
return self
1934

2035
def __exit__(self, exc_type, exc_value, tb):
2136
# type: (Any, Any, Any) -> None
37+
warnings.warn(
38+
self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE,
39+
stacklevel=2,
40+
category=DeprecationWarning,
41+
)
42+
2243
c = self._client
2344
if c is not None:
2445
c.close()
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
include_tracing_fields: Optional[bool] = None,
155+
):
156+
self.origin = origin
157+
self.event_type_mapping = event_type_mapping
158+
self.span_filter = span_filter
159+
self.include_tracing_fields = include_tracing_fields
160+
161+
def _include_tracing_fields(self) -> bool:
162+
"""
163+
By default, the values of tracing fields are not included in case they
164+
contain PII. A user may override that by passing `True` for the
165+
`include_tracing_fields` keyword argument of this integration or by
166+
setting `send_default_pii` to `True` in their Sentry client options.
167+
"""
168+
return (
169+
should_send_default_pii()
170+
if self.include_tracing_fields is None
171+
else self.include_tracing_fields
172+
)
173+
174+
def on_event(self, event: str, _span_state: TraceState) -> None:
175+
deserialized_event = json.loads(event)
176+
metadata = deserialized_event.get("metadata", {})
177+
178+
event_type = self.event_type_mapping(metadata)
179+
if event_type == EventTypeMapping.Ignore:
180+
return
181+
elif event_type == EventTypeMapping.Exc:
182+
process_exception(deserialized_event)
183+
elif event_type == EventTypeMapping.Breadcrumb:
184+
process_breadcrumb(deserialized_event)
185+
elif event_type == EventTypeMapping.Event:
186+
process_event(deserialized_event)
187+
188+
def on_new_span(self, attrs: str, span_id: str) -> TraceState:
189+
attrs = json.loads(attrs)
190+
metadata = attrs.get("metadata", {})
191+
192+
if not self.span_filter(metadata):
193+
return None
194+
195+
module_path = metadata.get("module_path")
196+
name = metadata.get("name")
197+
message = attrs.get("message")
198+
199+
if message is not None:
200+
sentry_span_name = message
201+
elif module_path is not None and name is not None:
202+
sentry_span_name = f"{module_path}::{name}" # noqa: E231
203+
elif name is not None:
204+
sentry_span_name = name
205+
else:
206+
sentry_span_name = "<unknown>"
207+
208+
kwargs = {
209+
"op": "function",
210+
"name": sentry_span_name,
211+
"origin": self.origin,
212+
}
213+
214+
scope = sentry_sdk.get_current_scope()
215+
parent_sentry_span = scope.span
216+
if parent_sentry_span:
217+
sentry_span = parent_sentry_span.start_child(**kwargs)
218+
else:
219+
sentry_span = scope.start_span(**kwargs)
220+
221+
fields = metadata.get("fields", [])
222+
for field in fields:
223+
if self._include_tracing_fields():
224+
sentry_span.set_data(field, attrs.get(field))
225+
else:
226+
sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE)
227+
228+
scope.span = sentry_span
229+
return (parent_sentry_span, sentry_span)
230+
231+
def on_close(self, span_id: str, span_state: TraceState) -> None:
232+
if span_state is None:
233+
return
234+
235+
parent_sentry_span, sentry_span = span_state
236+
sentry_span.finish()
237+
sentry_sdk.get_current_scope().span = parent_sentry_span
238+
239+
def on_record(self, span_id: str, values: str, span_state: TraceState) -> None:
240+
if span_state is None:
241+
return
242+
_parent_sentry_span, sentry_span = span_state
243+
244+
deserialized_values = json.loads(values)
245+
for key, value in deserialized_values.items():
246+
if self._include_tracing_fields():
247+
sentry_span.set_data(key, value)
248+
else:
249+
sentry_span.set_data(key, SENSITIVE_DATA_SUBSTITUTE)
250+
251+
252+
class RustTracingIntegration(Integration):
253+
"""
254+
Ingests tracing data from a Rust native extension's `tracing` instrumentation.
255+
256+
If a project uses more than one Rust native extension, each one will need
257+
its own instance of `RustTracingIntegration` with an initializer function
258+
specific to that extension.
259+
260+
Since all of the setup for this integration requires instance-specific state
261+
which is not available in `setup_once()`, setup instead happens in `__init__()`.
262+
"""
263+
264+
def __init__(
265+
self,
266+
identifier: str,
267+
initializer: Callable[[RustTracingLayer], None],
268+
event_type_mapping: Callable[
269+
[Dict[str, Any]], EventTypeMapping
270+
] = default_event_type_mapping,
271+
span_filter: Callable[[Dict[str, Any]], bool] = default_span_filter,
272+
include_tracing_fields: Optional[bool] = None,
273+
):
274+
self.identifier = identifier
275+
origin = f"auto.function.rust_tracing.{identifier}"
276+
self.tracing_layer = RustTracingLayer(
277+
origin, event_type_mapping, span_filter, include_tracing_fields
278+
)
279+
280+
initializer(self.tracing_layer)
281+
282+
@staticmethod
283+
def setup_once() -> None:
284+
pass

0 commit comments

Comments
 (0)