-
Notifications
You must be signed in to change notification settings - Fork 607
feat(integrations): Add integration for aiomysql
#4703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tonal
wants to merge
20
commits into
getsentry:master
Choose a base branch
from
tonal:patch-2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
878dc5b
integration for aiomysql
tonal 29b4e85
Drop _wrap_cursor_creation in integration aiomysql.py
tonal 28e79b6
Potential KeyError in _wrap_connect
tonal 950032f
Merge branch 'getsentry:master' into patch-2
tonal 421abfa
Merge branch 'master' into patch-2
tonal ab35ef8
Merge branch 'master' into patch-2
antonpirker c09eb44
Merge branch 'master' into patch-2
tonal b3b04a1
Merge branch 'master' into patch-2
tonal d9d7282
Merge branch 'master' into patch-2
tonal faf1a87
Merge branch 'getsentry:master' into patch-2
tonal 89b8669
feat(aiomysql): Add aiomysql integration
tonal ad11c32
fix(aiomysql): Address PR review comments
tonal e25d3ad
Merge remote-tracking branch 'origin/patch-2' into patch-2
tonal 5f26753
fix(aiomysql): Remove duplicate MySQL service block from Jinja template
tonal a7319c9
fix(aiomysql): Use public cursor.connection instead of private _conne…
tonal 5d39b16
fix(aiomysql): Use explicit signature in _wrap_connect to handle posi…
tonal facbe7b
Merge branch 'master' into patch-2
tonal 3d85885
fix(aiomysql): Format code and add cryptography dep for MySQL 8.0 auth
tonal 6e05b78
fix(aiomysql): Instrument Connection._connect to cover create_pool an…
tonal 308571f
fix(aiomysql): Reuse _set_db_data in connect span for consistent null…
tonal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, TypeVar, Callable, Awaitable | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk.consts import OP, SPANDATA | ||
| from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable | ||
| from sentry_sdk.tracing_utils import add_query_source, record_sql_queries | ||
| from sentry_sdk.utils import ( | ||
| capture_internal_exceptions, | ||
| parse_version, | ||
| ) | ||
|
|
||
| try: | ||
| import aiomysql # type: ignore[import-untyped] | ||
| from aiomysql.connection import Connection # type: ignore[import-untyped] | ||
| from aiomysql.cursors import Cursor # type: ignore[import-untyped] | ||
| except ImportError: | ||
| raise DidNotEnable("aiomysql not installed.") | ||
|
|
||
|
|
||
| class AioMySQLIntegration(Integration): | ||
| identifier = "aiomysql" | ||
| origin = f"auto.db.{identifier}" | ||
| _record_params = False | ||
|
|
||
| def __init__(self, *, record_params: bool = False): | ||
| AioMySQLIntegration._record_params = record_params | ||
|
|
||
| @staticmethod | ||
| def setup_once() -> None: | ||
| aiomysql_version = parse_version(aiomysql.__version__) | ||
| _check_minimum_version(AioMySQLIntegration, aiomysql_version) | ||
|
|
||
| Cursor.execute = _wrap_execute(Cursor.execute) | ||
| Cursor.executemany = _wrap_executemany(Cursor.executemany) | ||
|
|
||
| # Patch Connection._connect — this catches ALL connections: | ||
| # - aiomysql.connect() | ||
| # - aiomysql.create_pool() (pool.py does `from .connection import connect` | ||
| # which ultimately calls Connection._connect) | ||
| # - Reconnects | ||
| Connection._connect = _wrap_connect(Connection._connect) | ||
|
|
||
|
|
||
| T = TypeVar("T") | ||
|
|
||
|
|
||
| def _normalize_query(query: str | bytes | bytearray) -> str: | ||
| if isinstance(query, (bytes, bytearray)): | ||
| query = query.decode("utf-8", errors="replace") | ||
| return " ".join(query.split()) | ||
|
|
||
|
|
||
| def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: | ||
| """Wrap Cursor.execute to capture SQL queries.""" | ||
|
|
||
| async def _inner(*args: Any, **kwargs: Any) -> T: | ||
| if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: | ||
| return await f(*args, **kwargs) | ||
|
|
||
| cursor = args[0] | ||
|
|
||
| # Skip if flagged by executemany (avoids double-recording). | ||
| # Do NOT reset the flag here — it must stay True for the entire | ||
| # duration of executemany, which may call execute multiple times | ||
| # in a loop (non-INSERT fallback). Only _wrap_executemany's | ||
| # finally block should clear it. | ||
| if getattr(cursor, "_sentry_skip_next_execute", False): | ||
| return await f(*args, **kwargs) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| query = args[1] if len(args) > 1 else kwargs.get("query", "") | ||
| query_str = _normalize_query(query) | ||
| params = args[2] if len(args) > 2 else kwargs.get("args") | ||
|
|
||
| conn = _get_connection(cursor) | ||
|
|
||
| integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) | ||
| params_list = params if integration and integration._record_params else None | ||
| param_style = "pyformat" if params_list else None | ||
|
|
||
| with record_sql_queries( | ||
| cursor=None, | ||
| query=query_str, | ||
| params_list=params_list, | ||
| paramstyle=param_style, | ||
| executemany=False, | ||
| span_origin=AioMySQLIntegration.origin, | ||
| ) as span: | ||
| if conn: | ||
| _set_db_data(span, conn) | ||
| res = await f(*args, **kwargs) | ||
|
|
||
| with capture_internal_exceptions(): | ||
| add_query_source(span) | ||
|
|
||
| return res | ||
|
|
||
| return _inner | ||
|
|
||
|
|
||
| def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: | ||
| """Wrap Cursor.executemany to capture SQL queries.""" | ||
|
|
||
| async def _inner(*args: Any, **kwargs: Any) -> T: | ||
| if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: | ||
| return await f(*args, **kwargs) | ||
|
|
||
| cursor = args[0] | ||
| query = args[1] if len(args) > 1 else kwargs.get("query", "") | ||
| query_str = _normalize_query(query) | ||
| seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") | ||
|
|
||
| conn = _get_connection(cursor) | ||
|
|
||
| integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) | ||
| params_list = ( | ||
| seq_of_params if integration and integration._record_params else None | ||
| ) | ||
| param_style = "pyformat" if params_list else None | ||
|
|
||
| # Prevent double-recording: _do_execute_many calls self.execute internally | ||
| cursor._sentry_skip_next_execute = True | ||
| try: | ||
| with record_sql_queries( | ||
| cursor=None, | ||
| query=query_str, | ||
| params_list=params_list, | ||
| paramstyle=param_style, | ||
| executemany=True, | ||
| span_origin=AioMySQLIntegration.origin, | ||
| ) as span: | ||
| if conn: | ||
| _set_db_data(span, conn) | ||
| res = await f(*args, **kwargs) | ||
|
|
||
| with capture_internal_exceptions(): | ||
| add_query_source(span) | ||
|
|
||
| return res | ||
| finally: | ||
| cursor._sentry_skip_next_execute = False | ||
|
|
||
| return _inner | ||
|
|
||
|
|
||
| def _get_connection(cursor: Any) -> Any: | ||
| """Get the underlying connection from a cursor.""" | ||
| return getattr(cursor, "connection", None) | ||
|
|
||
|
|
||
| def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: | ||
| """Wrap Connection._connect to capture connection spans.""" | ||
|
|
||
| async def _inner(self: "Connection") -> T: | ||
| if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: | ||
| return await f(self) | ||
|
|
||
| with sentry_sdk.start_span( | ||
| op=OP.DB, | ||
| name="connect", | ||
| origin=AioMySQLIntegration.origin, | ||
| ) as span: | ||
| _set_db_data(span, self) | ||
|
|
||
| with capture_internal_exceptions(): | ||
| sentry_sdk.add_breadcrumb( | ||
| message="connect", | ||
| category="query", | ||
| data=span._data, | ||
| ) | ||
| res = await f(self) | ||
|
|
||
| return res | ||
|
|
||
| return _inner | ||
|
|
||
|
|
||
| def _set_db_data(span: Any, conn: Any) -> None: | ||
| """Set database-related span data from connection object.""" | ||
| span.set_data(SPANDATA.DB_SYSTEM, "mysql") | ||
|
|
||
| host = getattr(conn, "host", None) | ||
| if host is not None: | ||
| span.set_data(SPANDATA.SERVER_ADDRESS, host) | ||
|
|
||
| port = getattr(conn, "port", None) | ||
| if port is not None: | ||
| span.set_data(SPANDATA.SERVER_PORT, port) | ||
|
|
||
| database = getattr(conn, "db", None) | ||
| if database is not None: | ||
| span.set_data(SPANDATA.DB_NAME, database) | ||
|
|
||
| user = getattr(conn, "user", None) | ||
| if user is not None: | ||
| span.set_data(SPANDATA.DB_USER, user) | ||
|
sentry[bot] marked this conversation as resolved.
cursor[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import pytest | ||
|
|
||
| pytest.importorskip("aiomysql") | ||
| pytest.importorskip("pytest_asyncio") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.