-
Notifications
You must be signed in to change notification settings - Fork 549
feat(integrations): Add integration for asyncpg #2314
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
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
00d6ad9
feat(integrations): Add integration for asyncpg >= 0.23.0 (WIP)
mimre25 4a028a2
Merge branch 'master' into asyncpg-integration
antonpirker 63b48b0
Added asyncpg to test matrix
antonpirker d982755
Added dependency for tests
antonpirker 0d45020
Added dependency for tests
antonpirker 9756b69
test(asyncpg-integration): Read test PG connection params from enviro…
mimre25 44ef217
feat(asyncpg-integration): Add recording of "executemany" information
mimre25 8f8227a
fix(asyncpg-integration): Fix recording of span durations for asyncpg
mimre25 7089e6d
feat(integrations): Allow installing sentry-sdk[asyncpg]
mimre25 3dbe886
feat(asyncpg-integration): Add proper spans for cursors
mimre25 de6d835
feat(asyncpg-integration): Record calls to execute without params
mimre25 661ca50
feat(asyncpg-integration): Add tracve recording for connect calls
mimre25 8fa3d03
refactor(asyncpg-integration): Extract duplicated code into function
mimre25 d5656f9
fix(typing): Fix type annotations for asyncpg integration
mimre25 00c3f97
Merge branch 'master' into asyncpg-integration
antonpirker 52aa3a0
Merge branch 'master' into asyncpg-integration
antonpirker 63b58dc
Added db span data
antonpirker c528904
Linting fix
antonpirker c64f039
Trying to remove ParamSpec
antonpirker fd1bf6b
Reformat
antonpirker d179931
Fixed typing and more db span data recording.
antonpirker ea524dd
Removed syntax not known to python 2
antonpirker 97d9eab
Merge branch 'master' into asyncpg-integration
antonpirker bd75d0a
Merge branch 'master' into asyncpg-integration
antonpirker 07161a1
fix(tests): Fix expected results for asyncpg connect tests
mimre25 f04a9c7
fix(asyncpg-integration): Remove recording of every cursor execute
mimre25 4c977dd
fix(typing): Import __future__ annotations to allow modern type hints
mimre25 47aa451
fix(typing): Fix type hints for asyncpg integration
mimre25 9715829
Merge branch 'master' into asyncpg-integration
antonpirker 89c1023
Added postgres to asyncpg tests
antonpirker 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
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 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,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 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,3 @@ | ||
import pytest | ||
|
||
pytest.importorskip("asyncpg") |
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.