wrapture-instrumentation 1.0.0.dev2__tar.gz → 1.0.0.dev4__tar.gz

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.
Files changed (52) hide show
  1. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/CHANGES.md +5 -2
  2. {wrapture_instrumentation-1.0.0.dev2/src/wrapture_instrumentation.egg-info → wrapture_instrumentation-1.0.0.dev4}/PKG-INFO +2 -2
  3. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/pyproject.toml +1 -1
  4. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/__init__.py +1 -1
  5. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/README.md +7 -4
  6. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/app.py +27 -48
  7. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4/src/wrapture_instrumentation.egg-info}/PKG-INFO +2 -2
  8. wrapture_instrumentation-1.0.0.dev4/src/wrapture_instrumentation.egg-info/requires.txt +1 -0
  9. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_request_options.py +13 -7
  10. wrapture_instrumentation-1.0.0.dev2/src/wrapture_instrumentation.egg-info/requires.txt +0 -1
  11. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/LICENSE +0 -0
  12. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/MANIFEST.in +0 -0
  13. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/README.md +0 -0
  14. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/TESTING.md +0 -0
  15. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/demo/__init__.py +0 -0
  16. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/demo/framework_flask.py +0 -0
  17. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/demo/template_jinja2.py +0 -0
  18. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/setup.cfg +0 -0
  19. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/__init__.py +0 -0
  20. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/blueprints.py +0 -0
  21. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/common.py +0 -0
  22. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/scaffold.py +0 -0
  23. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/framework_flask/templating.py +0 -0
  24. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/py.typed +0 -0
  25. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/template_jinja2/README.md +0 -0
  26. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/template_jinja2/__init__.py +0 -0
  27. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation/template_jinja2/environment.py +0 -0
  28. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation.egg-info/SOURCES.txt +0 -0
  29. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation.egg-info/dependency_links.txt +0 -0
  30. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation.egg-info/entry_points.txt +0 -0
  31. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation.egg-info/not-zip-safe +0 -0
  32. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/src/wrapture_instrumentation.egg-info/top_level.txt +0 -0
  33. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/__init__.py +0 -0
  34. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/conftest.py +0 -0
  35. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/__init__.py +0 -0
  36. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/portal.py +0 -0
  37. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/shop.py +0 -0
  38. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_apply_remove.py +0 -0
  39. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_class.py +0 -0
  40. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_lifecycle.py +0 -0
  41. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_registration.py +0 -0
  42. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_templating.py +0 -0
  43. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/framework_flask/test_tracing.py +0 -0
  44. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/__init__.py +0 -0
  45. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/test_apply_remove.py +0 -0
  46. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/test_class.py +0 -0
  47. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/test_registration.py +0 -0
  48. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/test_rendering.py +0 -0
  49. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/template_jinja2/test_with_flask.py +0 -0
  50. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/test_package.py +0 -0
  51. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/test_wsgi.py +0 -0
  52. {wrapture_instrumentation-1.0.0.dev2 → wrapture_instrumentation-1.0.0.dev4}/tests/wsgi.py +0 -0
@@ -15,7 +15,9 @@ In development.
15
15
 
16
16
  - Flask (`flask`, Flask 3.x): every request records as one tree
17
17
  through the recording WSGI middleware installed at construction,
18
- annotated with the matched route pattern and endpoint; every view
18
+ annotated with the matched route pattern and endpoint (the route
19
+ is what wrapture's OpenTelemetry export names the span by, as
20
+ `http.route`); every view
19
21
  function is observed as it registers and labelled by its endpoint
20
22
  (blueprints and `MethodView`s included); lifecycle callbacks and
21
23
  error handlers are observed however they register; and failures
@@ -29,6 +31,7 @@ In development.
29
31
  settings switch the optional layers, all on by default:
30
32
  `lifecycle`, `handled_errors` and `templates`; two more shape what
31
33
  a recorded request carries: `ignore_paths` (path globs whose
32
- requests record nothing, request and view alike) and `redact`
34
+ requests record nothing at all, request and everything beneath it
35
+ alike) and `redact`
33
36
  (query parameters masked by name on top of the built-in sensitive
34
37
  set).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wrapture-instrumentation
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev4
4
4
  Summary: Instrumentation for common Python packages, applied through wrapture.
5
5
  Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
6
6
  License-Expression: BSD-2-Clause
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: wrapture>=1.0.0a5
20
+ Requires-Dist: wrapture>=1.0.0a12
21
21
  Dynamic: license-file
