rust_tracing.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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, 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. class RustTracingLevel(Enum):
  38. Trace = "TRACE"
  39. Debug = "DEBUG"
  40. Info = "INFO"
  41. Warn = "WARN"
  42. Error = "ERROR"
  43. class EventTypeMapping(Enum):
  44. Ignore = auto()
  45. Exc = auto()
  46. Breadcrumb = auto()
  47. Event = auto()
  48. def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr":
  49. level = RustTracingLevel(level)
  50. if level in (RustTracingLevel.Trace, RustTracingLevel.Debug):
  51. return "debug"
  52. elif level == RustTracingLevel.Info:
  53. return "info"
  54. elif level == RustTracingLevel.Warn:
  55. return "warning"
  56. elif level == RustTracingLevel.Error:
  57. return "error"
  58. else:
  59. # Better this than crashing
  60. return "info"
  61. def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]":
  62. metadata = event.get("metadata", {})
  63. contexts = {}
  64. location = {}
  65. for field in ["module_path", "file", "line"]:
  66. if field in metadata:
  67. location[field] = metadata[field]
  68. if len(location) > 0:
  69. contexts["rust_tracing_location"] = location
  70. fields = {}
  71. for field in metadata.get("fields", []):
  72. fields[field] = event.get(field)
  73. if len(fields) > 0:
  74. contexts["rust_tracing_fields"] = fields
  75. return contexts
  76. def process_event(event: "Dict[str, Any]") -> None:
  77. metadata = event.get("metadata", {})
  78. logger = metadata.get("target")
  79. level = tracing_level_to_sentry_level(metadata.get("level"))
  80. message: "sentry_sdk._types.Any" = event.get("message")
  81. contexts = extract_contexts(event)
  82. sentry_event: "sentry_sdk._types.Event" = {
  83. "logger": logger,
  84. "level": level,
  85. "message": message,
  86. "contexts": contexts,
  87. }
  88. sentry_sdk.capture_event(sentry_event)
  89. def process_exception(event: "Dict[str, Any]") -> None:
  90. process_event(event)
  91. def process_breadcrumb(event: "Dict[str, Any]") -> None:
  92. level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level"))
  93. message = event.get("message")
  94. sentry_sdk.add_breadcrumb(level=level, message=message)
  95. def default_span_filter(metadata: "Dict[str, Any]") -> bool:
  96. return RustTracingLevel(metadata.get("level")) in (
  97. RustTracingLevel.Error,
  98. RustTracingLevel.Warn,
  99. RustTracingLevel.Info,
  100. )
  101. def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping":
  102. level = RustTracingLevel(metadata.get("level"))
  103. if level == RustTracingLevel.Error:
  104. return EventTypeMapping.Exc
  105. elif level in (RustTracingLevel.Warn, RustTracingLevel.Info):
  106. return EventTypeMapping.Breadcrumb
  107. elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace):
  108. return EventTypeMapping.Ignore
  109. else:
  110. return EventTypeMapping.Ignore
  111. class RustTracingLayer:
  112. def __init__(
  113. self,
  114. origin: str,
  115. event_type_mapping: """Callable[
  116. [Dict[str, Any]], EventTypeMapping
  117. ]""" = default_event_type_mapping,
  118. span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter,
  119. include_tracing_fields: "Optional[bool]" = None,
  120. ):
  121. self.origin = origin
  122. self.event_type_mapping = event_type_mapping
  123. self.span_filter = span_filter
  124. self.include_tracing_fields = include_tracing_fields
  125. def _include_tracing_fields(self) -> bool:
  126. """
  127. By default, the values of tracing fields are not included in case they
  128. contain PII. A user may override that by passing `True` for the
  129. `include_tracing_fields` keyword argument of this integration or by
  130. setting `send_default_pii` to `True` in their Sentry client options.
  131. """
  132. return (
  133. should_send_default_pii()
  134. if self.include_tracing_fields is None
  135. else self.include_tracing_fields
  136. )
  137. def on_event(self, event: str, sentry_span: "SentrySpan") -> None:
  138. deserialized_event = json.loads(event)
  139. metadata = deserialized_event.get("metadata", {})
  140. event_type = self.event_type_mapping(metadata)
  141. if event_type == EventTypeMapping.Ignore:
  142. return
  143. elif event_type == EventTypeMapping.Exc:
  144. process_exception(deserialized_event)
  145. elif event_type == EventTypeMapping.Breadcrumb:
  146. process_breadcrumb(deserialized_event)
  147. elif event_type == EventTypeMapping.Event:
  148. process_event(deserialized_event)
  149. def on_new_span(self, attrs: str, span_id: str) -> "Optional[SentrySpan]":
  150. attrs = json.loads(attrs)
  151. metadata = attrs.get("metadata", {})
  152. if not self.span_filter(metadata):
  153. return None
  154. module_path = metadata.get("module_path")
  155. name = metadata.get("name")
  156. message = attrs.get("message")
  157. if message is not None:
  158. sentry_span_name = message
  159. elif module_path is not None and name is not None:
  160. sentry_span_name = f"{module_path}::{name}" # noqa: E231
  161. elif name is not None:
  162. sentry_span_name = name
  163. else:
  164. sentry_span_name = "<unknown>"
  165. kwargs = {
  166. "op": "function",
  167. "name": sentry_span_name,
  168. "origin": self.origin,
  169. }
  170. sentry_span = sentry_sdk.start_span(**kwargs)
  171. fields = metadata.get("fields", [])
  172. for field in fields:
  173. if self._include_tracing_fields():
  174. sentry_span.set_data(field, attrs.get(field))
  175. else:
  176. sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE)
  177. sentry_span.__enter__()
  178. return sentry_span
  179. def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None:
  180. if sentry_span is None:
  181. return
  182. sentry_span.__exit__(None, None, None)
  183. def on_record(self, span_id: str, values: str, sentry_span: "SentrySpan") -> None:
  184. if sentry_span is None:
  185. return
  186. deserialized_values = json.loads(values)
  187. for key, value in deserialized_values.items():
  188. if self._include_tracing_fields():
  189. sentry_span.set_data(key, value)
  190. else:
  191. sentry_span.set_data(key, SENSITIVE_DATA_SUBSTITUTE)
  192. class RustTracingIntegration(Integration):
  193. """
  194. Ingests tracing data from a Rust native extension's `tracing` instrumentation.
  195. If a project uses more than one Rust native extension, each one will need
  196. its own instance of `RustTracingIntegration` with an initializer function
  197. specific to that extension.
  198. Since all of the setup for this integration requires instance-specific state
  199. which is not available in `setup_once()`, setup instead happens in `__init__()`.
  200. """
  201. def __init__(
  202. self,
  203. identifier: str,
  204. initializer: "Callable[[RustTracingLayer], None]",
  205. event_type_mapping: """Callable[
  206. [Dict[str, Any]], EventTypeMapping
  207. ]""" = default_event_type_mapping,
  208. span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter,
  209. include_tracing_fields: "Optional[bool]" = None,
  210. ):
  211. self.identifier = identifier
  212. origin = f"auto.function.rust_tracing.{identifier}"
  213. self.tracing_layer = RustTracingLayer(
  214. origin, event_type_mapping, span_filter, include_tracing_fields
  215. )
  216. initializer(self.tracing_layer)
  217. @staticmethod
  218. def setup_once() -> None:
  219. pass