db.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. from sqlalchemy import create_engine, event
  2. from sqlalchemy.orm import Session, declarative_base, sessionmaker
  3. from .config import settings
  4. from .logging_setup import db_operation, db_query, log_query_error, query_origin
  5. def install_engine_logging(engine):
  6. if getattr(engine, "_gc_logging", False):
  7. return
  8. engine._gc_logging = True
  9. @event.listens_for(engine, "before_cursor_execute")
  10. def executing(conn, cursor, statement, parameters, context, executemany):
  11. context._gc_operation = "execute"
  12. @event.listens_for(engine, "after_cursor_execute")
  13. def executed(conn, cursor, statement, parameters, context, executemany):
  14. context._gc_operation = "fetch"
  15. @event.listens_for(engine, "handle_error")
  16. def query_failed(context):
  17. # Observational only: never replace exceptions or affect connection invalidation.
  18. error = context.sqlalchemy_exception or context.original_exception
  19. options = getattr(context.execution_context, "execution_options", {}) or {}
  20. active = db_query.get() or {}
  21. statement = context.statement if context.statement is not None else active.get("statement")
  22. log_query_error(
  23. error,
  24. statement,
  25. options.get("query_name") or active.get("query_name"),
  26. db_operation.get() or getattr(context.execution_context, "_gc_operation", "connect_or_transaction"),
  27. parameters=context.parameters,
  28. )
  29. class LoggingSession(Session):
  30. def execute(self, statement, params=None, *, execution_options=None, **kwargs):
  31. options = dict(execution_options or {})
  32. origin = options.setdefault(
  33. "query_name", getattr(statement, "_execution_options", {}).get("query_name") or query_origin()
  34. )
  35. token = db_operation.set("execute")
  36. query_token = db_query.set({"statement": statement, "query_name": origin})
  37. try:
  38. return super().execute(statement, params, execution_options=options, **kwargs)
  39. except Exception as error:
  40. log_query_error(error, statement, origin, "execute", parameters=params)
  41. raise
  42. finally:
  43. db_query.reset(query_token)
  44. db_operation.reset(token)
  45. def _transaction_operation(self, operation):
  46. token = db_operation.set(operation)
  47. try:
  48. return getattr(super(), operation)()
  49. except Exception as error:
  50. log_query_error(error, operation=operation)
  51. raise
  52. finally:
  53. db_operation.reset(token)
  54. def commit(self):
  55. return self._transaction_operation("commit")
  56. def rollback(self):
  57. return self._transaction_operation("rollback")
  58. def flush(self, objects=None):
  59. token = db_operation.set("flush")
  60. try:
  61. return super().flush(objects)
  62. except Exception as error:
  63. log_query_error(error, operation="flush")
  64. raise
  65. finally:
  66. db_operation.reset(token)
  67. engine = create_engine(
  68. settings.DATABASE_URL,
  69. connect_args={"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {},
  70. hide_parameters=True,
  71. )
  72. install_engine_logging(engine)
  73. SessionLocal = sessionmaker(bind=engine, class_=LoggingSession, autoflush=False, autocommit=False)
  74. Base = declarative_base()
  75. def get_session():
  76. db = SessionLocal()
  77. try:
  78. yield db
  79. finally:
  80. db.close()