wrapture-instrumentation-postgresql 1.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wrapture_instrumentation_postgresql/__init__.py +26 -0
- wrapture_instrumentation_postgresql/asyncpg/README.md +136 -0
- wrapture_instrumentation_postgresql/asyncpg/__init__.py +75 -0
- wrapture_instrumentation_postgresql/asyncpg/connection.py +220 -0
- wrapture_instrumentation_postgresql/asyncpg/cursor.py +88 -0
- wrapture_instrumentation_postgresql/asyncpg/prepared.py +74 -0
- wrapture_instrumentation_postgresql/common.py +169 -0
- wrapture_instrumentation_postgresql/psycopg/README.md +144 -0
- wrapture_instrumentation_postgresql/psycopg/__init__.py +57 -0
- wrapture_instrumentation_postgresql/psycopg/connection.py +168 -0
- wrapture_instrumentation_postgresql/psycopg/cursor.py +258 -0
- wrapture_instrumentation_postgresql/psycopg/transaction.py +126 -0
- wrapture_instrumentation_postgresql/psycopg2/README.md +131 -0
- wrapture_instrumentation_postgresql/psycopg2/__init__.py +49 -0
- wrapture_instrumentation_postgresql/psycopg2/factories.py +402 -0
- wrapture_instrumentation_postgresql/py.typed +0 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/METADATA +155 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/RECORD +22 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/WHEEL +5 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/entry_points.txt +4 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/licenses/LICENSE +24 -0
- wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instrumentation for the PostgreSQL client libraries, applied through
|
|
3
|
+
wrapture.
|
|
4
|
+
|
|
5
|
+
Each target lives in its own subpackage here (psycopg and psycopg2
|
|
6
|
+
now, asyncpg later), holding one wrapture.Instrumentation subclass
|
|
7
|
+
registered in the wrapture.instrumentation entry point group under the
|
|
8
|
+
bare target name. This module carries only the version: importing it
|
|
9
|
+
loads no instrumentation and no target.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _format_version(parts: tuple[str, ...]) -> str:
|
|
14
|
+
base = ".".join(parts[:3])
|
|
15
|
+
|
|
16
|
+
if len(parts) == 3:
|
|
17
|
+
return base
|
|
18
|
+
|
|
19
|
+
suffix = parts[3]
|
|
20
|
+
return (
|
|
21
|
+
f"{base}.{suffix}" if suffix.startswith(("dev", "post")) else f"{base}{suffix}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__version_info__ = ("1", "0", "0", "dev1")
|
|
26
|
+
__version__ = _format_version(__version_info__)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# asyncpg instrumentation
|
|
2
|
+
|
|
3
|
+
Query and transaction tracing for
|
|
4
|
+
[asyncpg](https://magicstack.github.io/asyncpg/), the asyncio
|
|
5
|
+
PostgreSQL client. Entry point name `asyncpg`, the package it
|
|
6
|
+
patches; supports asyncpg 0.29 and later, below 1.0; fully
|
|
7
|
+
removable.
|
|
8
|
+
|
|
9
|
+
## Enabling it
|
|
10
|
+
|
|
11
|
+
An `[[instrument]]` entry in `wrapture.toml` (with at least one sink
|
|
12
|
+
to hear the events):
|
|
13
|
+
|
|
14
|
+
```toml
|
|
15
|
+
[[instrument]]
|
|
16
|
+
name = "asyncpg"
|
|
17
|
+
|
|
18
|
+
[[sink]]
|
|
19
|
+
type = "printer"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
run under wrapture's runner (`python -m wrapture -m myapp`), or in a
|
|
23
|
+
test through the context manager:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
with wrapture.instrumentation("asyncpg"):
|
|
27
|
+
...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What you see
|
|
31
|
+
|
|
32
|
+
One `database` leaf per operation, recorded around its await: the
|
|
33
|
+
connection being opened, each query through a connection's `execute`,
|
|
34
|
+
`executemany`, `fetch`, `fetchrow`, `fetchval` or `fetchmany`, each
|
|
35
|
+
of a prepared statement's own fetches, each round trip a server-side
|
|
36
|
+
cursor makes, each COPY, and each transaction boundary:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
asyncpg:connect() -> '<Connection>'
|
|
40
|
+
asyncpg.connection:Connection.execute(query='<6 chars>', args='<0 values>', timeout=None) -> '<str>'
|
|
41
|
+
asyncpg.connection:Connection.fetch(query='<43 chars>', args='<1 values>', timeout=None, record_class=None) -> '<list>'
|
|
42
|
+
asyncpg.prepared_stmt:PreparedStatement.fetchval(args='<1 values>', column=0, timeout=None) -> '<int>'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- asyncpg's public surface is pure Python coroutine methods, so the
|
|
46
|
+
instrumentation binds them in place on the classes and records
|
|
47
|
+
each around its await; the Cython protocol beneath does the socket
|
|
48
|
+
work and is where the seams stop. There is no callback-style
|
|
49
|
+
completion to make spans overlap: one call, one span, closed when
|
|
50
|
+
the await returns or raises. Two tasks querying at once give
|
|
51
|
+
sibling spans that overlap in wall time, which is what they are.
|
|
52
|
+
|
|
53
|
+
- Every event carries the database contract keys `system`
|
|
54
|
+
(`postgresql`) and `operation` (the SQL's leading keyword, or
|
|
55
|
+
`CONNECT`, `DECLARE`, `FETCH`, `COPY`, `EXPLAIN`), which
|
|
56
|
+
wrapture's OpenTelemetry export maps to `db.system.name` and
|
|
57
|
+
`db.operation.name`, plus the `database`, `host` and `port` the
|
|
58
|
+
connection reached. A COPY to or from a table names it as
|
|
59
|
+
`collection`.
|
|
60
|
+
|
|
61
|
+
- Transactions need nothing of their own: `async with
|
|
62
|
+
conn.transaction():` issues its BEGIN, COMMIT, ROLLBACK, SAVEPOINT,
|
|
63
|
+
RELEASE SAVEPOINT and ROLLBACK TO through `Connection.execute`, so
|
|
64
|
+
each boundary is an ordinary execute event with its own operation.
|
|
65
|
+
|
|
66
|
+
- A prepared statement (`await conn.prepare(query)`) records each of
|
|
67
|
+
its `fetch`, `fetchrow`, `fetchval`, `fetchmany`, `executemany` and
|
|
68
|
+
`explain` calls, with the statement's SQL; the `prepare` itself is
|
|
69
|
+
not a query and is not recorded.
|
|
70
|
+
|
|
71
|
+
- A server-side cursor (`conn.cursor(query)` inside a transaction,
|
|
72
|
+
iterated with `async for` or driven by `fetch`, `fetchrow` and
|
|
73
|
+
`forward`) never passes through the connection's public methods,
|
|
74
|
+
so its round trips are recorded where they happen: one `DECLARE`
|
|
75
|
+
event when the cursor is bound (with the first batch of rows, for
|
|
76
|
+
an iteration), one `FETCH` event per batch fetched, and a `MOVE`
|
|
77
|
+
for a `forward`. The paths name asyncpg's private `BaseCursor`
|
|
78
|
+
methods, since those are the round trips.
|
|
79
|
+
|
|
80
|
+
- The capture policy is deliberate about sensitive data: query
|
|
81
|
+
arguments are never recorded, under any setting (they reduce to a
|
|
82
|
+
count); the SQL text reduces to its length unless the `statement`
|
|
83
|
+
setting is on; a COPY's records, source and output reduce to a
|
|
84
|
+
count or a type; and the connect event captures none of its
|
|
85
|
+
arguments, which carry the password. asyncpg sends arguments
|
|
86
|
+
server-side, so the recorded text always carries `$1` placeholders
|
|
87
|
+
rather than data, unless the application interpolated its own.
|
|
88
|
+
|
|
89
|
+
- A failing statement records the driver's exception
|
|
90
|
+
(`asyncpg.UndefinedTableError`, say) on the event as it escapes; a
|
|
91
|
+
timeout or cancellation arrives the same way. A pool made after the
|
|
92
|
+
instrumentation is applied opens its connections through the
|
|
93
|
+
bound `connect`, on the task that awaited it, so those record too.
|
|
94
|
+
|
|
95
|
+
## Settings
|
|
96
|
+
|
|
97
|
+
| Setting | Default | Controls |
|
|
98
|
+
| ------- | ------- | -------- |
|
|
99
|
+
| `statement` | `false` | Whether each query event records the SQL text as handed to the driver, as `statement`. Off by default because the driver cannot tell a literal an application interpolated from a placeholder; turn it on when your queries are parameterized, the text then carrying `$n` placeholders rather than data. Query arguments are never recorded either way. |
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
[[instrument]]
|
|
103
|
+
name = "asyncpg"
|
|
104
|
+
statement = true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## With the sqlalchemy instrumentation
|
|
108
|
+
|
|
109
|
+
An instrumented asyncpg beneath the core package's `sqlalchemy`
|
|
110
|
+
target (the `postgresql+asyncpg` async engine) composes through that
|
|
111
|
+
target's `leaf` setting. With the default `leaf = true` each
|
|
112
|
+
statement is one event and the driver's own events stay out of the
|
|
113
|
+
tree; with `leaf = false` they nest beneath each statement. The
|
|
114
|
+
asyncpg dialect prepares every statement and fetches through the
|
|
115
|
+
prepared statement, so what appears beneath `do_execute` is the
|
|
116
|
+
prepared statement's own fetch, and the commit's `execute` beneath
|
|
117
|
+
`_commit_impl`. Raw asyncpg use beside the engine records at the top
|
|
118
|
+
level either way, and the dialect's setup queries show up beside the
|
|
119
|
+
tree regardless, since they run outside the recorded seams.
|
|
120
|
+
|
|
121
|
+
## Known gap
|
|
122
|
+
|
|
123
|
+
A connection taken from a pool (`async with pool.acquire() as conn:`)
|
|
124
|
+
does not yet record its queries: asyncpg's pool proxy calls the
|
|
125
|
+
connection's methods through the class, `Connection.fetchval(
|
|
126
|
+
connection, query)`, a calling convention wrapture's signature check
|
|
127
|
+
does not yet handle. A fix is on wrapture's side and this note goes
|
|
128
|
+
when it lands. The pool's connects record, and so does everything on
|
|
129
|
+
a connection you opened yourself.
|
|
130
|
+
|
|
131
|
+
## How it patches
|
|
132
|
+
|
|
133
|
+
For the implementation detail see the module docstrings of
|
|
134
|
+
[connection.py](connection.py) (connect and the Connection methods),
|
|
135
|
+
[prepared.py](prepared.py) (prepared statements) and
|
|
136
|
+
[cursor.py](cursor.py) (server-side cursors).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Instrumentation for asyncpg: every query, the connections opened
|
|
2
|
+
and the transaction boundaries recorded as database events, by
|
|
3
|
+
bindings on the pure Python classes the driver is made of, each
|
|
4
|
+
recording around its await.
|
|
5
|
+
|
|
6
|
+
This module imports only wrapture. Everything that touches asyncpg
|
|
7
|
+
lives in the sibling modules, one per asyncpg module patched
|
|
8
|
+
(connection.py for asyncpg and asyncpg.connection, prepared.py for
|
|
9
|
+
asyncpg.prepared_stmt, cursor.py for asyncpg.cursor), each importing
|
|
10
|
+
only wrapture at top level and reaching asyncpg through the module
|
|
11
|
+
the hook is handed, so loading this class when a config loads never
|
|
12
|
+
imports asyncpg ahead of the hooks meant to fire on its import.
|
|
13
|
+
|
|
14
|
+
Importing asyncpg initialises every one of those modules, so all four
|
|
15
|
+
hooks fire from the one import; each binds through the module it is
|
|
16
|
+
handed and cleans up its own bindings.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import wrapture
|
|
24
|
+
from wrapture import Setting
|
|
25
|
+
|
|
26
|
+
from . import connection, cursor, prepared
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncpgInstrumentation(wrapture.Instrumentation):
|
|
30
|
+
"""Query and transaction tracing for asyncpg."""
|
|
31
|
+
|
|
32
|
+
description = "Query and transaction tracing for asyncpg."
|
|
33
|
+
|
|
34
|
+
target = "asyncpg"
|
|
35
|
+
supports = ">=0.29,<1"
|
|
36
|
+
removable = True
|
|
37
|
+
|
|
38
|
+
settings = {
|
|
39
|
+
"statement": Setting(
|
|
40
|
+
False,
|
|
41
|
+
"record the SQL text as handed to the driver on each query"
|
|
42
|
+
" event; off by default because the driver cannot tell a"
|
|
43
|
+
" literal an application interpolated from a placeholder,"
|
|
44
|
+
" and the text is only safe to record when queries are"
|
|
45
|
+
" parameterized",
|
|
46
|
+
),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@wrapture.instrumentation_hook("asyncpg")
|
|
50
|
+
def asyncpg(self, name: str, module: Any) -> None:
|
|
51
|
+
"""Bind the package's own spelling of connect once asyncpg
|
|
52
|
+
exists."""
|
|
53
|
+
|
|
54
|
+
connection.instrument_package(module, self)
|
|
55
|
+
|
|
56
|
+
@wrapture.instrumentation_hook("asyncpg.connection")
|
|
57
|
+
def asyncpg_connection(self, name: str, module: Any) -> None:
|
|
58
|
+
"""Bind connect and the Connection methods once
|
|
59
|
+
asyncpg.connection exists."""
|
|
60
|
+
|
|
61
|
+
connection.instrument(module, self)
|
|
62
|
+
|
|
63
|
+
@wrapture.instrumentation_hook("asyncpg.prepared_stmt")
|
|
64
|
+
def asyncpg_prepared_stmt(self, name: str, module: Any) -> None:
|
|
65
|
+
"""Bind the prepared statement's own fetch family once
|
|
66
|
+
asyncpg.prepared_stmt exists."""
|
|
67
|
+
|
|
68
|
+
prepared.instrument(module, self)
|
|
69
|
+
|
|
70
|
+
@wrapture.instrumentation_hook("asyncpg.cursor")
|
|
71
|
+
def asyncpg_cursor(self, name: str, module: Any) -> None:
|
|
72
|
+
"""Bind the server-side cursor round trips once asyncpg.cursor
|
|
73
|
+
exists."""
|
|
74
|
+
|
|
75
|
+
cursor.instrument(module, self)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""The connection seams: connections being opened, and Connection's
|
|
2
|
+
execute, fetch and COPY families, recorded as database events around
|
|
3
|
+
their awaits.
|
|
4
|
+
|
|
5
|
+
asyncpg's public surface is pure Python coroutine methods on
|
|
6
|
+
`Connection`, so each is bound in place on the class and records
|
|
7
|
+
around its await: `execute` (with or without arguments, a script of
|
|
8
|
+
several statements included), `executemany`, `fetch`, `fetchrow`,
|
|
9
|
+
`fetchval`, `fetchmany` (0.30 and later), and the four COPY methods.
|
|
10
|
+
The Cython protocol beneath them does the socket work and is where
|
|
11
|
+
the seams stop. A `PoolConnectionProxy` forwards each of these to the
|
|
12
|
+
real Connection's class attribute, so pooled connections record
|
|
13
|
+
through the same bindings.
|
|
14
|
+
|
|
15
|
+
`asyncpg.connection.connect` is the coroutine function every
|
|
16
|
+
connection comes from; `asyncpg.connect` is the same function under
|
|
17
|
+
the package's name, bound at import, so it is bound as a module
|
|
18
|
+
attribute too, as sqlite3's two spellings of `connect` are. A pool
|
|
19
|
+
looks `connection.connect` up when it is created, so a pool made after
|
|
20
|
+
the instrumentation is applied opens its connections through the
|
|
21
|
+
binding, on the task that awaits it. The connect event's arguments are
|
|
22
|
+
never captured (the dsn and the keyword form carry the password); the
|
|
23
|
+
server reached is annotated from the connection afterwards.
|
|
24
|
+
|
|
25
|
+
Transactions need no bindings of their own: `Transaction.start()`,
|
|
26
|
+
its commit and its rollback issue BEGIN, COMMIT, ROLLBACK, SAVEPOINT,
|
|
27
|
+
RELEASE SAVEPOINT and ROLLBACK TO through `Connection.execute`, so
|
|
28
|
+
each boundary records as a statement event with its own operation.
|
|
29
|
+
|
|
30
|
+
Every event carries the database contract keys and the server the
|
|
31
|
+
connection reached, which asyncpg keeps privately (`_params.database`
|
|
32
|
+
and `_addr`, a host and port pair or a socket path, stable since its
|
|
33
|
+
0.1x line) and which are read through a guarded helper. The SQL text
|
|
34
|
+
rides as `statement` only when the setting is on; the query arguments
|
|
35
|
+
(asyncpg's `*args`, sent server-side) are never recorded and reduce
|
|
36
|
+
to a count.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
import wrapture
|
|
44
|
+
|
|
45
|
+
from ..common import SYSTEM, captured, statement_data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def server_of(connection: Any) -> dict[str, Any]:
|
|
49
|
+
"""The `database`, `host` and `port` keys for an asyncpg
|
|
50
|
+
connection, from what it keeps privately; whichever it can
|
|
51
|
+
supply."""
|
|
52
|
+
|
|
53
|
+
data: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
params = getattr(connection, "_params", None)
|
|
56
|
+
database = getattr(params, "database", None)
|
|
57
|
+
if isinstance(database, str) and database:
|
|
58
|
+
data["database"] = database
|
|
59
|
+
|
|
60
|
+
addr = getattr(connection, "_addr", None)
|
|
61
|
+
if isinstance(addr, tuple) and len(addr) == 2:
|
|
62
|
+
host, port = addr
|
|
63
|
+
if isinstance(host, str):
|
|
64
|
+
data["host"] = host
|
|
65
|
+
if isinstance(port, int):
|
|
66
|
+
data["port"] = port
|
|
67
|
+
elif isinstance(addr, str) and addr:
|
|
68
|
+
data["host"] = addr
|
|
69
|
+
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def query_of(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
|
74
|
+
"""The query a Connection method was handed: the first positional,
|
|
75
|
+
else the keyword the method spells it as."""
|
|
76
|
+
|
|
77
|
+
if args:
|
|
78
|
+
return args[0]
|
|
79
|
+
|
|
80
|
+
return kwargs.get("query", kwargs.get("command"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def database_binding(
|
|
84
|
+
owner: Any, name: str, capture_args: Any = captured
|
|
85
|
+
) -> wrapture.Binding:
|
|
86
|
+
"""A ready binding on one of the driver's methods, shared with the
|
|
87
|
+
prepared statement and cursor modules."""
|
|
88
|
+
|
|
89
|
+
return wrapture.binding(
|
|
90
|
+
owner,
|
|
91
|
+
name,
|
|
92
|
+
category="database",
|
|
93
|
+
leaf=True,
|
|
94
|
+
capture_args=capture_args,
|
|
95
|
+
capture_result=captured,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def opens_binding(owner: Any) -> wrapture.Binding:
|
|
100
|
+
"""The connect binding for either spelling of connect."""
|
|
101
|
+
|
|
102
|
+
async def opens(
|
|
103
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
104
|
+
) -> Any:
|
|
105
|
+
wrapture.annotate(system=SYSTEM, operation="CONNECT")
|
|
106
|
+
|
|
107
|
+
connection = await wrapped(*args, **kwargs)
|
|
108
|
+
wrapture.annotate(**server_of(connection))
|
|
109
|
+
|
|
110
|
+
return connection
|
|
111
|
+
|
|
112
|
+
binding = database_binding(owner, "connect", "none")
|
|
113
|
+
binding.on_call.decorates(opens)
|
|
114
|
+
|
|
115
|
+
return binding
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def instrument_package(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
119
|
+
"""Bind the package's own spelling of connect; register its
|
|
120
|
+
removal as this trigger's cleanup."""
|
|
121
|
+
|
|
122
|
+
group = wrapture.bindings(connect=opens_binding(module))
|
|
123
|
+
group.apply()
|
|
124
|
+
|
|
125
|
+
instrumentation.on_cleanup(group.remove)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
129
|
+
"""Bind connect and the Connection methods; register their removal
|
|
130
|
+
as this trigger's cleanup."""
|
|
131
|
+
|
|
132
|
+
settings = instrumentation.settings
|
|
133
|
+
record_statement = bool(settings["statement"])
|
|
134
|
+
|
|
135
|
+
def data_for(
|
|
136
|
+
connection: Any, query: Any, operation: str | None = None
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
return statement_data(
|
|
139
|
+
query, connection, server_of(connection), record_statement, operation
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def queries(
|
|
143
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
144
|
+
) -> Any:
|
|
145
|
+
async def record() -> Any:
|
|
146
|
+
wrapture.annotate(**data_for(instance, query_of(args, kwargs)))
|
|
147
|
+
|
|
148
|
+
return await wrapped(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
return record()
|
|
151
|
+
|
|
152
|
+
def copies_table(
|
|
153
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
154
|
+
) -> Any:
|
|
155
|
+
async def record() -> Any:
|
|
156
|
+
table = args[0] if args else kwargs.get("table_name")
|
|
157
|
+
data = data_for(instance, None, "COPY")
|
|
158
|
+
if isinstance(table, str):
|
|
159
|
+
data["collection"] = table
|
|
160
|
+
wrapture.annotate(**data)
|
|
161
|
+
|
|
162
|
+
return await wrapped(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
return record()
|
|
165
|
+
|
|
166
|
+
def copies_query(
|
|
167
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
168
|
+
) -> Any:
|
|
169
|
+
async def record() -> Any:
|
|
170
|
+
data = data_for(instance, query_of(args, kwargs), "COPY")
|
|
171
|
+
wrapture.annotate(**data)
|
|
172
|
+
|
|
173
|
+
# With arguments, asyncpg renders them into the query
|
|
174
|
+
# through a SELECT on this same connection before the COPY:
|
|
175
|
+
# a bound call beneath this leaf, silenced, whose own
|
|
176
|
+
# annotation would otherwise leave the event saying
|
|
177
|
+
# SELECT. The COPY's data is written again on the way out.
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
return await wrapped(*args, **kwargs)
|
|
181
|
+
finally:
|
|
182
|
+
wrapture.annotate(**data)
|
|
183
|
+
|
|
184
|
+
return record()
|
|
185
|
+
|
|
186
|
+
named: dict[str, wrapture.Binding] = {"connect": opens_binding(module)}
|
|
187
|
+
|
|
188
|
+
connection_class = module.Connection
|
|
189
|
+
|
|
190
|
+
# The execute and fetch family; fetchmany arrived in 0.30.
|
|
191
|
+
|
|
192
|
+
for method in (
|
|
193
|
+
"execute",
|
|
194
|
+
"executemany",
|
|
195
|
+
"fetch",
|
|
196
|
+
"fetchrow",
|
|
197
|
+
"fetchval",
|
|
198
|
+
"fetchmany",
|
|
199
|
+
):
|
|
200
|
+
if method not in vars(connection_class):
|
|
201
|
+
continue
|
|
202
|
+
bound = database_binding(connection_class, method)
|
|
203
|
+
bound.on_call.decorates(queries)
|
|
204
|
+
named[method] = bound
|
|
205
|
+
|
|
206
|
+
# COPY: three methods name a table, one takes a query.
|
|
207
|
+
|
|
208
|
+
for method in ("copy_from_table", "copy_to_table", "copy_records_to_table"):
|
|
209
|
+
bound = database_binding(connection_class, method)
|
|
210
|
+
bound.on_call.decorates(copies_table)
|
|
211
|
+
named[method] = bound
|
|
212
|
+
|
|
213
|
+
bound = database_binding(connection_class, "copy_from_query")
|
|
214
|
+
bound.on_call.decorates(copies_query)
|
|
215
|
+
named["copy_from_query"] = bound
|
|
216
|
+
|
|
217
|
+
group = wrapture.bindings(**named)
|
|
218
|
+
group.apply()
|
|
219
|
+
|
|
220
|
+
instrumentation.on_cleanup(group.remove)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The server-side cursor seams: each round trip a cursor makes to
|
|
2
|
+
its portal, recorded as a database event around its await.
|
|
3
|
+
|
|
4
|
+
`connection.cursor(query, *args)` inside a transaction gives a
|
|
5
|
+
server-side cursor, iterated with `async for` or driven by `fetch`,
|
|
6
|
+
`fetchrow` and `forward`. Nothing there passes through Connection's
|
|
7
|
+
public methods: the DECLARE happens inside the cursor's own bind, and
|
|
8
|
+
every FETCH inside its exec. With no binding at all a query run this
|
|
9
|
+
way would leave no event, so the three private methods on
|
|
10
|
+
`BaseCursor` that make the round trips are bound: `_bind_exec` (the
|
|
11
|
+
DECLARE with the first batch, what `async for` does), `_bind` (the
|
|
12
|
+
DECLARE alone, for a cursor awaited then driven by hand) and `_exec`
|
|
13
|
+
(each FETCH). One event per round trip, whichever public method drove
|
|
14
|
+
it; binding `__anext__` instead would make an event per buffered row,
|
|
15
|
+
fifty times over. sqlalchemy's `_commit_impl` is the precedent for a
|
|
16
|
+
private seam where the public ones do not bound the work. `forward`,
|
|
17
|
+
which skips rows with a MOVE of its own rather than through `_exec`,
|
|
18
|
+
is bound as the public method it is.
|
|
19
|
+
|
|
20
|
+
The cursor knows its query (`_query`) and its connection, which
|
|
21
|
+
supply the statement (when the setting is on) and the server keys.
|
|
22
|
+
The operation is DECLARE for the two binds, FETCH for the exec and
|
|
23
|
+
MOVE for a forward.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import wrapture
|
|
31
|
+
|
|
32
|
+
from ..common import statement_data
|
|
33
|
+
from .connection import database_binding, server_of
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
37
|
+
"""Bind the cursor's round trips; register their removal as this
|
|
38
|
+
trigger's cleanup."""
|
|
39
|
+
|
|
40
|
+
settings = instrumentation.settings
|
|
41
|
+
record_statement = bool(settings["statement"])
|
|
42
|
+
|
|
43
|
+
def round_trips(operation: str) -> Any:
|
|
44
|
+
def decorator(
|
|
45
|
+
wrapped: Any,
|
|
46
|
+
instance: Any,
|
|
47
|
+
args: tuple[Any, ...],
|
|
48
|
+
kwargs: dict[str, Any],
|
|
49
|
+
) -> Any:
|
|
50
|
+
async def record() -> Any:
|
|
51
|
+
connection = getattr(instance, "_connection", None)
|
|
52
|
+
query = getattr(instance, "_query", None)
|
|
53
|
+
wrapture.annotate(
|
|
54
|
+
**statement_data(
|
|
55
|
+
query,
|
|
56
|
+
connection,
|
|
57
|
+
server_of(connection),
|
|
58
|
+
record_statement,
|
|
59
|
+
operation,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return await wrapped(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
return record()
|
|
66
|
+
|
|
67
|
+
return decorator
|
|
68
|
+
|
|
69
|
+
cursor_class = module.BaseCursor
|
|
70
|
+
named: dict[str, wrapture.Binding] = {}
|
|
71
|
+
|
|
72
|
+
for method, operation in (
|
|
73
|
+
("_bind_exec", "DECLARE"),
|
|
74
|
+
("_bind", "DECLARE"),
|
|
75
|
+
("_exec", "FETCH"),
|
|
76
|
+
):
|
|
77
|
+
bound = database_binding(cursor_class, method)
|
|
78
|
+
bound.on_call.decorates(round_trips(operation))
|
|
79
|
+
named[method.lstrip("_")] = bound
|
|
80
|
+
|
|
81
|
+
forward = database_binding(module.Cursor, "forward")
|
|
82
|
+
forward.on_call.decorates(round_trips("MOVE"))
|
|
83
|
+
named["forward"] = forward
|
|
84
|
+
|
|
85
|
+
group = wrapture.bindings(**named)
|
|
86
|
+
group.apply()
|
|
87
|
+
|
|
88
|
+
instrumentation.on_cleanup(group.remove)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The prepared statement seams: a PreparedStatement's own fetch and
|
|
2
|
+
execute family, recorded as database events around their awaits.
|
|
3
|
+
|
|
4
|
+
`await connection.prepare(query)` hands back a PreparedStatement whose
|
|
5
|
+
`fetch`, `fetchrow`, `fetchval`, `fetchmany`, `executemany` and
|
|
6
|
+
`explain` bypass Connection's public methods, driving the protocol
|
|
7
|
+
through the statement's own private path, so each is bound in its
|
|
8
|
+
own right. The statement's SQL is public (`get_query()`), and the
|
|
9
|
+
connection it belongs to supplies the server keys. The `prepare` call
|
|
10
|
+
itself is not a query and is not recorded; the round trip that
|
|
11
|
+
prepares happens inside it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import wrapture
|
|
19
|
+
|
|
20
|
+
from ..common import statement_data
|
|
21
|
+
from .connection import database_binding, server_of
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
25
|
+
"""Bind the prepared statement's methods; register their removal
|
|
26
|
+
as this trigger's cleanup."""
|
|
27
|
+
|
|
28
|
+
settings = instrumentation.settings
|
|
29
|
+
record_statement = bool(settings["statement"])
|
|
30
|
+
|
|
31
|
+
def executes(operation: str | None = None) -> Any:
|
|
32
|
+
def decorator(
|
|
33
|
+
wrapped: Any,
|
|
34
|
+
instance: Any,
|
|
35
|
+
args: tuple[Any, ...],
|
|
36
|
+
kwargs: dict[str, Any],
|
|
37
|
+
) -> Any:
|
|
38
|
+
async def record() -> Any:
|
|
39
|
+
connection = getattr(instance, "_connection", None)
|
|
40
|
+
query = instance.get_query()
|
|
41
|
+
wrapture.annotate(
|
|
42
|
+
**statement_data(
|
|
43
|
+
query,
|
|
44
|
+
connection,
|
|
45
|
+
server_of(connection),
|
|
46
|
+
record_statement,
|
|
47
|
+
operation,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return await wrapped(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
return record()
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
statement_class = module.PreparedStatement
|
|
58
|
+
named: dict[str, wrapture.Binding] = {}
|
|
59
|
+
|
|
60
|
+
for method in ("fetch", "fetchrow", "fetchval", "fetchmany", "executemany"):
|
|
61
|
+
if method not in vars(statement_class):
|
|
62
|
+
continue
|
|
63
|
+
bound = database_binding(statement_class, method)
|
|
64
|
+
bound.on_call.decorates(executes())
|
|
65
|
+
named[method] = bound
|
|
66
|
+
|
|
67
|
+
explain = database_binding(statement_class, "explain")
|
|
68
|
+
explain.on_call.decorates(executes("EXPLAIN"))
|
|
69
|
+
named["explain"] = explain
|
|
70
|
+
|
|
71
|
+
group = wrapture.bindings(**named)
|
|
72
|
+
group.apply()
|
|
73
|
+
|
|
74
|
+
instrumentation.on_cleanup(group.remove)
|