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,402 @@
|
|
|
1
|
+
"""The psycopg2 patches: the connect factory bound, and the
|
|
2
|
+
connections it returns made instances of recording subclasses, their
|
|
3
|
+
cursors likewise.
|
|
4
|
+
|
|
5
|
+
psycopg2's connection and cursor are C types, so there is no
|
|
6
|
+
attribute on them a binding could patch, and a proxy around them is
|
|
7
|
+
not an option either: psycopg2's own C entry points type-check the
|
|
8
|
+
objects handed back to them (`register_type`, and so `register_uuid`,
|
|
9
|
+
`register_hstore`, `register_json` and `register_composite`, plus
|
|
10
|
+
`quote_ident`, `Json(...).prepare()` and `lobject`), and a proxy
|
|
11
|
+
fails those checks. What psycopg2 does support is subclassing: the
|
|
12
|
+
`connection_factory=` argument to `connect()` and the `cursor_factory`
|
|
13
|
+
of a connection or a `cursor()` call are how its own extras
|
|
14
|
+
(`LoggingConnection`, `DictCursor`) work. The instrumentation uses
|
|
15
|
+
that mechanism: the one seam is `psycopg2.connect`, bound to record
|
|
16
|
+
the open and to substitute the requested connection factory with a
|
|
17
|
+
recording subclass of it, and that subclass hands out cursors that
|
|
18
|
+
are recording subclasses of whatever cursor class was asked for.
|
|
19
|
+
|
|
20
|
+
The recording subclasses are made per base class and cached: a mixin
|
|
21
|
+
of plain Python methods (`CursorMixin`, `ConnectionMixin`) placed
|
|
22
|
+
ahead of the base in the bases, each override calling the base's
|
|
23
|
+
method through super(), so the application's own factories
|
|
24
|
+
(`RealDictConnection`, `DictCursor`, `LoggingConnection`) keep
|
|
25
|
+
working and are simply recorded. The subclass takes the base's name
|
|
26
|
+
and module, so reprs read as before; isinstance checks and the C
|
|
27
|
+
type checks pass, since these are real subclasses. Those mixin
|
|
28
|
+
methods are what wrapture binds, each labelled with the psycopg2 name
|
|
29
|
+
it stands for (`psycopg2.extensions:cursor.execute`), the one thing
|
|
30
|
+
its recorded path cannot say.
|
|
31
|
+
|
|
32
|
+
Cursors are made recording in `ConnectionMixin.cursor`. A cursor class
|
|
33
|
+
named explicitly, in the call or on the connection's `cursor_factory`,
|
|
34
|
+
is substituted with its recording subclass before the base builds
|
|
35
|
+
the cursor. When nothing names one and the base would fall back to
|
|
36
|
+
psycopg2's own C cursor type, the recording subclass of that type is
|
|
37
|
+
passed explicitly, since an instance of the C type itself cannot be
|
|
38
|
+
reclassed afterwards. When a base class's own `cursor()` supplies a
|
|
39
|
+
default (the extras' connection classes do), it runs first and the
|
|
40
|
+
cursor it returns, an instance of a Python subclass, is reclassed to
|
|
41
|
+
the recording subclass of its class on the way out, which Python
|
|
42
|
+
permits for compatible heap types.
|
|
43
|
+
|
|
44
|
+
The recorded set is the acquisition, the execute family and the
|
|
45
|
+
transaction boundaries: `connect`; `execute`, `executemany` and
|
|
46
|
+
`callproc`; `copy_from`, `copy_to` and `copy_expert`; `commit` and
|
|
47
|
+
`rollback`; and the connection's commit-or-rollback context manager,
|
|
48
|
+
whose exit records which of the two it performed (psycopg2's does
|
|
49
|
+
not close the connection). Cursor creation and the fetch methods are
|
|
50
|
+
not recorded, and a named cursor's FETCHes not either, the model
|
|
51
|
+
every database target here follows. Every event carries the
|
|
52
|
+
database contract keys and the server reached, from the connection's
|
|
53
|
+
info; the SQL text as the application handed it over (psycopg2
|
|
54
|
+
interpolates the parameters client-side below this seam, so the
|
|
55
|
+
recorded text carries the placeholders) rides as `statement` only
|
|
56
|
+
when the setting is on, and bound parameters are never recorded.
|
|
57
|
+
|
|
58
|
+
Removing the instrumentation removes the bindings and restores
|
|
59
|
+
`connect`: connections already made keep their recording classes,
|
|
60
|
+
whose methods then delegate without recording.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
from types import TracebackType
|
|
66
|
+
from typing import Any, cast
|
|
67
|
+
|
|
68
|
+
import wrapture
|
|
69
|
+
|
|
70
|
+
from ..common import SYSTEM, captured, query_data, server_of
|
|
71
|
+
|
|
72
|
+
_connections: dict[type, type] = {}
|
|
73
|
+
_cursors: dict[type, type] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CursorMixin:
|
|
77
|
+
"""The recording overrides placed ahead of a psycopg2 cursor class:
|
|
78
|
+
each calls the base's method through super(), and each is bound
|
|
79
|
+
by wrapture below."""
|
|
80
|
+
|
|
81
|
+
def execute(self, query: Any, vars: Any = None) -> Any:
|
|
82
|
+
return cast(Any, super()).execute(query, vars)
|
|
83
|
+
|
|
84
|
+
def executemany(self, query: Any, vars_list: Any) -> Any:
|
|
85
|
+
return cast(Any, super()).executemany(query, vars_list)
|
|
86
|
+
|
|
87
|
+
def callproc(self, procname: Any, parameters: Any = None) -> Any:
|
|
88
|
+
return cast(Any, super()).callproc(procname, parameters)
|
|
89
|
+
|
|
90
|
+
def copy_from(
|
|
91
|
+
self,
|
|
92
|
+
file: Any,
|
|
93
|
+
table: Any,
|
|
94
|
+
sep: str = "\t",
|
|
95
|
+
null: str = "\\N",
|
|
96
|
+
size: int = 8192,
|
|
97
|
+
columns: Any = None,
|
|
98
|
+
) -> Any:
|
|
99
|
+
return cast(Any, super()).copy_from(
|
|
100
|
+
file, table, sep=sep, null=null, size=size, columns=columns
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def copy_to(
|
|
104
|
+
self,
|
|
105
|
+
file: Any,
|
|
106
|
+
table: Any,
|
|
107
|
+
sep: str = "\t",
|
|
108
|
+
null: str = "\\N",
|
|
109
|
+
columns: Any = None,
|
|
110
|
+
) -> Any:
|
|
111
|
+
return cast(Any, super()).copy_to(
|
|
112
|
+
file, table, sep=sep, null=null, columns=columns
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def copy_expert(self, sql: Any, file: Any, size: int = 8192) -> Any:
|
|
116
|
+
return cast(Any, super()).copy_expert(sql, file, size=size)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ConnectionMixin:
|
|
120
|
+
"""The recording overrides placed ahead of a psycopg2 connection
|
|
121
|
+
class: cursors come back as recording subclasses, and the
|
|
122
|
+
transaction boundaries call through to the base and are bound by
|
|
123
|
+
wrapture below."""
|
|
124
|
+
|
|
125
|
+
def cursor(self, *args: Any, **kwargs: Any) -> Any:
|
|
126
|
+
# A cursor class named in the call (by keyword or in the
|
|
127
|
+
# second positional slot) is substituted with its recording
|
|
128
|
+
# subclass before the base builds the cursor.
|
|
129
|
+
|
|
130
|
+
if "cursor_factory" in kwargs:
|
|
131
|
+
factory = kwargs["cursor_factory"]
|
|
132
|
+
elif len(args) > 1:
|
|
133
|
+
factory = args[1]
|
|
134
|
+
else:
|
|
135
|
+
factory = None
|
|
136
|
+
|
|
137
|
+
if factory is not None:
|
|
138
|
+
recording = recording_cursor(factory)
|
|
139
|
+
if "cursor_factory" in kwargs:
|
|
140
|
+
kwargs["cursor_factory"] = recording
|
|
141
|
+
else:
|
|
142
|
+
args = (args[0], recording, *args[2:])
|
|
143
|
+
|
|
144
|
+
# With none named and nothing set on the connection, a base
|
|
145
|
+
# that falls straight through to the C method would build an
|
|
146
|
+
# instance of the C cursor type, which cannot be reclassed
|
|
147
|
+
# afterwards, so its recording subclass is named explicitly.
|
|
148
|
+
|
|
149
|
+
elif getattr(self, "cursor_factory", None) is None and _falls_through(
|
|
150
|
+
type(self)
|
|
151
|
+
):
|
|
152
|
+
kwargs["cursor_factory"] = recording_cursor(_c_cursor_type())
|
|
153
|
+
|
|
154
|
+
cursor = cast(Any, super()).cursor(*args, **kwargs)
|
|
155
|
+
|
|
156
|
+
# A base's own cursor() may have supplied its default cursor
|
|
157
|
+
# class instead; its instance is reclassed on the way out.
|
|
158
|
+
|
|
159
|
+
if not isinstance(cursor, CursorMixin):
|
|
160
|
+
try:
|
|
161
|
+
cursor.__class__ = recording_cursor(type(cursor))
|
|
162
|
+
except TypeError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
return cursor
|
|
166
|
+
|
|
167
|
+
def commit(self) -> Any:
|
|
168
|
+
return cast(Any, super()).commit()
|
|
169
|
+
|
|
170
|
+
def rollback(self) -> Any:
|
|
171
|
+
return cast(Any, super()).rollback()
|
|
172
|
+
|
|
173
|
+
def __exit__(
|
|
174
|
+
self,
|
|
175
|
+
exc_type: type[BaseException] | None,
|
|
176
|
+
exc_value: BaseException | None,
|
|
177
|
+
traceback: TracebackType | None,
|
|
178
|
+
) -> Any:
|
|
179
|
+
return cast(Any, super()).__exit__(exc_type, exc_value, traceback)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _c_cursor_type() -> type:
|
|
183
|
+
import psycopg2.extensions
|
|
184
|
+
|
|
185
|
+
return cast(type, psycopg2.extensions.cursor)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _falls_through(cls: type) -> bool:
|
|
189
|
+
"""Whether cursor() on the given recording connection class, past
|
|
190
|
+
the mixin, is psycopg2's own C method rather than an override of
|
|
191
|
+
a base class supplying a cursor default of its own."""
|
|
192
|
+
|
|
193
|
+
import psycopg2.extensions
|
|
194
|
+
|
|
195
|
+
for base in cls.__mro__:
|
|
196
|
+
if base is ConnectionMixin:
|
|
197
|
+
continue
|
|
198
|
+
if "cursor" in vars(base):
|
|
199
|
+
return base is psycopg2.extensions.connection
|
|
200
|
+
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _recording(cache: dict[type, type], mixin: type, base: type) -> type:
|
|
205
|
+
if isinstance(base, type) and issubclass(base, mixin):
|
|
206
|
+
return base
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
return cache[base]
|
|
210
|
+
except KeyError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
made = type(
|
|
214
|
+
base.__name__,
|
|
215
|
+
(mixin, base),
|
|
216
|
+
{"__module__": base.__module__, "__qualname__": base.__qualname__},
|
|
217
|
+
)
|
|
218
|
+
cache[base] = made
|
|
219
|
+
|
|
220
|
+
return made
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def recording_cursor(base: type) -> type:
|
|
224
|
+
"""The recording subclass of a psycopg2 cursor class, made once
|
|
225
|
+
per class; a class already recording is returned as is."""
|
|
226
|
+
|
|
227
|
+
return _recording(_cursors, CursorMixin, base)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def recording_connection(base: type) -> type:
|
|
231
|
+
"""The recording subclass of a psycopg2 connection class, made
|
|
232
|
+
once per class; a class already recording is returned as is."""
|
|
233
|
+
|
|
234
|
+
return _recording(_connections, ConnectionMixin, base)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
238
|
+
"""Bind the connect factory and the mixins' methods; register
|
|
239
|
+
their removal as this trigger's cleanup."""
|
|
240
|
+
|
|
241
|
+
settings = instrumentation.settings
|
|
242
|
+
record_statement = bool(settings["statement"])
|
|
243
|
+
|
|
244
|
+
def data_for(
|
|
245
|
+
cursor: Any, query: Any, operation: str | None = None
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
return query_data(
|
|
248
|
+
query, cursor, cursor.connection.info, record_statement, operation
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def opens(
|
|
252
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
253
|
+
) -> Any:
|
|
254
|
+
wrapture.annotate(system=SYSTEM, operation="CONNECT")
|
|
255
|
+
|
|
256
|
+
# The requested connection class (keyword, or the second
|
|
257
|
+
# positional slot) is replaced with its recording subclass.
|
|
258
|
+
|
|
259
|
+
if "connection_factory" in kwargs:
|
|
260
|
+
factory = kwargs["connection_factory"]
|
|
261
|
+
elif len(args) > 1:
|
|
262
|
+
factory = args[1]
|
|
263
|
+
else:
|
|
264
|
+
factory = None
|
|
265
|
+
|
|
266
|
+
recording = recording_connection(factory or module.extensions.connection)
|
|
267
|
+
if "connection_factory" in kwargs or len(args) <= 1:
|
|
268
|
+
kwargs["connection_factory"] = recording
|
|
269
|
+
else:
|
|
270
|
+
args = (args[0], recording, *args[2:])
|
|
271
|
+
|
|
272
|
+
connection = wrapped(*args, **kwargs)
|
|
273
|
+
wrapture.annotate(**server_of(connection.info))
|
|
274
|
+
|
|
275
|
+
return connection
|
|
276
|
+
|
|
277
|
+
def queries(
|
|
278
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
279
|
+
) -> Any:
|
|
280
|
+
query = args[0] if args else kwargs.get("query")
|
|
281
|
+
wrapture.annotate(**data_for(instance, query))
|
|
282
|
+
|
|
283
|
+
return wrapped(*args, **kwargs)
|
|
284
|
+
|
|
285
|
+
def calls(
|
|
286
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
287
|
+
) -> Any:
|
|
288
|
+
procname = args[0] if args else kwargs.get("procname")
|
|
289
|
+
data = data_for(instance, None, "CALL")
|
|
290
|
+
if isinstance(procname, str):
|
|
291
|
+
data["procedure"] = procname
|
|
292
|
+
wrapture.annotate(**data)
|
|
293
|
+
|
|
294
|
+
return wrapped(*args, **kwargs)
|
|
295
|
+
|
|
296
|
+
def copies_table(
|
|
297
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
298
|
+
) -> Any:
|
|
299
|
+
table = args[1] if len(args) > 1 else kwargs.get("table")
|
|
300
|
+
data = data_for(instance, None, "COPY")
|
|
301
|
+
if isinstance(table, str):
|
|
302
|
+
data["collection"] = table
|
|
303
|
+
wrapture.annotate(**data)
|
|
304
|
+
|
|
305
|
+
return wrapped(*args, **kwargs)
|
|
306
|
+
|
|
307
|
+
def copies_statement(
|
|
308
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
309
|
+
) -> Any:
|
|
310
|
+
sql = args[0] if args else kwargs.get("sql")
|
|
311
|
+
wrapture.annotate(**data_for(instance, sql, "COPY"))
|
|
312
|
+
|
|
313
|
+
return wrapped(*args, **kwargs)
|
|
314
|
+
|
|
315
|
+
def performs(operation: str) -> Any:
|
|
316
|
+
def record(
|
|
317
|
+
wrapped: Any,
|
|
318
|
+
instance: Any,
|
|
319
|
+
args: tuple[Any, ...],
|
|
320
|
+
kwargs: dict[str, Any],
|
|
321
|
+
) -> Any:
|
|
322
|
+
wrapture.annotate(
|
|
323
|
+
system=SYSTEM, operation=operation, **server_of(instance.info)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return wrapped(*args, **kwargs)
|
|
327
|
+
|
|
328
|
+
return record
|
|
329
|
+
|
|
330
|
+
def leaves(
|
|
331
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
332
|
+
) -> Any:
|
|
333
|
+
# The exit commits unless an exception is on its way through.
|
|
334
|
+
|
|
335
|
+
exc_type = args[0] if args else kwargs.get("exc_type")
|
|
336
|
+
operation = "COMMIT" if exc_type is None else "ROLLBACK"
|
|
337
|
+
wrapture.annotate(
|
|
338
|
+
system=SYSTEM, operation=operation, **server_of(instance.info)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return wrapped(*args, **kwargs)
|
|
342
|
+
|
|
343
|
+
def database_binding(
|
|
344
|
+
owner: Any, name: str, label: str | None = None, capture_args: Any = captured
|
|
345
|
+
) -> wrapture.Binding:
|
|
346
|
+
return wrapture.binding(
|
|
347
|
+
owner,
|
|
348
|
+
name,
|
|
349
|
+
label=label,
|
|
350
|
+
category="database",
|
|
351
|
+
leaf=True,
|
|
352
|
+
capture_args=capture_args,
|
|
353
|
+
capture_result=captured,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
named: dict[str, wrapture.Binding] = {}
|
|
357
|
+
|
|
358
|
+
# The factory: the module attribute's path says psycopg2:connect
|
|
359
|
+
# already, so it takes no label, and none of its arguments are
|
|
360
|
+
# captured (the dsn carries the password).
|
|
361
|
+
|
|
362
|
+
connect = database_binding(module, "connect", capture_args="none")
|
|
363
|
+
connect.on_call.decorates(opens)
|
|
364
|
+
named["connect"] = connect
|
|
365
|
+
|
|
366
|
+
# The cursor methods, labelled with the psycopg2 names they stand
|
|
367
|
+
# for.
|
|
368
|
+
|
|
369
|
+
for method, decorator in (
|
|
370
|
+
("execute", queries),
|
|
371
|
+
("executemany", queries),
|
|
372
|
+
("callproc", calls),
|
|
373
|
+
("copy_from", copies_table),
|
|
374
|
+
("copy_to", copies_table),
|
|
375
|
+
("copy_expert", copies_statement),
|
|
376
|
+
):
|
|
377
|
+
bound = database_binding(
|
|
378
|
+
CursorMixin, method, label=f"psycopg2.extensions:cursor.{method}"
|
|
379
|
+
)
|
|
380
|
+
bound.on_call.decorates(decorator)
|
|
381
|
+
named[f"cursor_{method}"] = bound
|
|
382
|
+
|
|
383
|
+
# The transaction boundaries: the explicit calls, and the context
|
|
384
|
+
# manager exit that performs one of them.
|
|
385
|
+
|
|
386
|
+
for method, operation in (("commit", "COMMIT"), ("rollback", "ROLLBACK")):
|
|
387
|
+
bound = database_binding(
|
|
388
|
+
ConnectionMixin, method, label=f"psycopg2.extensions:connection.{method}"
|
|
389
|
+
)
|
|
390
|
+
bound.on_call.decorates(performs(operation))
|
|
391
|
+
named[f"connection_{method}"] = bound
|
|
392
|
+
|
|
393
|
+
closes = database_binding(
|
|
394
|
+
ConnectionMixin, "__exit__", label="psycopg2.extensions:connection.__exit__"
|
|
395
|
+
)
|
|
396
|
+
closes.on_call.decorates(leaves)
|
|
397
|
+
named["connection_exit"] = closes
|
|
398
|
+
|
|
399
|
+
group = wrapture.bindings(**named)
|
|
400
|
+
group.apply()
|
|
401
|
+
|
|
402
|
+
instrumentation.on_cleanup(group.remove)
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrapture-instrumentation-postgresql
|
|
3
|
+
Version: 1.0.0.dev1
|
|
4
|
+
Summary: Instrumentation for the PostgreSQL client libraries (psycopg, psycopg2, asyncpg), applied through wrapture.
|
|
5
|
+
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql
|
|
8
|
+
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/issues/
|
|
10
|
+
Keywords: wrapture,instrumentation,postgresql,psycopg,psycopg2,asyncpg,tracing
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: wrapture>=1.0.0a20
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# wrapture-instrumentation-postgresql
|
|
25
|
+
|
|
26
|
+
Instrumentation for the PostgreSQL client libraries, applied through
|
|
27
|
+
[wrapture](https://github.com/GrahamDumpleton/wrapture).
|
|
28
|
+
|
|
29
|
+
wrapture attaches bindings to arbitrary Python call sites without
|
|
30
|
+
modifying the code being observed, and its config layer can switch on
|
|
31
|
+
packaged instrumentation for a third-party package by name. This is
|
|
32
|
+
the PostgreSQL package in that collection: one `wrapture.Instrumentation`
|
|
33
|
+
class per client library, so tracing every query, connection and
|
|
34
|
+
transaction your application sends to PostgreSQL is one config entry
|
|
35
|
+
and no code.
|
|
36
|
+
|
|
37
|
+
> **Status: alpha, ahead of 1.0.0.** Developed against wrapture's
|
|
38
|
+
> alpha series, with pre-releases published to
|
|
39
|
+
> [PyPI](https://pypi.org/project/wrapture-instrumentation-postgresql/),
|
|
40
|
+
> and until 1.0.0 is final a plain `pip install
|
|
41
|
+
> wrapture-instrumentation-postgresql` picks up the latest pre-release
|
|
42
|
+
> automatically, so there is no need to pin a specific version.
|
|
43
|
+
|
|
44
|
+
## Why a separate package
|
|
45
|
+
|
|
46
|
+
The core
|
|
47
|
+
[wrapture-instrumentation](https://github.com/GrahamDumpleton/wrapture-instrumentation)
|
|
48
|
+
package deliberately covers only the standard library and third-party
|
|
49
|
+
packages that can be exercised in-process, with no separate backend
|
|
50
|
+
product or service needed to test against. A PostgreSQL driver is
|
|
51
|
+
exactly the kind of target the separate-package rule was drawn for:
|
|
52
|
+
its tests need a real server, so this package's suite runs one in a
|
|
53
|
+
docker container, and it carries the drivers as test dependencies
|
|
54
|
+
(some of them compiled wheels) and its own release cadence, so the
|
|
55
|
+
core package's test matrix stays light. One package covers every
|
|
56
|
+
client library for the one backend: psycopg, psycopg2 and asyncpg.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```console
|
|
61
|
+
$ pip install wrapture-instrumentation-postgresql
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Installing it brings wrapture and nothing else. No driver is a
|
|
65
|
+
dependency: each instrumentation is inert until its driver is present,
|
|
66
|
+
and wrapture checks the installed version against the range the
|
|
67
|
+
instrumentation supports at apply time.
|
|
68
|
+
|
|
69
|
+
## Using it
|
|
70
|
+
|
|
71
|
+
An `[[instrument]]` entry in `wrapture.toml` names the target:
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[[instrument]]
|
|
75
|
+
name = "psycopg"
|
|
76
|
+
|
|
77
|
+
[[sink]]
|
|
78
|
+
type = "printer"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
and the runner applies it before the application starts, so the patch
|
|
82
|
+
is in place before the driver is imported:
|
|
83
|
+
|
|
84
|
+
```console
|
|
85
|
+
$ python -m wrapture -m myapp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The same config works through
|
|
89
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
90
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`); through
|
|
91
|
+
[manual setup](https://wrapture.readthedocs.io/en/latest/manual-setup.html),
|
|
92
|
+
a few lines in the application's own startup where wrapping the launch
|
|
93
|
+
from outside is awkward; and, in a test, through
|
|
94
|
+
`wrapture.instrumentation("psycopg")` scoping the instrumentation to
|
|
95
|
+
a block. The
|
|
96
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
97
|
+
covers the config file itself.
|
|
98
|
+
|
|
99
|
+
To see what is installed, what it supports in the current
|
|
100
|
+
environment, and what settings it takes:
|
|
101
|
+
|
|
102
|
+
```console
|
|
103
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Provided instrumentation
|
|
107
|
+
|
|
108
|
+
| Target | Supported versions | Records | Settings |
|
|
109
|
+
| ------ | ------------------ | ------- | -------- |
|
|
110
|
+
| [`psycopg`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/psycopg/README.md) | psycopg 3.1+ (3.x) | Every query as one `database` leaf, however it was issued (a cursor's `execute` or `executemany`, the connection's shortcut, a streamed query, a COPY, a server-side cursor's DECLARE), plus the connection being opened and each transaction boundary (`commit`, `rollback`, the connection's context manager, and a `transaction()` block's begin and end, savepoints included); sync and async classes alike, and connections from a pool. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text is recorded only with the `statement` setting on, bound parameters never. | `statement` |
|
|
111
|
+
| [`psycopg2`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/psycopg2/README.md) | psycopg2 2.9+ (2.x), psycopg2-binary alike | Every query as one `database` leaf, however it was issued (`execute`, `executemany`, `callproc`, the extras' batch helpers, a named cursor's DECLARE), each COPY (`copy_from`, `copy_to`, `copy_expert`), the connection being opened and each transaction boundary (`commit`, `rollback`, the connection's context manager); through recording subclasses injected by psycopg2's own factory mechanism, so your `cursor_factory` and `connection_factory` classes keep working and are recorded too. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text (the template with its placeholders) is recorded only with the `statement` setting on, bound parameters never. | `statement` |
|
|
112
|
+
| [`asyncpg`](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/src/wrapture_instrumentation_postgresql/asyncpg/README.md) | asyncpg 0.29+ (0.x) | Every query as one `database` leaf recorded around its await, however it was issued (a connection's `execute`, `executemany`, `fetch`, `fetchrow`, `fetchval` or `fetchmany`, a prepared statement's own fetches, a server-side cursor's DECLARE and each FETCH), each COPY, the connection being opened, and each transaction boundary, which asyncpg issues through `execute`. Each event carries the system, the operation, and the database, host and port it reached; a failing statement records the driver's exception. The SQL text (with its `$n` placeholders) is recorded only with the `statement` setting on, query arguments never. A connection taken from a pool does not yet record its queries (a wrapture fix is pending). | `statement` |
|
|
113
|
+
|
|
114
|
+
The entry point name is the config's `name`""; the linked per-target
|
|
115
|
+
README is the full user documentation: what records, what the events
|
|
116
|
+
carry, the setting, and what is deliberately not traced.
|
|
117
|
+
|
|
118
|
+
## What is not traced
|
|
119
|
+
|
|
120
|
+
By design, and where it goes:
|
|
121
|
+
|
|
122
|
+
- Fetching rows: a query event closes when its execute returns, so
|
|
123
|
+
time spent iterating rows afterwards is the application's, and a
|
|
124
|
+
server-side cursor's FETCHes are not recorded (its DECLARE is).
|
|
125
|
+
|
|
126
|
+
- Pool checkouts (`psycopg_pool`): a connection taken from a pool
|
|
127
|
+
records its queries like any other, but taking and returning it are
|
|
128
|
+
not database operations and are not recorded.
|
|
129
|
+
|
|
130
|
+
- A connection taken from an asyncpg pool does not yet record its
|
|
131
|
+
queries: the pool proxy calls the connection's methods through the
|
|
132
|
+
class, a calling convention wrapture's signature check does not
|
|
133
|
+
yet handle. The fix is on wrapture's side.
|
|
134
|
+
|
|
135
|
+
- LISTEN/NOTIFY, large objects and two-phase commit are out of scope.
|
|
136
|
+
|
|
137
|
+
## Adding a target
|
|
138
|
+
|
|
139
|
+
Each client library is its own subpackage and entry point here. The
|
|
140
|
+
subpackage's `__init__.py` holds one `wrapture.Instrumentation`
|
|
141
|
+
subclass and imports only wrapture (and the package's own `common.py`,
|
|
142
|
+
which imports only wrapture too); everything that touches the driver
|
|
143
|
+
lives in sibling modules imported inside the hook. The class is
|
|
144
|
+
registered in `pyproject.toml` under
|
|
145
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
146
|
+
test suite under `tests/<target>/` and a `README.md` linked from the
|
|
147
|
+
table above. The
|
|
148
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
149
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
150
|
+
here covers the tests and the server they run against.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
BSD 2-Clause. See
|
|
155
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture-instrumentation-postgresql/blob/develop/LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
wrapture_instrumentation_postgresql/__init__.py,sha256=WA0NLvebvLdxRWlvGnabFloQHU1Q8b4b4VWjpV3aZSo,755
|
|
2
|
+
wrapture_instrumentation_postgresql/common.py,sha256=bkRjg8p4nb1HMhzDZN55J9Dh-kGZ6Wr0gUt1I9nEdNU,5457
|
|
3
|
+
wrapture_instrumentation_postgresql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
wrapture_instrumentation_postgresql/asyncpg/README.md,sha256=PPxCwe1LLrvHpEg7X516O5sfL2MqR9hbAcFPBrOG-z0,6014
|
|
5
|
+
wrapture_instrumentation_postgresql/asyncpg/__init__.py,sha256=yb_axU7Ii-2LyqOjzXKgB7Z3lV5jcXsrDWyNOXJyTwY,2689
|
|
6
|
+
wrapture_instrumentation_postgresql/asyncpg/connection.py,sha256=HH5vinUeXs678wpV-puoQ0RwIbyQ4nJfAMTnKYlSmy8,7538
|
|
7
|
+
wrapture_instrumentation_postgresql/asyncpg/cursor.py,sha256=2_X9YIxRSDDhTLAKAaJULAAlQobwGr6b4asxf_IB5Yk,3169
|
|
8
|
+
wrapture_instrumentation_postgresql/asyncpg/prepared.py,sha256=mW71-YRdvnyEkMO444ATjsnioa-IAKH7kwCaQVp5PKk,2485
|
|
9
|
+
wrapture_instrumentation_postgresql/psycopg/README.md,sha256=WzCd-v6b2wrfkaOZ2oNJBpPtPgHiRWaPe359o7wPTos,6642
|
|
10
|
+
wrapture_instrumentation_postgresql/psycopg/__init__.py,sha256=36aNivArazNIE8Mj_yzYFY_uYnHjlcfWAwc4y4oo2Y4,2019
|
|
11
|
+
wrapture_instrumentation_postgresql/psycopg/connection.py,sha256=tcuE2v6FVaEO9Or2ut0980yhKphN8sUp5N06m49Qq4A,5696
|
|
12
|
+
wrapture_instrumentation_postgresql/psycopg/cursor.py,sha256=qhK4i1Qf8Ho36oyCBQ714MGy0CBJNiY7MUqNDuwmEwU,9063
|
|
13
|
+
wrapture_instrumentation_postgresql/psycopg/transaction.py,sha256=27FP_Iin2jZknZ8IsLJquuK6mkSmz9llKOtCWVsmxLs,4427
|
|
14
|
+
wrapture_instrumentation_postgresql/psycopg2/README.md,sha256=AWU5p9nCbx7wEGQILk4cBauo9y_KfqlkRxU_TBsYS2A,5897
|
|
15
|
+
wrapture_instrumentation_postgresql/psycopg2/__init__.py,sha256=CP_9F74TjeDGeGU3S0DPyVvNa02HuYeaQqd0EsN0pOg,1649
|
|
16
|
+
wrapture_instrumentation_postgresql/psycopg2/factories.py,sha256=cBcc5W1ywljUqIQSu8_7YV_M4t3Q4ktCYk-kkonr5Ds,14314
|
|
17
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/licenses/LICENSE,sha256=RPMyRnXwgrw9co5o6wY9G3DKlb-oBZ44tpiRnuYeec8,1299
|
|
18
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/METADATA,sha256=P76R8MC0fqKxN71T71AjYtRofVZyoxX3ay7_rIrCdZg,8933
|
|
19
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
20
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/entry_points.txt,sha256=pPB1skIzk-3RIv8jXObnlfM0rW7uiKTnCgyQ6hJ4lA0,261
|
|
21
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/top_level.txt,sha256=xAb-vQUXGc51uRzpvxOF3M-gjE9VqNlTzdlLOFkDPHA,36
|
|
22
|
+
wrapture_instrumentation_postgresql-1.0.0.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright (c) 2026, Graham Dumpleton
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
16
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
17
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
18
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
19
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
20
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
21
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
22
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
23
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
24
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wrapture_instrumentation_postgresql
|