sqlalchemy.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. from sentry_sdk.consts import SPANSTATUS, SPANDATA
  2. from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
  3. from sentry_sdk.tracing_utils import add_query_source, record_sql_queries
  4. from sentry_sdk.utils import (
  5. capture_internal_exceptions,
  6. ensure_integration_enabled,
  7. parse_version,
  8. )
  9. try:
  10. from sqlalchemy.engine import Engine # type: ignore
  11. from sqlalchemy.event import listen # type: ignore
  12. from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore
  13. except ImportError:
  14. raise DidNotEnable("SQLAlchemy not installed.")
  15. from typing import TYPE_CHECKING
  16. if TYPE_CHECKING:
  17. from typing import Any
  18. from typing import ContextManager
  19. from typing import Optional
  20. from sentry_sdk.tracing import Span
  21. class SqlalchemyIntegration(Integration):
  22. identifier = "sqlalchemy"
  23. origin = f"auto.db.{identifier}"
  24. @staticmethod
  25. def setup_once():
  26. # type: () -> None
  27. version = parse_version(SQLALCHEMY_VERSION)
  28. _check_minimum_version(SqlalchemyIntegration, version)
  29. listen(Engine, "before_cursor_execute", _before_cursor_execute)
  30. listen(Engine, "after_cursor_execute", _after_cursor_execute)
  31. listen(Engine, "handle_error", _handle_error)
  32. @ensure_integration_enabled(SqlalchemyIntegration)
  33. def _before_cursor_execute(
  34. conn, cursor, statement, parameters, context, executemany, *args
  35. ):
  36. # type: (Any, Any, Any, Any, Any, bool, *Any) -> None
  37. ctx_mgr = record_sql_queries(
  38. cursor,
  39. statement,
  40. parameters,
  41. paramstyle=context and context.dialect and context.dialect.paramstyle or None,
  42. executemany=executemany,
  43. span_origin=SqlalchemyIntegration.origin,
  44. )
  45. context._sentry_sql_span_manager = ctx_mgr
  46. span = ctx_mgr.__enter__()
  47. if span is not None:
  48. _set_db_data(span, conn)
  49. context._sentry_sql_span = span
  50. @ensure_integration_enabled(SqlalchemyIntegration)
  51. def _after_cursor_execute(conn, cursor, statement, parameters, context, *args):
  52. # type: (Any, Any, Any, Any, Any, *Any) -> None
  53. ctx_mgr = getattr(context, "_sentry_sql_span_manager", None) # type: Optional[ContextManager[Any]]
  54. if ctx_mgr is not None:
  55. context._sentry_sql_span_manager = None
  56. ctx_mgr.__exit__(None, None, None)
  57. span = getattr(context, "_sentry_sql_span", None) # type: Optional[Span]
  58. if span is not None:
  59. with capture_internal_exceptions():
  60. add_query_source(span)
  61. def _handle_error(context, *args):
  62. # type: (Any, *Any) -> None
  63. execution_context = context.execution_context
  64. if execution_context is None:
  65. return
  66. span = getattr(execution_context, "_sentry_sql_span", None) # type: Optional[Span]
  67. if span is not None:
  68. span.set_status(SPANSTATUS.INTERNAL_ERROR)
  69. # _after_cursor_execute does not get called for crashing SQL stmts. Judging
  70. # from SQLAlchemy codebase it does seem like any error coming into this
  71. # handler is going to be fatal.
  72. ctx_mgr = getattr(execution_context, "_sentry_sql_span_manager", None) # type: Optional[ContextManager[Any]]
  73. if ctx_mgr is not None:
  74. execution_context._sentry_sql_span_manager = None
  75. ctx_mgr.__exit__(None, None, None)
  76. # See: https://docs.sqlalchemy.org/en/20/dialects/index.html
  77. def _get_db_system(name):
  78. # type: (str) -> Optional[str]
  79. name = str(name)
  80. if "sqlite" in name:
  81. return "sqlite"
  82. if "postgres" in name:
  83. return "postgresql"
  84. if "mariadb" in name:
  85. return "mariadb"
  86. if "mysql" in name:
  87. return "mysql"
  88. if "oracle" in name:
  89. return "oracle"
  90. return None
  91. def _set_db_data(span, conn):
  92. # type: (Span, Any) -> None
  93. db_system = _get_db_system(conn.engine.name)
  94. if db_system is not None:
  95. span.set_data(SPANDATA.DB_SYSTEM, db_system)
  96. if conn.engine.url is None:
  97. return
  98. db_name = conn.engine.url.database
  99. if db_name is not None:
  100. span.set_data(SPANDATA.DB_NAME, db_name)
  101. server_address = conn.engine.url.host
  102. if server_address is not None:
  103. span.set_data(SPANDATA.SERVER_ADDRESS, server_address)
  104. server_port = conn.engine.url.port
  105. if server_port is not None:
  106. span.set_data(SPANDATA.SERVER_PORT, server_port)