rust_tracing.py 8.9 KB

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