ddapm-test-agent 1.32.0__py3-none-any.whl → 1.33.0__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.
- ddapm_test_agent/vcr_proxy.py +80 -23
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/METADATA +9 -1
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/RECORD +8 -8
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/WHEEL +0 -0
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/entry_points.txt +0 -0
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/licenses/LICENSE.BSD3 +0 -0
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/licenses/LICENSE.apache2 +0 -0
- {ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/top_level.txt +0 -0
ddapm_test_agent/vcr_proxy.py
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Dict
|
|
5
8
|
from typing import Optional
|
|
6
9
|
from urllib.parse import urljoin
|
|
7
10
|
|
|
8
11
|
from aiohttp.web import Request
|
|
9
12
|
from aiohttp.web import Response
|
|
10
13
|
import requests
|
|
14
|
+
from requests_aws4auth import AWS4Auth
|
|
11
15
|
import vcr
|
|
12
16
|
|
|
13
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Used for AWS signature recalculation for aws services initial proxying
|
|
22
|
+
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
|
|
23
|
+
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
24
|
+
|
|
25
|
+
|
|
14
26
|
def url_path_join(base_url: str, path: str) -> str:
|
|
15
27
|
"""Join a base URL with a path, handling slashes automatically."""
|
|
16
28
|
return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
|
|
17
29
|
|
|
18
30
|
|
|
31
|
+
AWS_SERVICES = {
|
|
32
|
+
"bedrock-runtime": "bedrock",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
19
36
|
PROVIDER_BASE_URLS = {
|
|
20
37
|
"openai": "https://api.openai.com/v1",
|
|
21
38
|
"azure-openai": "https://dd.openai.azure.com/",
|
|
@@ -23,8 +40,25 @@ PROVIDER_BASE_URLS = {
|
|
|
23
40
|
"anthropic": "https://api.anthropic.com/",
|
|
24
41
|
"datadog": "https://api.datadoghq.com/",
|
|
25
42
|
"genai": "https://generativelanguage.googleapis.com/",
|
|
43
|
+
"bedrock-runtime": f"https://bedrock-runtime.{AWS_REGION}.amazonaws.com",
|
|
26
44
|
}
|
|
27
45
|
|
|
46
|
+
CASSETTE_FILTER_HEADERS = [
|
|
47
|
+
"authorization",
|
|
48
|
+
"OpenAI-Organization",
|
|
49
|
+
"api-key",
|
|
50
|
+
"x-api-key",
|
|
51
|
+
"dd-api-key",
|
|
52
|
+
"dd-application-key",
|
|
53
|
+
"x-goog-api-key",
|
|
54
|
+
"x-amz-security-token",
|
|
55
|
+
"x-amz-content-sha256",
|
|
56
|
+
"x-amz-date",
|
|
57
|
+
"x-amz-user-agent",
|
|
58
|
+
"amz-sdk-invocation-id",
|
|
59
|
+
"amz-sdk-request",
|
|
60
|
+
]
|
|
61
|
+
|
|
28
62
|
NORMALIZERS = [
|
|
29
63
|
(
|
|
30
64
|
r"--form-data-boundary-[^\r\n]+",
|
|
@@ -65,6 +99,21 @@ def normalize_multipart_body(body: bytes) -> str:
|
|
|
65
99
|
return f"[binary_data_{hex_digest}]"
|
|
66
100
|
|
|
67
101
|
|
|
102
|
+
def parse_authorization_header(auth_header: str) -> Dict[str, str]:
|
|
103
|
+
"""Parse AWS Authorization header to extract components"""
|
|
104
|
+
if not auth_header.startswith("AWS4-HMAC-SHA256 "):
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
auth_parts = auth_header[len("AWS4-HMAC-SHA256 ") :].split(",")
|
|
108
|
+
parsed = {}
|
|
109
|
+
|
|
110
|
+
for part in auth_parts:
|
|
111
|
+
key, value = part.split("=", 1)
|
|
112
|
+
parsed[key.strip()] = value.strip()
|
|
113
|
+
|
|
114
|
+
return parsed
|
|
115
|
+
|
|
116
|
+
|
|
68
117
|
def get_vcr(subdirectory: str, vcr_cassettes_directory: str) -> vcr.VCR:
|
|
69
118
|
cassette_dir = os.path.join(vcr_cassettes_directory, subdirectory)
|
|
70
119
|
|
|
@@ -72,15 +121,7 @@ def get_vcr(subdirectory: str, vcr_cassettes_directory: str) -> vcr.VCR:
|
|
|
72
121
|
cassette_library_dir=cassette_dir,
|
|
73
122
|
record_mode="once",
|
|
74
123
|
match_on=["path", "method"],
|
|
75
|
-
filter_headers=
|
|
76
|
-
"authorization",
|
|
77
|
-
"OpenAI-Organization",
|
|
78
|
-
"api-key",
|
|
79
|
-
"x-api-key",
|
|
80
|
-
"dd-api-key",
|
|
81
|
-
"dd-application-key",
|
|
82
|
-
"x-goog-api-key",
|
|
83
|
-
],
|
|
124
|
+
filter_headers=CASSETTE_FILTER_HEADERS,
|
|
84
125
|
)
|
|
85
126
|
|
|
86
127
|
|
|
@@ -125,31 +166,47 @@ async def proxy_request(request: Request, vcr_cassettes_directory: str) -> Respo
|
|
|
125
166
|
body_bytes = await request.read()
|
|
126
167
|
|
|
127
168
|
vcr_cassette_prefix = request.pop("vcr_cassette_prefix", None)
|
|
128
|
-
|
|
129
169
|
cassette_name = generate_cassette_name(path, request.method, body_bytes, vcr_cassette_prefix)
|
|
170
|
+
|
|
171
|
+
request_kwargs: Dict[str, Any] = {
|
|
172
|
+
"method": request.method,
|
|
173
|
+
"url": target_url,
|
|
174
|
+
"headers": headers,
|
|
175
|
+
"data": body_bytes,
|
|
176
|
+
"cookies": dict(request.cookies),
|
|
177
|
+
"allow_redirects": False,
|
|
178
|
+
"stream": True,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if provider in AWS_SERVICES and not os.path.exists(os.path.join(vcr_cassettes_directory, provider, cassette_name)):
|
|
182
|
+
if not AWS_SECRET_ACCESS_KEY:
|
|
183
|
+
return Response(
|
|
184
|
+
body="AWS_SECRET_ACCESS_KEY environment variable not set for aws signature recalculation",
|
|
185
|
+
status=400,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
auth_header = request.headers.get("Authorization", "")
|
|
189
|
+
auth_parts = parse_authorization_header(auth_header)
|
|
190
|
+
aws_access_key = auth_parts.get("Credential", "").split("/")[0]
|
|
191
|
+
|
|
192
|
+
auth = AWS4Auth(aws_access_key, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_SERVICES[provider])
|
|
193
|
+
request_kwargs["auth"] = auth
|
|
194
|
+
|
|
130
195
|
with get_vcr(provider, vcr_cassettes_directory).use_cassette(f"{cassette_name}.yaml"):
|
|
131
|
-
|
|
132
|
-
method=request.method,
|
|
133
|
-
url=target_url,
|
|
134
|
-
headers=headers,
|
|
135
|
-
data=body_bytes,
|
|
136
|
-
cookies=dict(request.cookies),
|
|
137
|
-
allow_redirects=False,
|
|
138
|
-
stream=True,
|
|
139
|
-
)
|
|
196
|
+
provider_response = requests.request(**request_kwargs)
|
|
140
197
|
|
|
141
198
|
# Extract content type without charset
|
|
142
|
-
content_type =
|
|
199
|
+
content_type = provider_response.headers.get("content-type", "")
|
|
143
200
|
if ";" in content_type:
|
|
144
201
|
content_type = content_type.split(";")[0].strip()
|
|
145
202
|
|
|
146
203
|
response = Response(
|
|
147
|
-
body=
|
|
148
|
-
status=
|
|
204
|
+
body=provider_response.content,
|
|
205
|
+
status=provider_response.status_code,
|
|
149
206
|
content_type=content_type,
|
|
150
207
|
)
|
|
151
208
|
|
|
152
|
-
for key, value in
|
|
209
|
+
for key, value in provider_response.headers.items():
|
|
153
210
|
if key.lower() not in (
|
|
154
211
|
"content-length",
|
|
155
212
|
"transfer-encoding",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddapm-test-agent
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.33.0
|
|
4
4
|
Summary: Test agent for Datadog APM client libraries
|
|
5
5
|
Home-page: https://github.com/Datadog/dd-apm-test-agent
|
|
6
6
|
Author: Kyle Verhoog
|
|
@@ -24,6 +24,7 @@ Requires-Dist: requests
|
|
|
24
24
|
Requires-Dist: typing_extensions
|
|
25
25
|
Requires-Dist: yarl
|
|
26
26
|
Requires-Dist: vcrpy
|
|
27
|
+
Requires-Dist: requests-aws4auth
|
|
27
28
|
Requires-Dist: opentelemetry-proto<1.37.0,>1.33.0
|
|
28
29
|
Requires-Dist: protobuf>=3.19.0
|
|
29
30
|
Requires-Dist: grpcio<2.0,>=1.66.2
|
|
@@ -192,6 +193,13 @@ The cassettes are matched based on the path, method, and body of the request. To
|
|
|
192
193
|
|
|
193
194
|
Optionally specifying whatever mounted path is used for the cassettes directory. The test agent comes with a default set of cassettes for OpenAI, Azure OpenAI, and DeepSeek.
|
|
194
195
|
|
|
196
|
+
#### AWS Services
|
|
197
|
+
AWS service proxying, specifically recording cassettes for the first time, requires a `AWS_SECRET_ACCESS_KEY` environment variable to be set for the container running the test agent. This is used to recalculate the AWS signature for the request, as the one generated client-side likely used `{test-agent-host}:{test-agent-port}/vcr/{aws-service}` as the host, and the signature will mismatch that on the actual AWS service.
|
|
198
|
+
|
|
199
|
+
Additionally, the `AWS_REGION` environment variable can be set, defaulting to `us-east-1`.
|
|
200
|
+
|
|
201
|
+
To add a new AWS service to proxy, add an entry in the `PROVIDER_BASE_URLS` for its provider url, and an entry in the `AWS_SERVICES` dictionary for the service name, since they are not always a one-to-one mapping with the implied provider url (e.g, `https://bedrock-runtime.{AWS_REGION}.amazonaws.com` is the provider url, but the service name is `bedrock`, as `bedrock` also has multiple sub services, like `converse`).
|
|
202
|
+
|
|
195
203
|
#### Usage in clients
|
|
196
204
|
|
|
197
205
|
To use this feature in your client, you can use the `/vcr/{provider}` endpoint to proxy requests to the provider API.
|
|
@@ -16,11 +16,11 @@ ddapm_test_agent/trace_snapshot.py,sha256=g2MhKi8UE-Wsf6PtuzPoXymcW-cYRUnvj63SP9
|
|
|
16
16
|
ddapm_test_agent/tracerflare.py,sha256=uoSjhPCOKZflgJn5JLv1Unh4gUdAR1-YbC9_1n1iH9w,954
|
|
17
17
|
ddapm_test_agent/tracestats.py,sha256=q_WQZnh2kXSSN3fRIBe_0jMYCBQHcaS3fZmJTge4lWc,2073
|
|
18
18
|
ddapm_test_agent/tracestats_snapshot.py,sha256=VsB6MVnHPjPWHVWnnDdCXJcVKL_izKXEf9lvJ0qbjNQ,3609
|
|
19
|
-
ddapm_test_agent/vcr_proxy.py,sha256=
|
|
20
|
-
ddapm_test_agent-1.
|
|
21
|
-
ddapm_test_agent-1.
|
|
22
|
-
ddapm_test_agent-1.
|
|
23
|
-
ddapm_test_agent-1.
|
|
24
|
-
ddapm_test_agent-1.
|
|
25
|
-
ddapm_test_agent-1.
|
|
26
|
-
ddapm_test_agent-1.
|
|
19
|
+
ddapm_test_agent/vcr_proxy.py,sha256=qnzLD5f1LcKIcLFmqU0n5fzcfWTIt0m173pbpBfeXK4,6745
|
|
20
|
+
ddapm_test_agent-1.33.0.dist-info/licenses/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493
|
|
21
|
+
ddapm_test_agent-1.33.0.dist-info/licenses/LICENSE.apache2,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342
|
|
22
|
+
ddapm_test_agent-1.33.0.dist-info/METADATA,sha256=hxIxnr5yrlh06q1YfaKgeX7RcQUx0XzC7nM2pgHQQQ4,28483
|
|
23
|
+
ddapm_test_agent-1.33.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
ddapm_test_agent-1.33.0.dist-info/entry_points.txt,sha256=ulayVs6YJ-0Ej2kxbwn39wOHDVXbyQgFgsbRQmXydcs,250
|
|
25
|
+
ddapm_test_agent-1.33.0.dist-info/top_level.txt,sha256=A9jiKOrrg6VjFAk-mtlSVYN4wr0VsZe58ehGR6IW47U,17
|
|
26
|
+
ddapm_test_agent-1.33.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/licenses/LICENSE.BSD3
RENAMED
|
File without changes
|
{ddapm_test_agent-1.32.0.dist-info → ddapm_test_agent-1.33.0.dist-info}/licenses/LICENSE.apache2
RENAMED
|
File without changes
|
|
File without changes
|