wrapture-instrumentation 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/__init__.py +26 -0
- wrapture_instrumentation/framework_flask/__init__.py +36 -0
- wrapture_instrumentation/framework_flask/hooks.py +115 -0
- wrapture_instrumentation/py.typed +0 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/METADATA +134 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/RECORD +10 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/WHEEL +5 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/entry_points.txt +2 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/licenses/LICENSE +24 -0
- wrapture_instrumentation-1.0.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instrumentation for common Python packages, applied through wrapture.
|
|
3
|
+
|
|
4
|
+
Each target package has its own subpackage here, named for its
|
|
5
|
+
category and the target (framework_flask, external_requests), holding
|
|
6
|
+
one wrapture.Instrumentation subclass registered in the
|
|
7
|
+
wrapture.instrumentation entry point group under the bare target name.
|
|
8
|
+
This module carries only the version: importing it loads no
|
|
9
|
+
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,36 @@
|
|
|
1
|
+
"""Instrumentation for Flask: request and view tracing for every
|
|
2
|
+
application the process creates.
|
|
3
|
+
|
|
4
|
+
This module imports only wrapture. Everything that touches Flask lives
|
|
5
|
+
in the sibling hooks module, which itself imports only wrapture at top
|
|
6
|
+
level, so loading this class when a config loads never imports Flask
|
|
7
|
+
ahead of the hook meant to fire on its import.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import wrapture
|
|
15
|
+
|
|
16
|
+
from . import hooks
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlaskInstrumentation(wrapture.Instrumentation):
|
|
20
|
+
"""Request and view tracing for Flask applications."""
|
|
21
|
+
|
|
22
|
+
# The description defaults to the distribution's summary, which
|
|
23
|
+
# describes the whole collection; each class in a multi-target
|
|
24
|
+
# package says what it alone does.
|
|
25
|
+
|
|
26
|
+
description = "Request and view tracing for Flask applications."
|
|
27
|
+
|
|
28
|
+
target = "flask"
|
|
29
|
+
supports = ">=3.0,<4"
|
|
30
|
+
removable = True
|
|
31
|
+
|
|
32
|
+
@wrapture.instrumentation_hook("flask.app")
|
|
33
|
+
def flask_app(self, name: str, module: Any) -> None:
|
|
34
|
+
"""Patch the Flask class's choke points once flask.app exists."""
|
|
35
|
+
|
|
36
|
+
hooks.instrument(module, self)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""The Flask patches, at Flask's own choke points.
|
|
2
|
+
|
|
3
|
+
Three bindings, all created with when=False so they are behaviour-only:
|
|
4
|
+
the plumbing is not the trace, so they act without ever recording
|
|
5
|
+
their own calls.
|
|
6
|
+
|
|
7
|
+
- Flask.__init__ installs the recording WSGI middleware on each new
|
|
8
|
+
instance's wsgi_app attribute, the documented place Flask middleware
|
|
9
|
+
goes, so every request records as one "request" event however the
|
|
10
|
+
instance was made: module level, an application factory, several
|
|
11
|
+
applications in one process.
|
|
12
|
+
- Flask.add_url_rule substitutes wrapture.observed(view_func) as
|
|
13
|
+
routes register, so every view handler records as a "call" event
|
|
14
|
+
beneath its request, wherever the view came from: module functions,
|
|
15
|
+
closures, blueprints from other modules, MethodView classes (which
|
|
16
|
+
register a generated view function). Flask captures view functions
|
|
17
|
+
into its dispatch table the moment a route registers, before any
|
|
18
|
+
observation could exist, which is why registration itself is the
|
|
19
|
+
point to intercept. observed() is idempotent, so a view registered
|
|
20
|
+
twice is not wrapped twice.
|
|
21
|
+
- Flask.handle_exception is the one place a view's exception can be
|
|
22
|
+
seen after Flask catches it: wsgi_app catches around the dispatch
|
|
23
|
+
and hands the exception here, which returns the 500 response, so
|
|
24
|
+
the request itself completes normally. The binding notes the
|
|
25
|
+
exception against the enclosing request event with note_exception(),
|
|
26
|
+
so the request shows the failure beside its status.
|
|
27
|
+
|
|
28
|
+
The three are applied as one group whose remove() is registered as a
|
|
29
|
+
cleanup callback with on_cleanup(), which is what lets remove(), and
|
|
30
|
+
with it AppliedConfig.revert() and the instrumentation() context
|
|
31
|
+
manager, take the whole thing down again.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
import wrapture
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def wrap_app(
|
|
42
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
43
|
+
) -> Any:
|
|
44
|
+
"""Run Flask.__init__, then install the recording middleware on the
|
|
45
|
+
new instance's wsgi_app."""
|
|
46
|
+
|
|
47
|
+
outcome = wrapped(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
# Label with the app's own import name, so requests read as
|
|
50
|
+
# "myapp.wsgi_app" and two apps in one process stay distinct.
|
|
51
|
+
|
|
52
|
+
instance.wsgi_app = wrapture.WSGIMiddleware(
|
|
53
|
+
instance.wsgi_app, label=f"{instance.name}.wsgi_app"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return outcome
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def wrap_view(
|
|
60
|
+
args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
61
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
62
|
+
"""Substitute an observed view function into an add_url_rule call.
|
|
63
|
+
|
|
64
|
+
The signature is add_url_rule(rule, endpoint=None, view_func=None,
|
|
65
|
+
...), so the view is either the view_func keyword or the third
|
|
66
|
+
positional argument; a registration without a view (an endpoint
|
|
67
|
+
name alone) passes through untouched.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if kwargs.get("view_func") is not None:
|
|
71
|
+
kwargs = dict(kwargs, view_func=wrapture.observed(kwargs["view_func"]))
|
|
72
|
+
elif len(args) >= 3 and args[2] is not None:
|
|
73
|
+
args = (*args[:2], wrapture.observed(args[2]), *args[3:])
|
|
74
|
+
|
|
75
|
+
return args, kwargs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def note_failure(
|
|
79
|
+
wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Note the exception handle_exception received against the
|
|
82
|
+
enclosing request event, then let the handler run."""
|
|
83
|
+
|
|
84
|
+
# The exception is the handler's one positional argument. Aim the
|
|
85
|
+
# note at the request the middleware recorded, not at this call,
|
|
86
|
+
# which is not recorded anyway; outside a request (a handler
|
|
87
|
+
# invoked with nothing recording) this is a no-op.
|
|
88
|
+
|
|
89
|
+
exception = args[0] if args else kwargs.get("e")
|
|
90
|
+
if exception is not None:
|
|
91
|
+
wrapture.note_exception(exception, event=wrapture.current_event(kind="request"))
|
|
92
|
+
|
|
93
|
+
return wrapped(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
|
|
97
|
+
"""Bind the three choke points on the Flask class found in the
|
|
98
|
+
flask.app module, apply them as one group, and register the
|
|
99
|
+
group's removal as the instrumentation's cleanup."""
|
|
100
|
+
|
|
101
|
+
constructor = wrapture.binding(module.Flask, "__init__", when=False)
|
|
102
|
+
constructor.on_call.decorates(wrap_app)
|
|
103
|
+
|
|
104
|
+
registrar = wrapture.binding(module.Flask, "add_url_rule", when=False)
|
|
105
|
+
registrar.on_call.transforms_args(wrap_view)
|
|
106
|
+
|
|
107
|
+
handler = wrapture.binding(module.Flask, "handle_exception", when=False)
|
|
108
|
+
handler.on_call.decorates(note_failure)
|
|
109
|
+
|
|
110
|
+
group = wrapture.bindings(
|
|
111
|
+
constructor=constructor, registrar=registrar, handler=handler
|
|
112
|
+
)
|
|
113
|
+
group.apply()
|
|
114
|
+
|
|
115
|
+
instrumentation.on_cleanup(group.remove)
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrapture-instrumentation
|
|
3
|
+
Version: 1.0.0.dev1
|
|
4
|
+
Summary: Instrumentation for common Python packages, 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
|
|
8
|
+
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation/issues/
|
|
10
|
+
Keywords: wrapture,instrumentation,monkey patching,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 :: Implementation :: CPython
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: wrapture>=1.0.0a4
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# wrapture-instrumentation
|
|
24
|
+
|
|
25
|
+
Instrumentation for common Python packages, applied through
|
|
26
|
+
[wrapture](https://github.com/GrahamDumpleton/wrapture).
|
|
27
|
+
|
|
28
|
+
wrapture attaches bindings to arbitrary Python call sites without
|
|
29
|
+
modifying the code being observed, and its config layer can switch on
|
|
30
|
+
packaged instrumentation for a third-party package by name. This
|
|
31
|
+
project is the collection of that packaged instrumentation: one
|
|
32
|
+
`wrapture.Instrumentation` class per target package (Flask first,
|
|
33
|
+
more to follow), each registered under the bare target name, so that
|
|
34
|
+
tracing a framework is one config entry and no code.
|
|
35
|
+
|
|
36
|
+
> **Status: pre-alpha.** The package is being built target by target
|
|
37
|
+
> against wrapture's alpha series; nothing is published to PyPI yet.
|
|
38
|
+
> Flask is the first target and covers its basics; see the table below.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
$ pip install wrapture-instrumentation
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Installing it brings wrapture and nothing else. No target package is
|
|
47
|
+
a dependency: the instrumentation for a package you do not have is
|
|
48
|
+
inert, and wrapture checks the installed version of each target
|
|
49
|
+
against the range the instrumentation supports at apply time.
|
|
50
|
+
|
|
51
|
+
## Using it
|
|
52
|
+
|
|
53
|
+
An `[[instrument]]` entry in `wrapture.toml` names a target:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[[instrument]]
|
|
57
|
+
name = "flask"
|
|
58
|
+
|
|
59
|
+
[[sink]]
|
|
60
|
+
type = "printer"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
and the runner applies it before the application starts, so the
|
|
64
|
+
patches are in place before the framework is imported:
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
$ python -m wrapture -m myapp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same config works through
|
|
71
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
72
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`) and, in a test,
|
|
73
|
+
through `wrapture.instrumentation("flask")` scoping the
|
|
74
|
+
instrumentation to a block. The
|
|
75
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
76
|
+
covers the config file itself.
|
|
77
|
+
|
|
78
|
+
To see what is installed, what each instrumentation supports in the
|
|
79
|
+
current environment, and what settings it takes:
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
and to generate the `[[instrument]]` entries to paste into a config,
|
|
86
|
+
every one disabled and every setting commented out at its default:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
$ python -m wrapture.tools instrumentation --toml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Provided instrumentation
|
|
93
|
+
|
|
94
|
+
| Target | Supported versions | Records | Settings |
|
|
95
|
+
| ------ | ------------------ | ------- | -------- |
|
|
96
|
+
| `flask` | Flask 3.x | Every request as one tree (the recording WSGI middleware installed on each application at construction), every view function beneath its request (observed as routes register, blueprints and `MethodView`s included), and the exception Flask catches on its way to a 500 noted on the request. | none yet |
|
|
97
|
+
|
|
98
|
+
The entry point name is the config's `name`; the table says what each
|
|
99
|
+
instrumentation does in the current cut. Settings, further choke
|
|
100
|
+
points and wider version ranges are being added target by target.
|
|
101
|
+
|
|
102
|
+
## Adding a target
|
|
103
|
+
|
|
104
|
+
Each target lives in its own subpackage under
|
|
105
|
+
`src/wrapture_instrumentation/`, named `<category>_<target>`:
|
|
106
|
+
`framework_flask`, `external_requests`, `database_sqlite3`. The
|
|
107
|
+
category says what kind of thing the target is and, with it, which
|
|
108
|
+
part of wrapture the instrumentation mostly uses:
|
|
109
|
+
|
|
110
|
+
- `framework_`: web frameworks, and their extensions as compound
|
|
111
|
+
names (`framework_flask_restful`).
|
|
112
|
+
- `external_`: outbound HTTP and RPC clients and service SDKs.
|
|
113
|
+
- `database_`: DB-API drivers and SQL toolkits.
|
|
114
|
+
- `datastore_`: other stores and caches.
|
|
115
|
+
- `task_`: task queues. `messaging_`: brokers and their clients.
|
|
116
|
+
- `server_`: WSGI and ASGI servers. `template_`: template engines.
|
|
117
|
+
|
|
118
|
+
A new category is added when a target fits none of these. The
|
|
119
|
+
directory name is internal; the entry point name, and so the name a
|
|
120
|
+
config uses, is always the bare target.
|
|
121
|
+
|
|
122
|
+
The subpackage's `__init__.py` holds one `wrapture.Instrumentation`
|
|
123
|
+
subclass and imports only wrapture; everything that touches the
|
|
124
|
+
target lives in `hooks.py`, reached by an import inside `apply()`.
|
|
125
|
+
The class is registered in `pyproject.toml` under
|
|
126
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
127
|
+
test suite under `tests/<category>_<target>/`. The
|
|
128
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
129
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
130
|
+
here covers the tests.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
BSD-2-Clause, as wrapture.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
wrapture_instrumentation/__init__.py,sha256=nwGba4lOYVvBYcR7FYxR5cqYiVUmCGW2_ao66slVMYg,783
|
|
2
|
+
wrapture_instrumentation/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
wrapture_instrumentation/framework_flask/__init__.py,sha256=DtfT6EALymqOhKboyn5XToJQKch_C781kMSDz0IbZqs,1108
|
|
4
|
+
wrapture_instrumentation/framework_flask/hooks.py,sha256=EtS_Rw_IIiQp_sHICupAC08NgGTqzEo0k8uZn7Sa2pw,4534
|
|
5
|
+
wrapture_instrumentation-1.0.0.dev1.dist-info/licenses/LICENSE,sha256=RPMyRnXwgrw9co5o6wY9G3DKlb-oBZ44tpiRnuYeec8,1299
|
|
6
|
+
wrapture_instrumentation-1.0.0.dev1.dist-info/METADATA,sha256=xqT_KxyHckRsVLxPS2SRaoozBoPCnXVMunRV1L3L5ns,5353
|
|
7
|
+
wrapture_instrumentation-1.0.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
wrapture_instrumentation-1.0.0.dev1.dist-info/entry_points.txt,sha256=UZXOmg--ham7LOqtX-KHrGPo2TZxz7ttUI6UTbcFCMo,97
|
|
9
|
+
wrapture_instrumentation-1.0.0.dev1.dist-info/top_level.txt,sha256=IIkfVp5ZVFBXZhNnU-lt7OnulEuRBy6EGW9Ci2q3B-o,25
|
|
10
|
+
wrapture_instrumentation-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
|