wrapture-instrumentation-aws 1.0.0.dev1__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.
- wrapture_instrumentation_aws-1.0.0.dev1/CHANGES.md +9 -0
- wrapture_instrumentation_aws-1.0.0.dev1/LICENSE +24 -0
- wrapture_instrumentation_aws-1.0.0.dev1/MANIFEST.in +7 -0
- wrapture_instrumentation_aws-1.0.0.dev1/PKG-INFO +159 -0
- wrapture_instrumentation_aws-1.0.0.dev1/README.md +136 -0
- wrapture_instrumentation_aws-1.0.0.dev1/TESTING.md +197 -0
- wrapture_instrumentation_aws-1.0.0.dev1/demo/__init__.py +0 -0
- wrapture_instrumentation_aws-1.0.0.dev1/demo/botocore.py +136 -0
- wrapture_instrumentation_aws-1.0.0.dev1/pyproject.toml +127 -0
- wrapture_instrumentation_aws-1.0.0.dev1/setup.cfg +4 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/__init__.py +26 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/botocore/README.md +131 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/botocore/__init__.py +52 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/botocore/client.py +95 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/botocore/common.py +69 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/botocore/services.py +171 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws/py.typed +0 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/PKG-INFO +159 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/SOURCES.txt +32 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/dependency_links.txt +1 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/entry_points.txt +2 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/not-zip-safe +1 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/requires.txt +1 -0
- wrapture_instrumentation_aws-1.0.0.dev1/src/wrapture_instrumentation_aws.egg-info/top_level.txt +1 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/__init__.py +0 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/__init__.py +0 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/conftest.py +49 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/test_apply_remove.py +58 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/test_class.py +70 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/test_composition.py +168 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/test_recording.py +190 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/botocore/test_registration.py +81 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/conftest.py +88 -0
- wrapture_instrumentation_aws-1.0.0.dev1/tests/test_package.py +181 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
## Version 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release of wrapture-instrumentation-aws. Everything is new in
|
|
6
|
+
this version, so rather than listing changes, see the
|
|
7
|
+
[README](README.md) for what the package provides: the table of
|
|
8
|
+
covered targets there links to each target's own notes describing
|
|
9
|
+
what it records and its settings.
|
|
@@ -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,7 @@
|
|
|
1
|
+
# setuptools includes test_*.py files by default but not the helpers
|
|
2
|
+
# they import (conftest.py, the package __init__.py), so the tests
|
|
3
|
+
# directory is taken whole, with the repository's other notes.
|
|
4
|
+
graft tests
|
|
5
|
+
graft demo
|
|
6
|
+
include CHANGES.md TESTING.md
|
|
7
|
+
global-exclude __pycache__ *.py[cod]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrapture-instrumentation-aws
|
|
3
|
+
Version: 1.0.0.dev1
|
|
4
|
+
Summary: Instrumentation for the AWS SDK (boto3 and botocore), 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-aws
|
|
8
|
+
Project-URL: Documentation, https://wrapture.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture-instrumentation-aws/issues/
|
|
10
|
+
Keywords: wrapture,instrumentation,aws,boto3,botocore,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-aws
|
|
25
|
+
|
|
26
|
+
Instrumentation for the AWS SDK (boto3 and botocore), 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 AWS package in that collection: one `wrapture.Instrumentation`
|
|
33
|
+
class for the AWS SDK, so tracing every call your application makes to
|
|
34
|
+
S3, DynamoDB, SQS and the rest is one config entry and no code.
|
|
35
|
+
|
|
36
|
+
> **Status: alpha, ahead of 1.0.0.** Developed against wrapture's
|
|
37
|
+
> alpha series, with pre-releases published to
|
|
38
|
+
> [PyPI](https://pypi.org/project/wrapture-instrumentation-aws/), and
|
|
39
|
+
> until 1.0.0 is final a plain `pip install
|
|
40
|
+
> wrapture-instrumentation-aws` picks up the latest pre-release
|
|
41
|
+
> automatically, so there is no need to pin a specific version.
|
|
42
|
+
|
|
43
|
+
## Why a separate package
|
|
44
|
+
|
|
45
|
+
The core
|
|
46
|
+
[wrapture-instrumentation](https://github.com/GrahamDumpleton/wrapture-instrumentation)
|
|
47
|
+
package deliberately covers only the standard library and third-party
|
|
48
|
+
packages that can be exercised in-process, with no separate backend
|
|
49
|
+
product or service needed to test against. AWS is exactly the kind of
|
|
50
|
+
product the separate-package rule was drawn for. This package carries
|
|
51
|
+
its own heavier test dependencies (moto, which reimplements the AWS
|
|
52
|
+
APIs in memory and pulls in cryptography, PyYAML, Flask and werkzeug)
|
|
53
|
+
and its own release cadence, so the core package's free-threaded test
|
|
54
|
+
matrix stays light. It will grow to hold the AWS SDK's async twin
|
|
55
|
+
(aiobotocore) and a messaging trace-propagation layer.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ pip install wrapture-instrumentation-aws
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Installing it brings wrapture and nothing else. boto3, botocore and
|
|
64
|
+
moto are not dependencies: the instrumentation is inert until botocore
|
|
65
|
+
is present, and wrapture checks the installed version against the
|
|
66
|
+
range the instrumentation supports at apply time.
|
|
67
|
+
|
|
68
|
+
## Using it
|
|
69
|
+
|
|
70
|
+
An `[[instrument]]` entry in `wrapture.toml` names the target:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[[instrument]]
|
|
74
|
+
name = "botocore"
|
|
75
|
+
|
|
76
|
+
[[sink]]
|
|
77
|
+
type = "printer"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
and the runner applies it before the application starts, so the patch
|
|
81
|
+
is in place before boto3 is imported:
|
|
82
|
+
|
|
83
|
+
```console
|
|
84
|
+
$ python -m wrapture -m myapp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The entry point name is `botocore`, the seam every AWS call passes
|
|
88
|
+
through: boto3 is sugar over botocore, so a config that traces
|
|
89
|
+
botocore traces boto3. The same config works through
|
|
90
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
91
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`); through
|
|
92
|
+
[manual setup](https://wrapture.readthedocs.io/en/latest/manual-setup.html),
|
|
93
|
+
a few lines in the application's own startup where wrapping the launch
|
|
94
|
+
from outside is awkward; and, in a test, through
|
|
95
|
+
`wrapture.instrumentation("botocore")` scoping the instrumentation to
|
|
96
|
+
a block. The
|
|
97
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
98
|
+
covers the config file itself.
|
|
99
|
+
|
|
100
|
+
To see what is installed, what it supports in the current
|
|
101
|
+
environment, and what settings it takes:
|
|
102
|
+
|
|
103
|
+
```console
|
|
104
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Provided instrumentation
|
|
108
|
+
|
|
109
|
+
| Target | Supported versions | Records | Settings |
|
|
110
|
+
| ------ | ------------------ | ------- | -------- |
|
|
111
|
+
| [`botocore`](https://github.com/GrahamDumpleton/wrapture-instrumentation-aws/blob/develop/src/wrapture_instrumentation_aws/botocore/README.md) | botocore 1.34+ (1.x), and boto3 above it | Every AWS API call as one event at the SDK's single dispatch seam, whichever service or client made it, named `service/operation` (`s3/GetObject`) and categorised per service (S3 external, DynamoDB a datastore, SQS/SNS/Kinesis messaging, Lambda and Step Functions tasks); carrying the system, service, operation, region, endpoint host and port, the resource identifiers (bucket and key, table, queue, topic, stream, function), and from the response the status, request id and any retry count; a failing call's `ClientError` recorded as the exception with its status and AWS error code beside it. Retries fold into the one call; a paginator or waiter is one event per underlying call. The parameters reduce to a count and the response to its type: payloads, items and message bodies are never recorded. | `leaf` |
|
|
112
|
+
|
|
113
|
+
The entry point name is the config's `name`; the linked per-target
|
|
114
|
+
README is the full user documentation: what records, what the events
|
|
115
|
+
carry, the setting, and what is deliberately not traced.
|
|
116
|
+
|
|
117
|
+
## What is not traced
|
|
118
|
+
|
|
119
|
+
By design, and where it goes:
|
|
120
|
+
|
|
121
|
+
- No trace identity is propagated to AWS in this version. A silenced
|
|
122
|
+
urllib3 beneath the call injects none either, by wrapture's
|
|
123
|
+
propagation-follows-recording contract, so AWS is never handed
|
|
124
|
+
headers it would not understand. Trace context into SQS, SNS and
|
|
125
|
+
Kinesis message attributes, and X-Ray `X-Amzn-Trace-Id`
|
|
126
|
+
propagation, are planned for later versions.
|
|
127
|
+
|
|
128
|
+
- The async SDK (aiobotocore, aioboto3) is a separate top-level
|
|
129
|
+
package and will get its own `Instrumentation` class and
|
|
130
|
+
`aiobotocore` entry point in this repository, once the sync target
|
|
131
|
+
is whole.
|
|
132
|
+
|
|
133
|
+
- s3transfer's multipart uploads fan out to worker threads that do
|
|
134
|
+
not carry the recording context, so those individual calls may not
|
|
135
|
+
nest under the caller.
|
|
136
|
+
|
|
137
|
+
- Bedrock and other LLM telemetry are out of scope.
|
|
138
|
+
|
|
139
|
+
## Adding a target
|
|
140
|
+
|
|
141
|
+
The AWS SDK's one dispatch seam fronts every service, so a single
|
|
142
|
+
target covers them all; a future async twin (aiobotocore) would be a
|
|
143
|
+
second subpackage and entry point here. The subpackage's `__init__.py`
|
|
144
|
+
holds one `wrapture.Instrumentation` subclass and imports only
|
|
145
|
+
wrapture; everything that touches botocore lives in sibling modules
|
|
146
|
+
(`client.py` for the seam, `services.py` for the per-service table,
|
|
147
|
+
`common.py` for the capture policy), imported inside the hook. The
|
|
148
|
+
class is registered in `pyproject.toml` under
|
|
149
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
150
|
+
test suite under `tests/<target>/` and a `README.md` linked from the
|
|
151
|
+
table above. The
|
|
152
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
153
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
154
|
+
here covers the tests.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
BSD 2-Clause. See
|
|
159
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture-instrumentation-aws/blob/develop/LICENSE).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# wrapture-instrumentation-aws
|
|
2
|
+
|
|
3
|
+
Instrumentation for the AWS SDK (boto3 and botocore), applied through
|
|
4
|
+
[wrapture](https://github.com/GrahamDumpleton/wrapture).
|
|
5
|
+
|
|
6
|
+
wrapture attaches bindings to arbitrary Python call sites without
|
|
7
|
+
modifying the code being observed, and its config layer can switch on
|
|
8
|
+
packaged instrumentation for a third-party package by name. This is
|
|
9
|
+
the AWS package in that collection: one `wrapture.Instrumentation`
|
|
10
|
+
class for the AWS SDK, so tracing every call your application makes to
|
|
11
|
+
S3, DynamoDB, SQS and the rest is one config entry and no code.
|
|
12
|
+
|
|
13
|
+
> **Status: alpha, ahead of 1.0.0.** Developed against wrapture's
|
|
14
|
+
> alpha series, with pre-releases published to
|
|
15
|
+
> [PyPI](https://pypi.org/project/wrapture-instrumentation-aws/), and
|
|
16
|
+
> until 1.0.0 is final a plain `pip install
|
|
17
|
+
> wrapture-instrumentation-aws` picks up the latest pre-release
|
|
18
|
+
> automatically, so there is no need to pin a specific version.
|
|
19
|
+
|
|
20
|
+
## Why a separate package
|
|
21
|
+
|
|
22
|
+
The core
|
|
23
|
+
[wrapture-instrumentation](https://github.com/GrahamDumpleton/wrapture-instrumentation)
|
|
24
|
+
package deliberately covers only the standard library and third-party
|
|
25
|
+
packages that can be exercised in-process, with no separate backend
|
|
26
|
+
product or service needed to test against. AWS is exactly the kind of
|
|
27
|
+
product the separate-package rule was drawn for. This package carries
|
|
28
|
+
its own heavier test dependencies (moto, which reimplements the AWS
|
|
29
|
+
APIs in memory and pulls in cryptography, PyYAML, Flask and werkzeug)
|
|
30
|
+
and its own release cadence, so the core package's free-threaded test
|
|
31
|
+
matrix stays light. It will grow to hold the AWS SDK's async twin
|
|
32
|
+
(aiobotocore) and a messaging trace-propagation layer.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
$ pip install wrapture-instrumentation-aws
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Installing it brings wrapture and nothing else. boto3, botocore and
|
|
41
|
+
moto are not dependencies: the instrumentation is inert until botocore
|
|
42
|
+
is present, and wrapture checks the installed version against the
|
|
43
|
+
range the instrumentation supports at apply time.
|
|
44
|
+
|
|
45
|
+
## Using it
|
|
46
|
+
|
|
47
|
+
An `[[instrument]]` entry in `wrapture.toml` names the target:
|
|
48
|
+
|
|
49
|
+
```toml
|
|
50
|
+
[[instrument]]
|
|
51
|
+
name = "botocore"
|
|
52
|
+
|
|
53
|
+
[[sink]]
|
|
54
|
+
type = "printer"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
and the runner applies it before the application starts, so the patch
|
|
58
|
+
is in place before boto3 is imported:
|
|
59
|
+
|
|
60
|
+
```console
|
|
61
|
+
$ python -m wrapture -m myapp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The entry point name is `botocore`, the seam every AWS call passes
|
|
65
|
+
through: boto3 is sugar over botocore, so a config that traces
|
|
66
|
+
botocore traces boto3. The same config works through
|
|
67
|
+
[autowrapt](https://github.com/GrahamDumpleton/autowrapt) injection
|
|
68
|
+
(`AUTOWRAPT_BOOTSTRAP=wrapture python myapp.py`); through
|
|
69
|
+
[manual setup](https://wrapture.readthedocs.io/en/latest/manual-setup.html),
|
|
70
|
+
a few lines in the application's own startup where wrapping the launch
|
|
71
|
+
from outside is awkward; and, in a test, through
|
|
72
|
+
`wrapture.instrumentation("botocore")` scoping the instrumentation to
|
|
73
|
+
a block. The
|
|
74
|
+
[ad-hoc tracing guide](https://wrapture.readthedocs.io/en/latest/ad-hoc-tracing.html)
|
|
75
|
+
covers the config file itself.
|
|
76
|
+
|
|
77
|
+
To see what is installed, what it supports in the current
|
|
78
|
+
environment, and what settings it takes:
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
$ python -m wrapture.tools instrumentation --verbose
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Provided instrumentation
|
|
85
|
+
|
|
86
|
+
| Target | Supported versions | Records | Settings |
|
|
87
|
+
| ------ | ------------------ | ------- | -------- |
|
|
88
|
+
| [`botocore`](https://github.com/GrahamDumpleton/wrapture-instrumentation-aws/blob/develop/src/wrapture_instrumentation_aws/botocore/README.md) | botocore 1.34+ (1.x), and boto3 above it | Every AWS API call as one event at the SDK's single dispatch seam, whichever service or client made it, named `service/operation` (`s3/GetObject`) and categorised per service (S3 external, DynamoDB a datastore, SQS/SNS/Kinesis messaging, Lambda and Step Functions tasks); carrying the system, service, operation, region, endpoint host and port, the resource identifiers (bucket and key, table, queue, topic, stream, function), and from the response the status, request id and any retry count; a failing call's `ClientError` recorded as the exception with its status and AWS error code beside it. Retries fold into the one call; a paginator or waiter is one event per underlying call. The parameters reduce to a count and the response to its type: payloads, items and message bodies are never recorded. | `leaf` |
|
|
89
|
+
|
|
90
|
+
The entry point name is the config's `name`; the linked per-target
|
|
91
|
+
README is the full user documentation: what records, what the events
|
|
92
|
+
carry, the setting, and what is deliberately not traced.
|
|
93
|
+
|
|
94
|
+
## What is not traced
|
|
95
|
+
|
|
96
|
+
By design, and where it goes:
|
|
97
|
+
|
|
98
|
+
- No trace identity is propagated to AWS in this version. A silenced
|
|
99
|
+
urllib3 beneath the call injects none either, by wrapture's
|
|
100
|
+
propagation-follows-recording contract, so AWS is never handed
|
|
101
|
+
headers it would not understand. Trace context into SQS, SNS and
|
|
102
|
+
Kinesis message attributes, and X-Ray `X-Amzn-Trace-Id`
|
|
103
|
+
propagation, are planned for later versions.
|
|
104
|
+
|
|
105
|
+
- The async SDK (aiobotocore, aioboto3) is a separate top-level
|
|
106
|
+
package and will get its own `Instrumentation` class and
|
|
107
|
+
`aiobotocore` entry point in this repository, once the sync target
|
|
108
|
+
is whole.
|
|
109
|
+
|
|
110
|
+
- s3transfer's multipart uploads fan out to worker threads that do
|
|
111
|
+
not carry the recording context, so those individual calls may not
|
|
112
|
+
nest under the caller.
|
|
113
|
+
|
|
114
|
+
- Bedrock and other LLM telemetry are out of scope.
|
|
115
|
+
|
|
116
|
+
## Adding a target
|
|
117
|
+
|
|
118
|
+
The AWS SDK's one dispatch seam fronts every service, so a single
|
|
119
|
+
target covers them all; a future async twin (aiobotocore) would be a
|
|
120
|
+
second subpackage and entry point here. The subpackage's `__init__.py`
|
|
121
|
+
holds one `wrapture.Instrumentation` subclass and imports only
|
|
122
|
+
wrapture; everything that touches botocore lives in sibling modules
|
|
123
|
+
(`client.py` for the seam, `services.py` for the per-service table,
|
|
124
|
+
`common.py` for the capture policy), imported inside the hook. The
|
|
125
|
+
class is registered in `pyproject.toml` under
|
|
126
|
+
`[project.entry-points."wrapture.instrumentation"]`, and gets its own
|
|
127
|
+
test suite under `tests/<target>/` and a `README.md` linked from the
|
|
128
|
+
table above. The
|
|
129
|
+
[instrumentation packages](https://wrapture.readthedocs.io/en/latest/instrumentation-packages.html)
|
|
130
|
+
page of the wrapture documentation is the full contract; TESTING.md
|
|
131
|
+
here covers the tests.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
BSD 2-Clause. See
|
|
136
|
+
[LICENSE](https://github.com/GrahamDumpleton/wrapture-instrumentation-aws/blob/develop/LICENSE).
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Where the tests are
|
|
4
|
+
|
|
5
|
+
Tests live in the [tests/](tests/) directory at the top of the
|
|
6
|
+
repository, separate from the package code in
|
|
7
|
+
src/wrapture_instrumentation_aws/. Test files are named `test_*.py`
|
|
8
|
+
and are discovered by pytest, configured via the
|
|
9
|
+
`[tool.pytest.ini_options]` section of [pyproject.toml](pyproject.toml).
|
|
10
|
+
|
|
11
|
+
The directory has two levels:
|
|
12
|
+
|
|
13
|
+
- Package-level tests directly under tests/: the version, the rule
|
|
14
|
+
that importing the package or loading any registered class never
|
|
15
|
+
imports a target, and the listing tool reporting every entry
|
|
16
|
+
cleanly.
|
|
17
|
+
|
|
18
|
+
- One subdirectory per target, `tests/<target>/` (`tests/botocore/`),
|
|
19
|
+
holding that instrumentation's suite: settings validation, applying
|
|
20
|
+
and removing the class directly, the whole path through
|
|
21
|
+
`wrapture.instrumentation()` with a timeline recording what the
|
|
22
|
+
bindings observe, resolving the entry point by name, a check that
|
|
23
|
+
the installed botocore satisfies the class's `supports` range, and
|
|
24
|
+
the composition tests over a real wire.
|
|
25
|
+
|
|
26
|
+
Shared helpers live in [tests/conftest.py](tests/conftest.py).
|
|
27
|
+
|
|
28
|
+
## Testing with moto
|
|
29
|
+
|
|
30
|
+
The suite drives real boto3 clients against
|
|
31
|
+
[moto](https://docs.getmoto.org/), which reimplements the AWS APIs in
|
|
32
|
+
memory. Two modes are used, and the split maps onto what the suite
|
|
33
|
+
needs:
|
|
34
|
+
|
|
35
|
+
- In-process mode (`moto.mock_aws` as a context manager) registers a
|
|
36
|
+
`before-send` handler on botocore's event system and answers each
|
|
37
|
+
request from the in-memory backend, no socket. Everything above
|
|
38
|
+
`before-send`, `_make_api_call` included, is real botocore
|
|
39
|
+
executing normally, so the instrumented path is exercised with full
|
|
40
|
+
fidelity. This is the workhorse: the `aws` fixture opens `mock_aws()`
|
|
41
|
+
with dummy credentials and a region in the environment, and the
|
|
42
|
+
tests drive real clients and assert the tape.
|
|
43
|
+
|
|
44
|
+
- Server mode (`ThreadedMotoServer` from `moto.server`, on a thread,
|
|
45
|
+
no docker) is a real HTTP server speaking the AWS protocols on
|
|
46
|
+
localhost, with boto3 pointed at it via `endpoint_url`. It supplies
|
|
47
|
+
a genuine wire for the composition tests with the core package's
|
|
48
|
+
urllib3 target, and for asserting what headers the request carried
|
|
49
|
+
on the wire.
|
|
50
|
+
|
|
51
|
+
Because the seam sits strictly above moto's in-process interception,
|
|
52
|
+
the tests are faithful; the corollary is that this instrumentation
|
|
53
|
+
must never bind at or below botocore's HTTP send, where in-process
|
|
54
|
+
moto would short-circuit beneath it.
|
|
55
|
+
|
|
56
|
+
moto needs cryptography, which has no free threaded wheel for 3.13 or
|
|
57
|
+
3.15 yet, so those two builds are left out of the version matrix
|
|
58
|
+
rather than run with nothing installed. The suites also guard their
|
|
59
|
+
imports with `pytest.importorskip`, so a build where moto or the core
|
|
60
|
+
package is absent skips rather than errors.
|
|
61
|
+
|
|
62
|
+
## Running the tests
|
|
63
|
+
|
|
64
|
+
All tooling in this project goes through
|
|
65
|
+
[uv](https://docs.astral.sh/uv/), which manages the project
|
|
66
|
+
environment and installs the package, its development dependencies
|
|
67
|
+
(including pytest) and the target packages the tests need.
|
|
68
|
+
|
|
69
|
+
The simplest way to run the test suite is via the Justfile target:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
just test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Extra arguments are passed through to pytest, for example:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
just test -v
|
|
79
|
+
just test tests/botocore/test_recording.py
|
|
80
|
+
just test -k redirect
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
One target's suite alone:
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
just test-target botocore
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Equivalently, run pytest directly with uv:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
uv run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Watching what the tests record
|
|
96
|
+
|
|
97
|
+
The suites assert on tapes rather than printing anything, but the
|
|
98
|
+
recorded events can be watched live for visual verification. Setting
|
|
99
|
+
WRAPTURE_PRINTER in the environment installs a process-wide
|
|
100
|
+
wrapture.Printer sink for the session, streaming one line to stderr as
|
|
101
|
+
each operation begins and a closing line with its outcome and timing;
|
|
102
|
+
pytest captures stderr, so add -s to see it:
|
|
103
|
+
|
|
104
|
+
```console
|
|
105
|
+
WRAPTURE_PRINTER=1 just test tests/botocore -s
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For a purpose-built run rather than the tests' traffic, the demo
|
|
109
|
+
module under demo/ applies the instrumentation, drives boto3 against
|
|
110
|
+
moto, and prints both the live stream and the reconstructed tree with
|
|
111
|
+
timings:
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
just demo-botocore
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
With --otel the same events also export as OpenTelemetry spans over
|
|
118
|
+
OTLP (to http://localhost:4318, or wherever
|
|
119
|
+
OTEL_EXPORTER_OTLP_ENDPOINT points), for verifying the spans in a
|
|
120
|
+
local backend such as Jaeger; the Justfile target overlays the
|
|
121
|
+
wrapture[otel] dependencies for the run:
|
|
122
|
+
|
|
123
|
+
```console
|
|
124
|
+
just demo-botocore --otel
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Testing across Python versions
|
|
128
|
+
|
|
129
|
+
The project supports Python 3.12 through 3.15, and the free threaded
|
|
130
|
+
build of 3.14. The supported list is defined at the top of the
|
|
131
|
+
[Justfile](Justfile); the free threaded builds of 3.13 and 3.15 are
|
|
132
|
+
excluded until moto's dependency wheels exist for them. The default
|
|
133
|
+
version used by plain `just test` is pinned in
|
|
134
|
+
[.python-version](.python-version).
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
just test-all
|
|
138
|
+
just test-python 3.14t
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
uv downloads any Python version it does not already have, and each
|
|
142
|
+
version gets its own environment (.venv-VERSION) so the default .venv
|
|
143
|
+
is left untouched.
|
|
144
|
+
|
|
145
|
+
## Testing across target versions
|
|
146
|
+
|
|
147
|
+
The `test` dependency group installs boto3 (and so botocore) at
|
|
148
|
+
whatever version the lock resolves. The instrumentation's `supports`
|
|
149
|
+
range is kept honest by running the suite against other boto3 lines,
|
|
150
|
+
overlaid on the project environment for that run; boto3 pins botocore
|
|
151
|
+
to its matching minor, so overlaying boto3 is how a botocore line is
|
|
152
|
+
tested. Each line has a place in `boto3_versions` in the Justfile, run
|
|
153
|
+
one at a time by `just test-botocore 1.34.162` or all by
|
|
154
|
+
`just test-botocore-all`, and the CI workflow runs the same matrix. A
|
|
155
|
+
test in the suite asserts the installed botocore satisfies `supports`,
|
|
156
|
+
so a matrix entry outside the range fails loudly rather than passing
|
|
157
|
+
vacuously.
|
|
158
|
+
|
|
159
|
+
## Testing against unreleased wrapture
|
|
160
|
+
|
|
161
|
+
The package depends on a released wrapture. To run the tests against a
|
|
162
|
+
checkout of wrapture in the sibling directory ../wrapture, without
|
|
163
|
+
editing pyproject.toml:
|
|
164
|
+
|
|
165
|
+
```console
|
|
166
|
+
just test-dev
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
which overlays that checkout as an editable install for the run. This
|
|
170
|
+
is the way to exercise the package against unreleased wrapture
|
|
171
|
+
changes, such as a new resolver capability the seam relies on.
|
|
172
|
+
|
|
173
|
+
## Writing tests
|
|
174
|
+
|
|
175
|
+
- Put new test files in tests/ (package level) or tests/<target>/
|
|
176
|
+
(for one instrumentation) and name them `test_*.py`.
|
|
177
|
+
|
|
178
|
+
- Import the package under test as `wrapture_instrumentation_aws`, and
|
|
179
|
+
the instrumentation classes from their subpackages. The project is
|
|
180
|
+
installed into the uv-managed environment, so no path manipulation
|
|
181
|
+
is needed.
|
|
182
|
+
|
|
183
|
+
- Guard the target imports with `pytest.importorskip` at the top of
|
|
184
|
+
the module (`boto3 = pytest.importorskip("boto3")`), so a build
|
|
185
|
+
without moto or boto3 skips the suite rather than erroring.
|
|
186
|
+
|
|
187
|
+
- Validate behaviour with wrapture's own unit testing layer:
|
|
188
|
+
`wrapture.timeline()` to record what the instrumentation's bindings
|
|
189
|
+
observe and the tape's tree and queries to assert on it, and
|
|
190
|
+
`wrapture.instrumentation()` to scope an application of a class to a
|
|
191
|
+
block, driving real boto3 clients against moto. Do not use
|
|
192
|
+
`unittest.mock`. If something cannot be expressed that way, write it
|
|
193
|
+
plainly with a comment naming the gap, and say so when summarising
|
|
194
|
+
the work.
|
|
195
|
+
|
|
196
|
+
- Tests should not depend on anything in the scratch/ directory,
|
|
197
|
+
which is not part of the repository.
|
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Drive boto3 against moto with the AWS SDK instrumentation applied.
|
|
2
|
+
|
|
3
|
+
The instrumentation is resolved by its entry point name, and the
|
|
4
|
+
calls go to moto's in-memory backend, so no real AWS and no
|
|
5
|
+
credentials beyond dummy ones are needed. The calls cover the shapes
|
|
6
|
+
that matter: an S3 put and get (external, with a bucket and key), a
|
|
7
|
+
DynamoDB put (a datastore, with a collection), an SQS send (messaging,
|
|
8
|
+
with a destination), a paginated list (one event per page), and a
|
|
9
|
+
failing get (a ClientError recorded with its status and code). Each
|
|
10
|
+
runs beneath an observed function, so the leaves sit in a tree.
|
|
11
|
+
|
|
12
|
+
Two views of the run always print: the live stream and the tree
|
|
13
|
+
reconstructed with timings. With --otel the same events also export
|
|
14
|
+
as OpenTelemetry spans to a local OTLP endpoint (http://localhost:4318
|
|
15
|
+
unless OTEL_EXPORTER_OTLP_ENDPOINT says otherwise), where each span's
|
|
16
|
+
kind and attributes follow the call's category: an S3 call CLIENT with
|
|
17
|
+
rpc attributes, a DynamoDB call CLIENT with db attributes, an SQS call
|
|
18
|
+
PRODUCER with messaging attributes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import wrapture
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def add_otel_sink() -> None:
|
|
31
|
+
"""Register the OpenTelemetry sink; exits with guidance when the
|
|
32
|
+
optional dependencies are missing."""
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import wrapture.otel
|
|
36
|
+
except ImportError as error:
|
|
37
|
+
raise SystemExit(
|
|
38
|
+
"the OpenTelemetry dependencies are not installed; run the"
|
|
39
|
+
" demo through `just demo-botocore --otel`, which overlays"
|
|
40
|
+
" wrapture[otel] for the run"
|
|
41
|
+
) from error
|
|
42
|
+
|
|
43
|
+
wrapture.add_sink(wrapture.otel.sink(service_name="wrapture-botocore-demo"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(arguments: list[str] | None = None) -> None:
|
|
47
|
+
"""Run the demo: apply the instrumentation, make the calls against
|
|
48
|
+
moto, print the live stream and the tree, and flush any
|
|
49
|
+
exporters."""
|
|
50
|
+
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
prog="demo.botocore",
|
|
53
|
+
description="Drive boto3 against moto with the instrumentation"
|
|
54
|
+
" applied, printing the live stream and the tree.",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--otel",
|
|
58
|
+
action="store_true",
|
|
59
|
+
help="also export the events as OpenTelemetry spans over OTLP",
|
|
60
|
+
)
|
|
61
|
+
options = parser.parse_args(arguments)
|
|
62
|
+
|
|
63
|
+
os.environ.setdefault("AWS_ACCESS_KEY_ID", "testing")
|
|
64
|
+
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "testing")
|
|
65
|
+
os.environ.setdefault("AWS_SESSION_TOKEN", "testing")
|
|
66
|
+
os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1")
|
|
67
|
+
|
|
68
|
+
if options.otel:
|
|
69
|
+
add_otel_sink()
|
|
70
|
+
|
|
71
|
+
wrapture.add_sink(wrapture.Printer(stream=sys.stdout))
|
|
72
|
+
|
|
73
|
+
print("== live stream ==")
|
|
74
|
+
|
|
75
|
+
import boto3
|
|
76
|
+
from botocore.exceptions import ClientError
|
|
77
|
+
from moto import mock_aws
|
|
78
|
+
|
|
79
|
+
with (
|
|
80
|
+
mock_aws(),
|
|
81
|
+
wrapture.instrumentation("botocore"),
|
|
82
|
+
wrapture.timeline() as tape,
|
|
83
|
+
):
|
|
84
|
+
|
|
85
|
+
@wrapture.observed
|
|
86
|
+
def exercise() -> None:
|
|
87
|
+
s3 = boto3.client("s3")
|
|
88
|
+
s3.create_bucket(Bucket="reports")
|
|
89
|
+
s3.put_object(Bucket="reports", Key="q1.csv", Body=b"secret contents")
|
|
90
|
+
s3.get_object(Bucket="reports", Key="q1.csv")["Body"].read()
|
|
91
|
+
|
|
92
|
+
dynamodb = boto3.client("dynamodb")
|
|
93
|
+
dynamodb.create_table(
|
|
94
|
+
TableName="orders",
|
|
95
|
+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
|
96
|
+
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
|
97
|
+
BillingMode="PAY_PER_REQUEST",
|
|
98
|
+
)
|
|
99
|
+
dynamodb.put_item(TableName="orders", Item={"id": {"S": "1"}})
|
|
100
|
+
|
|
101
|
+
sqs = boto3.client("sqs")
|
|
102
|
+
queue = sqs.create_queue(QueueName="jobs")["QueueUrl"]
|
|
103
|
+
sqs.send_message(QueueUrl=queue, MessageBody="run report")
|
|
104
|
+
|
|
105
|
+
for name in ("a", "b"):
|
|
106
|
+
s3.put_object(Bucket="reports", Key=name, Body=b"x")
|
|
107
|
+
list(
|
|
108
|
+
s3.get_paginator("list_objects_v2").paginate(
|
|
109
|
+
Bucket="reports", PaginationConfig={"PageSize": 1}
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
s3.get_object(Bucket="reports", Key="missing")
|
|
115
|
+
except ClientError as error:
|
|
116
|
+
print(f"expected failure: {error.response['Error']['Code']}")
|
|
117
|
+
|
|
118
|
+
exercise()
|
|
119
|
+
|
|
120
|
+
print()
|
|
121
|
+
print("== tree ==")
|
|
122
|
+
print(tape.tree(times=True))
|
|
123
|
+
|
|
124
|
+
wrapture.shutdown()
|
|
125
|
+
|
|
126
|
+
if options.otel:
|
|
127
|
+
endpoint = os.environ.get(
|
|
128
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"
|
|
129
|
+
)
|
|
130
|
+
print()
|
|
131
|
+
print("== otel ==")
|
|
132
|
+
print(f"spans flushed to {endpoint} as service wrapture-botocore-demo")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|