22
22
 
23
23
  # wrapture-instrumentation
@@ -27,7 +27,7 @@ classifiers = [
27
27
  # install Flask, and whether a target is present and at a supported
28
28
  # version is checked by wrapture at apply time from the class data.
29
29
  dependencies = [
30
- "wrapture>=1.0.0a5",
30
+ "wrapture>=1.0.0a12",
31
31
  ]
32
32
 
33
33
  [project.urls]
@@ -22,5 +22,5 @@ def _format_version(parts: tuple[str, ...]) -> str:
22
22
  )
23
23
 
24
24
 
25
- __version_info__ = ("1", "0", "0", "dev2")
25
+ __version_info__ = ("1", "0", "0", "dev4")
26
26
  __version__ = _format_version(__version_info__)
@@ -71,7 +71,10 @@ GET /shaky (portal.wsgi_app) -> '422 UNPROCESSABLE ENTITY' !! ValueError [671
71
71
  matched route pattern (`route = "/quote/<item>"`) and endpoint
72
72
  (`endpoint = "quoted"`), the low-cardinality keys to group by; the
73
73
  raw path stays as the per-request detail. A request that matched no
74
- route (a 404) has neither key.
74
+ route (a 404) has neither key. wrapture's OpenTelemetry export
75
+ reads `route` as the semantic-convention `http.route` and names the
76
+ request span by it (`GET /quote/<item>`), so a backend groups by
77
+ endpoint rather than by URL.
75
78
 
76
79
  - Every view function is observed as its route registers, wherever it
77
80
  came from: plain functions, `view_func` by keyword or position,
@@ -138,7 +141,7 @@ a layer:
138
141
 
139
142
  | Setting | Default | Controls |
140
143
  | ------- | ------- | -------- |
141
- | `ignore_paths` | `[]` | Request paths not to record, as path globs (`"/health"`, `"/static/*"`). A matching request runs and answers as normal but records nothing: the middleware skips the request event, and the same predicate rides on the observed views, so no stray view roots appear. Lifecycle callbacks observed under the `lifecycle` switch still record on ignored paths. |
144
+ | `ignore_paths` | `[]` | Request paths not to record, as path globs (`"/health"`, `"/static/*"`). A matching request runs and answers as normal but records nothing at all: the request event is skipped and everything beneath it is silenced for the request's whole extent, its view, lifecycle callbacks, error handlers and template renders included, so no stray roots appear. It is wrapture's `filter_requests(ignore={"path": [...]})` with `tree=True` on the middleware. |
142
145
  | `redact` | `[]` | Query string parameters to mask by name in the recorded query, on top of the built-in sensitive set (passwords, tokens, keys and session ids are always masked). The parameter still reaches the application; only the recording is masked. |
143
146
  | `lifecycle` | `true` | Observing before/after/teardown callbacks as they register. Every registered callback runs on every request (extensions register these liberally: user loaders, session cleanup, header stamping), so this is the layer to switch off when the trees are noisier than they are informative. The callbacks still run; they run unobserved. |
144
147
  | `handled_errors` | `true` | Noting an exception a registered handler absorbed against its request. The handler's own run is core and stays observed either way. |
@@ -163,8 +166,8 @@ lifecycle = false
163
166
  - Flask signals: blinker plumbing; bind the receivers ad hoc with
164
167
  wrapture if you need them.
165
168
  - `jsonify`, `send_file`, `send_static_file`: response plumbing;
166
- static file traffic is better excluded wholesale once
167
- `ignore_paths` lands.
169
+ static file traffic is better excluded wholesale with
170
+ `ignore_paths`.
168
171
 
169
172
  ## How it patches
170
173
 
@@ -8,7 +8,10 @@ their own calls.
8
8
  instance's wsgi_app attribute, the documented place Flask middleware
9
9
  goes, so every request records as one "request" event however the
10
10
  instance was made: module level, an application factory, several
11
- applications in one process.
11
+ applications in one process. The ignore_paths setting rides on the
12
+ middleware as a filter_requests() filter with tree=True, so an
13
+ ignored request records nothing at all, its view, callbacks and
14
+ template renders included.
12
15
 
13
16
  - Flask.add_url_rule substitutes wrapture.observed(view_func) as
14
17
  routes register, labelled with the route's endpoint, so every view
@@ -64,7 +67,6 @@ manager, take the whole thing down again.
64
67
 
65
68
  from __future__ import annotations
66
69
 
67
- import fnmatch
68
70
  from collections.abc import Callable, Sequence
69
71
  from typing import Any
70
72
 
@@ -73,42 +75,24 @@ import wrapture
73
75
  from .common import observing_registration
74
76
 
75
77
 
76
- def _declines(ignore_paths: Sequence[str]) -> Callable[..., bool] | None:
77
- """The when= predicate for the ignore_paths setting, or None when
78
- the setting is empty: record unless the request path matches one
79
- of the globs. Shared by the middleware and the observed views, so
80
- an ignored request records neither its request event nor its
81
- view."""
82
-
83
- if not ignore_paths:
84
- return None
85
-
86
- patterns = tuple(ignore_paths)
87
-
88
- def wanted(instance: Any, args: tuple[Any, ...], kwargs: Any) -> bool:
89
- # The view form of the predicate: read the path from the
90
- # request context (the middleware form gets the environ
91
- # directly and never reaches here). A call outside a request
92
- # context is not an ignorable request; record it.
93
-
94
- from flask import has_request_context, request
95
-
96
- if not has_request_context():
97
- return True
98
-
99
- return not any(
100
- fnmatch.fnmatchcase(request.path, pattern) for pattern in patterns
101
- )
102
-
103
- return wanted
104
-
105
-
106
78
  def wrap_app(ignore_paths: Sequence[str], redact: Sequence[str]) -> Callable[..., Any]:
107
79
  """Build the Flask.__init__ decorator that installs the recording
108
80
  middleware on each new instance's wsgi_app, carrying the
109
- ignore_paths and redact settings into it."""
81
+ ignore_paths and redact settings into it.
82
+
83
+ ignore_paths becomes a filter_requests() filter on the middleware's
84
+ when=, with tree=True so that a declined request silences
85
+ everything beneath it for its whole extent; without the flag the
86
+ request's view, callbacks and renders would still record, each as
87
+ a parentless root. tree= is only valid alongside a when=, so both
88
+ are left unset when there is nothing to ignore.
89
+ """
110
90
 
111
- when = list(ignore_paths) or None
91
+ request_filter = (
92
+ wrapture.filter_requests(ignore={"path": list(ignore_paths)})
93
+ if ignore_paths
94
+ else None
95
+ )
112
96
  policy = wrapture.redact(*redact) if redact else None
113
97
 
114
98
  def install(
@@ -122,7 +106,8 @@ def wrap_app(ignore_paths: Sequence[str], redact: Sequence[str]) -> Callable[...
122
106
  instance.wsgi_app = wrapture.WSGIMiddleware(
123
107
  instance.wsgi_app,
124
108
  label=f"{instance.name}.wsgi_app",
125
- when=when,
109
+ when=request_filter,
110
+ tree=request_filter is not None,
126
111
  capture_args=policy,
127
112
  )
128
113
 
@@ -147,34 +132,28 @@ def _endpoint(args: tuple[Any, ...], kwargs: dict[str, Any], view: Any) -> str:
147
132
  return str(endpoint) if endpoint is not None else view.__name__
148
133
 
149
134
 
150
- def wrap_view(ignore_paths: Sequence[str]) -> Callable[..., Any]:
135
+ def wrap_view() -> Callable[..., Any]:
151
136
  """Build the add_url_rule transformer that substitutes an observed
152
137
  view function, labelled by its endpoint, into each registration.
153
138
 
154
139
  The signature is add_url_rule(rule, endpoint=None, view_func=None,
155
140
  ...), so the view is either the view_func keyword or the third
156
141
  positional argument; a registration without a view (an endpoint
157
- name alone) passes through untouched. The ignore_paths predicate
158
- rides on each observed view, so an ignored request's view goes
159
- unrecorded along with its request event.
142
+ name alone) passes through untouched. The view needs no predicate
143
+ of its own: an ignored request is silenced beneath the middleware,
144
+ view included.
160
145
  """
161
146
 
162
- when = _declines(ignore_paths)
163
-
164
147
  def transform(
165
148
  args: tuple[Any, ...], kwargs: dict[str, Any]
166
149
  ) -> tuple[tuple[Any, ...], dict[str, Any]]:
167
150
  if kwargs.get("view_func") is not None:
168
151
  view = kwargs["view_func"]
169
- observed = wrapture.observed(
170
- view, label=_endpoint(args, kwargs, view), when=when
171
- )
152
+ observed = wrapture.observed(view, label=_endpoint(args, kwargs, view))
172
153
  kwargs = dict(kwargs, view_func=observed)
173
154
  elif len(args) >= 3 and args[2] is not None:
174
155
  view = args[2]
175
- observed = wrapture.observed(
176
- view, label=_endpoint(args, kwargs, view), when=when
177
- )
156
+ observed = wrapture.observed(view, label=_endpoint(args, kwargs, view))
178
157
  args = (*args[:2], observed, *args[3:])
179
158
 
180
159
  return args, kwargs
@@ -261,7 +240,7 @@ def instrument(module: Any, instrumentation: wrapture.Instrumentation) -> None:
261
240
  )
262
241
 
263
242
  registrar = wrapture.binding(module.Flask, "add_url_rule", when=False)
264
- registrar.on_call.transforms_args(wrap_view(settings["ignore_paths"]))
243
+ registrar.on_call.transforms_args(wrap_view())
265
244
 
266
245
  router = wrapture.binding(module.Flask, "preprocess_request", when=False)
267
246
  router.on_call.decorates(annotate_route)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wrapture-instrumentation
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev4
4
4
  Summary: Instrumentation for common Python packages, applied through wrapture.
5
5
  Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
6
6
  License-Expression: BSD-2-Clause
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: wrapture>=1.0.0a5
20
+ Requires-Dist: wrapture>=1.0.0a12
21
21
  Dynamic: license-file
22
22
 
23
23
  # wrapture-instrumentation
@@ -12,7 +12,8 @@ from wrapture_instrumentation.framework_flask import FlaskInstrumentation
12
12
 
13
13
 
14
14
  def make_probe() -> flask.Flask:
15
- """An application with a health endpoint beside a working route."""
15
+ """An application with a health endpoint beside a working route,
16
+ and a lifecycle callback that runs on every request."""
16
17
 
17
18
  app = flask.Flask("probe")
18
19
 
@@ -22,6 +23,10 @@ def make_probe() -> flask.Flask:
22
23
  def work() -> str:
23
24
  return "done"
24
25
 
26
+ def stamp() -> None:
27
+ return None
28
+
29
+ app.before_request(stamp)
25
30
  app.add_url_rule("/health", "health", health)
26
31
  app.add_url_rule("/static/<name>", "asset", lambda name: name)
27
32
  app.add_url_rule("/work", "work", work)
@@ -30,10 +35,10 @@ def make_probe() -> flask.Flask:
30
35
 
31
36
 
32
37
  def test_ignored_paths_record_nothing_and_still_answer() -> None:
33
- # An ignored request leaves no events at all: the middleware skips
34
- # the request event and the same predicate rides on the observed
35
- # view, so neither shows up as a stray root. The application still
36
- # runs and answers.
38
+ # An ignored request leaves no events at all: the middleware's
39
+ # filter declines the request with tree=True, so the view and the
40
+ # lifecycle callback beneath it are silenced rather than left as
41
+ # stray roots. The application still runs and answers.
37
42
 
38
43
  with (
39
44
  instrumentation(FlaskInstrumentation, ignore_paths=["/health", "/static/*"]),
@@ -53,6 +58,7 @@ def test_ignored_paths_record_nothing_and_still_answer() -> None:
53
58
 
54
59
  assert [(event.kind, event.label or event.path) for event in tape.all] == [
55
60
  ("request", "probe.wsgi_app"),
61
+ ("call", f"{__name__}:make_probe.<locals>.stamp"),
56
62
  ("call", "work"),
57
63
  ]
58
64
 
@@ -67,7 +73,7 @@ def test_redact_masks_named_query_parameters() -> None:
67
73
  ):
68
74
  request(make_probe(), "GET", "/work", query="voucher=SECRET50&limit=5&token=t")
69
75
 
70
- (seen, _) = tape.all
76
+ seen = tape.all[0]
71
77
  assert seen.data["query"] == "voucher=<redacted>&limit=5&token=<redacted>"
72
78
  assert "SECRET50" not in repr(seen.data)
73
79
 
@@ -101,6 +107,6 @@ def test_defaults_ignore_nothing_and_redact_only_the_built_ins() -> None:
101
107
  with instrumentation(FlaskInstrumentation), timeline() as tape:
102
108
  request(make_probe(), "GET", "/health", query="voucher=v&api_key=k")
103
109
 
104
- (seen, _) = tape.all
110
+ seen = tape.all[0]
105
111
  assert seen.data["path"] == "/health"
106
112
  assert seen.data["query"] == "voucher=v&api_key=<redacted>"