| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- """
- This integration ingests tracing data from native extensions written in Rust.
- Using it requires additional setup on the Rust side to accept a
- `RustTracingLayer` Python object and register it with the `tracing-subscriber`
- using an adapter from the `pyo3-python-tracing-subscriber` crate. For example:
- ```rust
- #[pyfunction]
- pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) {
- tracing_subscriber::registry()
- .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl))
- .init();
- }
- ```
- Usage in Python would then look like:
- ```
- sentry_sdk.init(
- dsn=sentry_dsn,
- integrations=[
- RustTracingIntegration(
- "demo_rust_extension",
- demo_rust_extension.initialize_tracing,
- event_type_mapping=event_type_mapping,
- )
- ],
- )
- ```
- Each native extension requires its own integration.
- """
- import json
- from enum import Enum, auto
- from typing import Any, Callable, Dict, Tuple, Optional
- import sentry_sdk
- from sentry_sdk.integrations import Integration
- from sentry_sdk.scope import should_send_default_pii
- from sentry_sdk.tracing import Span as SentrySpan
- from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
- TraceState = Optional[Tuple[Optional[SentrySpan], SentrySpan]]
- class RustTracingLevel(Enum):
- Trace = "TRACE"
- Debug = "DEBUG"
- Info = "INFO"
- Warn = "WARN"
- Error = "ERROR"
- class EventTypeMapping(Enum):
- Ignore = auto()
- Exc = auto()
- Breadcrumb = auto()
- Event = auto()
- def tracing_level_to_sentry_level(level):
- # type: (str) -> sentry_sdk._types.LogLevelStr
- level = RustTracingLevel(level)
- if level in (RustTracingLevel.Trace, RustTracingLevel.Debug):
- return "debug"
- elif level == RustTracingLevel.Info:
- return "info"
- elif level == RustTracingLevel.Warn:
- return "warning"
- elif level == RustTracingLevel.Error:
- return "error"
- else:
- # Better this than crashing
- return "info"
- def extract_contexts(event: Dict[str, Any]) -> Dict[str, Any]:
- metadata = event.get("metadata", {})
- contexts = {}
- location = {}
- for field in ["module_path", "file", "line"]:
- if field in metadata:
- location[field] = metadata[field]
- if len(location) > 0:
- contexts["rust_tracing_location"] = location
- fields = {}
- for field in metadata.get("fields", []):
- fields[field] = event.get(field)
- if len(fields) > 0:
- contexts["rust_tracing_fields"] = fields
- return contexts
- def process_event(event: Dict[str, Any]) -> None:
- metadata = event.get("metadata", {})
- logger = metadata.get("target")
- level = tracing_level_to_sentry_level(metadata.get("level"))
- message = event.get("message") # type: sentry_sdk._types.Any
- contexts = extract_contexts(event)
- sentry_event = {
- "logger": logger,
- "level": level,
- "message": message,
- "contexts": contexts,
- } # type: sentry_sdk._types.Event
- sentry_sdk.capture_event(sentry_event)
- def process_exception(event: Dict[str, Any]) -> None:
- process_event(event)
- def process_breadcrumb(event: Dict[str, Any]) -> None:
- level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level"))
- message = event.get("message")
- sentry_sdk.add_breadcrumb(level=level, message=message)
- def default_span_filter(metadata: Dict[str, Any]) -> bool:
- return RustTracingLevel(metadata.get("level")) in (
- RustTracingLevel.Error,
- RustTracingLevel.Warn,
- RustTracingLevel.Info,
- )
- def default_event_type_mapping(metadata: Dict[str, Any]) -> EventTypeMapping:
- level = RustTracingLevel(metadata.get("level"))
- if level == RustTracingLevel.Error:
- return EventTypeMapping.Exc
- elif level in (RustTracingLevel.Warn, RustTracingLevel.Info):
- return EventTypeMapping.Breadcrumb
- elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace):
- return EventTypeMapping.Ignore
- else:
- return EventTypeMapping.Ignore
- class RustTracingLayer:
- def __init__(
- self,
- origin: str,
- event_type_mapping: Callable[
- [Dict[str, Any]], EventTypeMapping
- ] = default_event_type_mapping,
- span_filter: Callable[[Dict[str, Any]], bool] = default_span_filter,
- include_tracing_fields: Optional[bool] = None,
- ):
- self.origin = origin
- self.event_type_mapping = event_type_mapping
- self.span_filter = span_filter
- self.include_tracing_fields = include_tracing_fields
- def _include_tracing_fields(self) -> bool:
- """
- By default, the values of tracing fields are not included in case they
- contain PII. A user may override that by passing `True` for the
- `include_tracing_fields` keyword argument of this integration or by
- setting `send_default_pii` to `True` in their Sentry client options.
- """
- return (
- should_send_default_pii()
- if self.include_tracing_fields is None
- else self.include_tracing_fields
- )
- def on_event(self, event: str, _span_state: TraceState) -> None:
- deserialized_event = json.loads(event)
- metadata = deserialized_event.get("metadata", {})
- event_type = self.event_type_mapping(metadata)
- if event_type == EventTypeMapping.Ignore:
- return
- elif event_type == EventTypeMapping.Exc:
- process_exception(deserialized_event)
- elif event_type == EventTypeMapping.Breadcrumb:
- process_breadcrumb(deserialized_event)
- elif event_type == EventTypeMapping.Event:
- process_event(deserialized_event)
- def on_new_span(self, attrs: str, span_id: str) -> TraceState:
- attrs = json.loads(attrs)
- metadata = attrs.get("metadata", {})
- if not self.span_filter(metadata):
- return None
- module_path = metadata.get("module_path")
- name = metadata.get("name")
- message = attrs.get("message")
- if message is not None:
- sentry_span_name = message
- elif module_path is not None and name is not None:
- sentry_span_name = f"{module_path}::{name}" # noqa: E231
- elif name is not None:
- sentry_span_name = name
- else:
- sentry_span_name = "<unknown>"
- kwargs = {
- "op": "function",
- "name": sentry_span_name,
- "origin": self.origin,
- }
- scope = sentry_sdk.get_current_scope()
- parent_sentry_span = scope.span
- if parent_sentry_span:
- sentry_span = parent_sentry_span.start_child(**kwargs)
- else:
- sentry_span = scope.start_span(**kwargs)
- fields = metadata.get("fields", [])
- for field in fields:
- if self._include_tracing_fields():
- sentry_span.set_data(field, attrs.get(field))
- else:
- sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE)
- scope.span = sentry_span
- return (parent_sentry_span, sentry_span)
- def on_close(self, span_id: str, span_state: TraceState) -> None:
- if span_state is None:
- return
- parent_sentry_span, sentry_span = span_state
- sentry_span.finish()
- sentry_sdk.get_current_scope().span = parent_sentry_span
- def on_record(self, span_id: str, values: str, span_state: TraceState) -> None:
- if span_state is None:
- return
- _parent_sentry_span, sentry_span = span_state
- deserialized_values = json.loads(values)
- for key, value in deserialized_values.items():
- if self._include_tracing_fields():
- sentry_span.set_data(key, value)
- else:
- sentry_span.set_data(key, SENSITIVE_DATA_SUBSTITUTE)
- class RustTracingIntegration(Integration):
- """
- Ingests tracing data from a Rust native extension's `tracing` instrumentation.
- If a project uses more than one Rust native extension, each one will need
- its own instance of `RustTracingIntegration` with an initializer function
- specific to that extension.
- Since all of the setup for this integration requires instance-specific state
- which is not available in `setup_once()`, setup instead happens in `__init__()`.
- """
- def __init__(
- self,
- identifier: str,
- initializer: Callable[[RustTracingLayer], None],
- event_type_mapping: Callable[
- [Dict[str, Any]], EventTypeMapping
- ] = default_event_type_mapping,
- span_filter: Callable[[Dict[str, Any]], bool] = default_span_filter,
- include_tracing_fields: Optional[bool] = None,
- ):
- self.identifier = identifier
- origin = f"auto.function.rust_tracing.{identifier}"
- self.tracing_layer = RustTracingLayer(
- origin, event_type_mapping, span_filter, include_tracing_fields
- )
- initializer(self.tracing_layer)
- @staticmethod
- def setup_once() -> None:
- pass
|