-
Notifications
You must be signed in to change notification settings - Fork 505
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(integrations): Add integration for asyncpg (#2314)
So far this records every statement that is directly issued, as well as the SQL statements that are used for cursors and prepared statements.
- Loading branch information
Showing
9 changed files
with
788 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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,102 @@ | ||
name: Test asyncpg | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
- release/** | ||
|
||
pull_request: | ||
|
||
# Cancel in progress workflows on pull_requests. | ||
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||
cancel-in-progress: true | ||
|
||
permissions: | ||
contents: read | ||
|
||
env: | ||
BUILD_CACHE_KEY: ${{ github.sha }} | ||
CACHED_BUILD_PATHS: | | ||
${{ github.workspace }}/dist-serverless | ||
jobs: | ||
test: | ||
name: asyncpg, python ${{ matrix.python-version }}, ${{ matrix.os }} | ||
runs-on: ${{ matrix.os }} | ||
timeout-minutes: 30 | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
python-version: ["3.7","3.8","3.9","3.10","3.11"] | ||
# python3.6 reached EOL and is no longer being supported on | ||
# new versions of hosted runners on Github Actions | ||
# ubuntu-20.04 is the last version that supported python3.6 | ||
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 | ||
os: [ubuntu-20.04] | ||
services: | ||
postgres: | ||
image: postgres | ||
env: | ||
POSTGRES_PASSWORD: sentry | ||
# Set health checks to wait until postgres has started | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval 10s | ||
--health-timeout 5s | ||
--health-retries 5 | ||
# Maps tcp port 5432 on service container to the host | ||
ports: | ||
- 5432:5432 | ||
env: | ||
SENTRY_PYTHON_TEST_POSTGRES_USER: postgres | ||
SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry | ||
SENTRY_PYTHON_TEST_POSTGRES_NAME: ci_test | ||
SENTRY_PYTHON_TEST_POSTGRES_HOST: localhost | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
|
||
- name: Setup Test Env | ||
run: | | ||
pip install coverage "tox>=3,<4" | ||
- name: Test asyncpg | ||
uses: nick-fields/retry@v2 | ||
with: | ||
timeout_minutes: 15 | ||
max_attempts: 2 | ||
retry_wait_seconds: 5 | ||
shell: bash | ||
command: | | ||
set -x # print commands that are executed | ||
coverage erase | ||
# Run tests | ||
./scripts/runtox.sh "py${{ matrix.python-version }}-asyncpg" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch && | ||
coverage combine .coverage* && | ||
coverage xml -i | ||
- uses: codecov/codecov-action@v3 | ||
with: | ||
token: ${{ secrets.CODECOV_TOKEN }} | ||
files: coverage.xml | ||
|
||
|
||
check_required_tests: | ||
name: All asyncpg tests passed or skipped | ||
needs: test | ||
# Always run this, even if a dependent job failed | ||
if: always() | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- name: Check for failures | ||
if: contains(needs.test.result, 'failure') | ||
run: | | ||
echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 |
This file contains 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 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 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,202 @@ | ||
from __future__ import annotations | ||
import contextlib | ||
from typing import Any, TypeVar, Callable, Awaitable, Iterator | ||
|
||
from asyncpg.cursor import BaseCursor # type: ignore | ||
|
||
from sentry_sdk import Hub | ||
from sentry_sdk.consts import OP, SPANDATA | ||
from sentry_sdk.integrations import Integration, DidNotEnable | ||
from sentry_sdk.tracing import Span | ||
from sentry_sdk.tracing_utils import record_sql_queries | ||
from sentry_sdk.utils import parse_version, capture_internal_exceptions | ||
|
||
try: | ||
import asyncpg # type: ignore[import] | ||
|
||
except ImportError: | ||
raise DidNotEnable("asyncpg not installed.") | ||
|
||
# asyncpg.__version__ is a string containing the semantic version in the form of "<major>.<minor>.<patch>" | ||
asyncpg_version = parse_version(asyncpg.__version__) | ||
|
||
if asyncpg_version is not None and asyncpg_version < (0, 23, 0): | ||
raise DidNotEnable("asyncpg >= 0.23.0 required") | ||
|
||
|
||
class AsyncPGIntegration(Integration): | ||
identifier = "asyncpg" | ||
_record_params = False | ||
|
||
def __init__(self, *, record_params: bool = False): | ||
AsyncPGIntegration._record_params = record_params | ||
|
||
@staticmethod | ||
def setup_once() -> None: | ||
asyncpg.Connection.execute = _wrap_execute( | ||
asyncpg.Connection.execute, | ||
) | ||
|
||
asyncpg.Connection._execute = _wrap_connection_method( | ||
asyncpg.Connection._execute | ||
) | ||
asyncpg.Connection._executemany = _wrap_connection_method( | ||
asyncpg.Connection._executemany, executemany=True | ||
) | ||
asyncpg.Connection.cursor = _wrap_cursor_creation(asyncpg.Connection.cursor) | ||
asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) | ||
asyncpg.connect_utils._connect_addr = _wrap_connect_addr( | ||
asyncpg.connect_utils._connect_addr | ||
) | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: | ||
async def _inner(*args: Any, **kwargs: Any) -> T: | ||
hub = Hub.current | ||
integration = hub.get_integration(AsyncPGIntegration) | ||
|
||
# Avoid recording calls to _execute twice. | ||
# Calls to Connection.execute with args also call | ||
# Connection._execute, which is recorded separately | ||
# args[0] = the connection object, args[1] is the query | ||
if integration is None or len(args) > 2: | ||
return await f(*args, **kwargs) | ||
|
||
query = args[1] | ||
with record_sql_queries(hub, None, query, None, None, executemany=False): | ||
res = await f(*args, **kwargs) | ||
return res | ||
|
||
return _inner | ||
|
||
|
||
SubCursor = TypeVar("SubCursor", bound=BaseCursor) | ||
|
||
|
||
@contextlib.contextmanager | ||
def _record( | ||
hub: Hub, | ||
cursor: SubCursor | None, | ||
query: str, | ||
params_list: tuple[Any, ...] | None, | ||
*, | ||
executemany: bool = False, | ||
) -> Iterator[Span]: | ||
integration = hub.get_integration(AsyncPGIntegration) | ||
if not integration._record_params: | ||
params_list = None | ||
|
||
param_style = "pyformat" if params_list else None | ||
|
||
with record_sql_queries( | ||
hub, | ||
cursor, | ||
query, | ||
params_list, | ||
param_style, | ||
executemany=executemany, | ||
record_cursor_repr=cursor is not None, | ||
) as span: | ||
yield span | ||
|
||
|
||
def _wrap_connection_method( | ||
f: Callable[..., Awaitable[T]], *, executemany: bool = False | ||
) -> Callable[..., Awaitable[T]]: | ||
async def _inner(*args: Any, **kwargs: Any) -> T: | ||
hub = Hub.current | ||
integration = hub.get_integration(AsyncPGIntegration) | ||
|
||
if integration is None: | ||
return await f(*args, **kwargs) | ||
|
||
query = args[1] | ||
params_list = args[2] if len(args) > 2 else None | ||
with _record(hub, None, query, params_list, executemany=executemany) as span: | ||
_set_db_data(span, args[0]) | ||
res = await f(*args, **kwargs) | ||
return res | ||
|
||
return _inner | ||
|
||
|
||
def _wrap_cursor_creation(f: Callable[..., T]) -> Callable[..., T]: | ||
def _inner(*args: Any, **kwargs: Any) -> T: # noqa: N807 | ||
hub = Hub.current | ||
integration = hub.get_integration(AsyncPGIntegration) | ||
|
||
if integration is None: | ||
return f(*args, **kwargs) | ||
|
||
query = args[1] | ||
params_list = args[2] if len(args) > 2 else None | ||
|
||
with _record( | ||
hub, | ||
None, | ||
query, | ||
params_list, | ||
executemany=False, | ||
) as span: | ||
_set_db_data(span, args[0]) | ||
res = f(*args, **kwargs) | ||
span.set_data("db.cursor", res) | ||
|
||
return res | ||
|
||
return _inner | ||
|
||
|
||
def _wrap_connect_addr(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: | ||
async def _inner(*args: Any, **kwargs: Any) -> T: | ||
hub = Hub.current | ||
integration = hub.get_integration(AsyncPGIntegration) | ||
|
||
if integration is None: | ||
return await f(*args, **kwargs) | ||
|
||
user = kwargs["params"].user | ||
database = kwargs["params"].database | ||
|
||
with hub.start_span(op=OP.DB, description="connect") as span: | ||
span.set_data(SPANDATA.DB_SYSTEM, "postgresql") | ||
addr = kwargs.get("addr") | ||
if addr: | ||
try: | ||
span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) | ||
span.set_data(SPANDATA.SERVER_PORT, addr[1]) | ||
except IndexError: | ||
pass | ||
span.set_data(SPANDATA.DB_NAME, database) | ||
span.set_data(SPANDATA.DB_USER, user) | ||
|
||
with capture_internal_exceptions(): | ||
hub.add_breadcrumb(message="connect", category="query", data=span._data) | ||
res = await f(*args, **kwargs) | ||
|
||
return res | ||
|
||
return _inner | ||
|
||
|
||
def _set_db_data(span: Span, conn: Any) -> None: | ||
span.set_data(SPANDATA.DB_SYSTEM, "postgresql") | ||
|
||
addr = conn._addr | ||
if addr: | ||
try: | ||
span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) | ||
span.set_data(SPANDATA.SERVER_PORT, addr[1]) | ||
except IndexError: | ||
pass | ||
|
||
database = conn._params.database | ||
if database: | ||
span.set_data(SPANDATA.DB_NAME, database) | ||
|
||
user = conn._params.user | ||
if user: | ||
span.set_data(SPANDATA.DB_USER, user) |
This file contains 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 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 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,3 @@ | ||
import pytest | ||
|
||
pytest.importorskip("asyncpg") |
Oops, something went wrong.