ddapm-test-agent 1.32.0__py3-none-any.whl → 1.33.1__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.
@@ -1,4 +1,4 @@
1
1
  def _get_version():
2
- import pkg_resources # type: ignore[import-untyped]
2
+ import pkg_resources
3
3
 
4
4
  return pkg_resources.get_distribution(__name__).version
ddapm_test_agent/agent.py CHANGED
@@ -133,7 +133,7 @@ def _session_token(request: Request) -> Optional[str]:
133
133
 
134
134
  async def _vcr_proxy_cassette_prefix(request: Request) -> Optional[str]:
135
135
  try:
136
- request_body: dict[str, str] = await request.json()
136
+ request_body: Dict[str, str] = await request.json()
137
137
  requested_test_name = request_body.get("test_name")
138
138
  return requested_test_name
139
139
  except (json.JSONDecodeError, UnicodeDecodeError):
@@ -165,7 +165,7 @@ async def handle_exception_middleware(request: Request, handler: _Handler) -> we
165
165
 
166
166
  async def _forward_request(
167
167
  request_data: bytes, headers: Mapping[str, str], full_agent_url: str
168
- ) -> tuple[ClientResponse, str]:
168
+ ) -> Tuple[ClientResponse, str]:
169
169
  async with ClientSession() as session:
170
170
  async with session.post(
171
171
  full_agent_url,
@@ -929,10 +929,9 @@ class Agent:
929
929
  # Get the span attributes that are to be removed for this snapshot.
930
930
  default_attribute_regex_replaces: Dict[str, str] = request.app["snapshot_regex_placeholders"]
931
931
  regex_overrides = _parse_map(request.url.query.get("regex_placeholders", ""))
932
- attribute_regex_replaces = dict(
933
- (f"{{{key}}}", re.compile(regex))
934
- for (key, regex) in (default_attribute_regex_replaces | regex_overrides).items()
935
- )
932
+ regex_replaces = default_attribute_regex_replaces.copy()
933
+ regex_replaces.update(regex_overrides)
934
+ attribute_regex_replaces = dict((f"{{{key}}}", re.compile(regex)) for (key, regex) in regex_replaces.items())
936
935
  log.info("using regex placeholders %r", attribute_regex_replaces)
937
936
 
938
937
  if "span_id" in span_removes:
@@ -3,13 +3,13 @@ import json
3
3
  import logging
4
4
  import operator
5
5
  import pprint
6
- from re import Pattern
7
6
  import textwrap
8
7
  from typing import Any
9
8
  from typing import Dict
10
9
  from typing import List
11
10
  from typing import Optional
12
11
  from typing import OrderedDict as OrderedDictType
12
+ from typing import Pattern
13
13
  from typing import Set
14
14
  from typing import Tuple
15
15
  from typing import cast
@@ -188,9 +188,13 @@ def _match_traces(t1s: List[Trace], t2s: List[Trace]) -> List[Tuple[Trace, Trace
188
188
  return matches
189
189
 
190
190
 
191
- def _diff_spans(
192
- s1: Span, s2: Span, ignored: Set[str]
193
- ) -> Tuple[List[str], List[str], List[str], List[Tuple[List[str], List[str]]], List[Tuple[List[str], List[str]]]]:
191
+ def _diff_spans(s1: Span, s2: Span, ignored: Set[str]) -> Tuple[
192
+ List[str],
193
+ List[str],
194
+ List[str],
195
+ List[Tuple[List[str], List[str]]],
196
+ List[Tuple[List[str], List[str]]],
197
+ ]:
194
198
  """Return differing attributes between two spans and their meta/metrics maps.
195
199
 
196
200
  It is assumed that the spans have passed through preliminary validation
@@ -255,7 +259,11 @@ def _diff_spans(
255
259
  )
256
260
  link_diff = []
257
261
  for d1, d2, ign in [
258
- (l1_no_tags, l2_no_tags, set(i[11:] for i in ignored if i.startswith("span_links."))),
262
+ (
263
+ l1_no_tags,
264
+ l2_no_tags,
265
+ set(i[11:] for i in ignored if i.startswith("span_links.")),
266
+ ),
259
267
  (
260
268
  l1.get("attributes") or {},
261
269
  l2.get("attributes") or {},
@@ -288,7 +296,11 @@ def _diff_spans(
288
296
  )
289
297
  event_diff = []
290
298
  for d1, d2, ign in [
291
- (l1_no_tags, l2_no_tags, set(i[12:] for i in ignored if i.startswith("span_events."))),
299
+ (
300
+ l1_no_tags,
301
+ l2_no_tags,
302
+ set(i[12:] for i in ignored if i.startswith("span_events.")),
303
+ ),
292
304
  (
293
305
  e1.get("attributes") or {},
294
306
  e2.get("attributes") or {},
@@ -307,7 +319,13 @@ def _diff_spans(
307
319
  results.append(event_diffs) # type: ignore
308
320
 
309
321
  return cast(
310
- Tuple[List[str], List[str], List[str], List[Tuple[List[str], List[str]]], List[Tuple[List[str], List[str]]]],
322
+ Tuple[
323
+ List[str],
324
+ List[str],
325
+ List[str],
326
+ List[Tuple[List[str], List[str]]],
327
+ List[Tuple[List[str], List[str]]],
328
+ ],
311
329
  tuple(results),
312
330
  )
313
331
 
@@ -334,9 +352,13 @@ def _compare_traces(expected: Trace, received: Trace, ignored: Set[str]) -> None
334
352
  ) as frame:
335
353
  frame.add_item(f"Expected span:\n{pprint.pformat(s_exp)}")
336
354
  frame.add_item(f"Received span:\n{pprint.pformat(s_rec)}")
337
- top_level_diffs, meta_diffs, metrics_diffs, span_link_diffs, span_event_diffs = _diff_spans(
338
- s_exp, s_rec, ignored
339
- )
355
+ (
356
+ top_level_diffs,
357
+ meta_diffs,
358
+ metrics_diffs,
359
+ span_link_diffs,
360
+ span_event_diffs,
361
+ ) = _diff_spans(s_exp, s_rec, ignored)
340
362
 
341
363
  for diffs, diff_type, d_exp, d_rec in [
342
364
  (top_level_diffs, "span", s_exp, s_rec),
@@ -363,7 +385,12 @@ def _compare_traces(expected: Trace, received: Trace, ignored: Set[str]) -> None
363
385
 
364
386
  for i, (link_level_diffs, attribute_diffs) in enumerate(span_link_diffs):
365
387
  for diffs, diff_type, d_exp, d_rec in [
366
- (link_level_diffs, f"{i}", s_exp["span_links"][i], s_rec["span_links"][i]),
388
+ (
389
+ link_level_diffs,
390
+ f"{i}",
391
+ s_exp["span_links"][i],
392
+ s_rec["span_links"][i],
393
+ ),
367
394
  (
368
395
  attribute_diffs,
369
396
  f"{i} attributes",
@@ -387,7 +414,12 @@ def _compare_traces(expected: Trace, received: Trace, ignored: Set[str]) -> None
387
414
 
388
415
  for i, (event_level_diffs, attribute_diffs) in enumerate(span_event_diffs):
389
416
  for diffs, diff_type, d_exp, d_rec in [
390
- (event_level_diffs, f"{i}", s_exp["span_events"][i], s_rec["span_events"][i]),
417
+ (
418
+ event_level_diffs,
419
+ f"{i}",
420
+ s_exp["span_events"][i],
421
+ s_rec["span_events"][i],
422
+ ),
391
423
  (
392
424
  attribute_diffs,
393
425
  f"{i} attributes",
@@ -545,4 +577,7 @@ def generate_snapshot(
545
577
  removed: Optional[List[str]] = None,
546
578
  attribute_regex_replaces: Optional[Dict[str, Pattern[str]]] = None,
547
579
  ) -> str:
548
- return _snapshot_json(_normalize_traces(received_traces, attribute_regex_replaces or {}), removed or [])
580
+ return _snapshot_json(
581
+ _normalize_traces(received_traces, attribute_regex_replaces or {}),
582
+ removed or [],
583
+ )
@@ -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,50 @@ 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)
130
- with get_vcr(provider, vcr_cassettes_directory).use_cassette(f"{cassette_name}.yaml"):
131
- oai_response = requests.request(
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
- )
170
+ cassette_file_name = f"{cassette_name}.yaml"
171
+
172
+ request_kwargs: Dict[str, Any] = {
173
+ "method": request.method,
174
+ "url": target_url,
175
+ "headers": headers,
176
+ "data": body_bytes,
177
+ "cookies": dict(request.cookies),
178
+ "allow_redirects": False,
179
+ "stream": True,
180
+ }
181
+
182
+ if provider in AWS_SERVICES and not os.path.exists(
183
+ os.path.join(vcr_cassettes_directory, provider, cassette_file_name)
184
+ ):
185
+ if not AWS_SECRET_ACCESS_KEY:
186
+ return Response(
187
+ body="AWS_SECRET_ACCESS_KEY environment variable not set for aws signature recalculation",
188
+ status=400,
189
+ )
190
+
191
+ auth_header = request.headers.get("Authorization", "")
192
+ auth_parts = parse_authorization_header(auth_header)
193
+ aws_access_key = auth_parts.get("Credential", "").split("/")[0]
194
+
195
+ auth = AWS4Auth(aws_access_key, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_SERVICES[provider])
196
+ request_kwargs["auth"] = auth
197
+
198
+ with get_vcr(provider, vcr_cassettes_directory).use_cassette(cassette_file_name):
199
+ provider_response = requests.request(**request_kwargs)
140
200
 
141
201
  # Extract content type without charset
142
- content_type = oai_response.headers.get("content-type", "")
202
+ content_type = provider_response.headers.get("content-type", "")
143
203
  if ";" in content_type:
144
204
  content_type = content_type.split(";")[0].strip()
145
205
 
146
206
  response = Response(
147
- body=oai_response.content,
148
- status=oai_response.status_code,
207
+ body=provider_response.content,
208
+ status=provider_response.status_code,
149
209
  content_type=content_type,
150
210
  )
151
211
 
152
- for key, value in oai_response.headers.items():
212
+ for key, value in provider_response.headers.items():
153
213
  if key.lower() not in (
154
214
  "content-length",
155
215
  "transfer-encoding",
@@ -1,15 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddapm-test-agent
3
- Version: 1.32.0
3
+ Version: 1.33.1
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
7
7
  Author-email: kyle@verhoog.ca
8
8
  License: BSD 3
9
9
  Classifier: Programming Language :: Python
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
10
  Classifier: Programming Language :: Python :: 3.11
14
11
  Classifier: Programming Language :: Python :: 3.12
15
12
  Classifier: Programming Language :: Python :: 3.13
@@ -24,6 +21,7 @@ Requires-Dist: requests
24
21
  Requires-Dist: typing_extensions
25
22
  Requires-Dist: yarl
26
23
  Requires-Dist: vcrpy
24
+ Requires-Dist: requests-aws4auth
27
25
  Requires-Dist: opentelemetry-proto<1.37.0,>1.33.0
28
26
  Requires-Dist: protobuf>=3.19.0
29
27
  Requires-Dist: grpcio<2.0,>=1.66.2
@@ -63,11 +61,6 @@ See the [Development](#development) section for how to get the test agent runnin
63
61
 
64
62
  ## Installation
65
63
 
66
- The test agent can be installed using [nix](https://docs.determinate.systems/getting-started/):
67
-
68
- nix profile install github:datadog/dd-apm-test-agent#ddapm-test-agent
69
- # nix profile upgrade ddapm-test-agent # to upgrade
70
-
71
64
  The test agent can be installed from PyPI:
72
65
 
73
66
  pip install ddapm-test-agent
@@ -192,6 +185,13 @@ The cassettes are matched based on the path, method, and body of the request. To
192
185
 
193
186
  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
187
 
188
+ #### AWS Services
189
+ 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.
190
+
191
+ Additionally, the `AWS_REGION` environment variable can be set, defaulting to `us-east-1`.
192
+
193
+ 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`).
194
+
195
195
  #### Usage in clients
196
196
 
197
197
  To use this feature in your client, you can use the `/vcr/{provider}` endpoint to proxy requests to the provider API.
@@ -1,5 +1,5 @@
1
- ddapm_test_agent/__init__.py,sha256=hJBQduemued6MAWH6Uk2gqJx2vb0_dd0UO52pJ83nLM,138
2
- ddapm_test_agent/agent.py,sha256=S5VzW5yvBQKqsGj50Xqb_u8gXvRnZguwWTZXn6BcX-M,73960
1
+ ddapm_test_agent/__init__.py,sha256=IEYMDM-xI0IoHYSYw4Eva5263puB_crrrbLstOCScRw,106
2
+ ddapm_test_agent/agent.py,sha256=296wweDNsF7cw5QuGdSzbKa4vWrVpea3rvZU4Laddkc,74000
3
3
  ddapm_test_agent/apmtelemetry.py,sha256=w_9-yUDh1dgox-FfLqeOHU2C14GcjOjen-_SVagiZrc,861
4
4
  ddapm_test_agent/checks.py,sha256=pBa4YKZQVA8qaTVJ_XgMA6TmlUZNh99YOrCFJA7fwo0,6865
5
5
  ddapm_test_agent/client.py,sha256=ViEmiRX9Y3SQ-KBhSc-FdzBmIVIe8Ij9jj-Q6VGyzLY,7359
@@ -12,15 +12,15 @@ ddapm_test_agent/metrics.py,sha256=EZo7lSec2oAiH7tUqavKZ2MJM7TwbuFGE3AT3cXwmSM,3
12
12
  ddapm_test_agent/remoteconfig.py,sha256=_QjYUKc3JF31DxdvISDXgslm5WVnYWAw0hyckWuLc1c,3606
13
13
  ddapm_test_agent/trace.py,sha256=t0OR8w3NcZK-EOOoadgPITiZqS5tAJGtxqLVGLEw7Kg,45816
14
14
  ddapm_test_agent/trace_checks.py,sha256=bRg2eLKoHROXIFJRbujMUn0T3x1X8pZso-j8wXNomec,9972
15
- ddapm_test_agent/trace_snapshot.py,sha256=g2MhKi8UE-Wsf6PtuzPoXymcW-cYRUnvj63SP9FETJs,22354
15
+ ddapm_test_agent/trace_snapshot.py,sha256=ayOUcCFo6xyotFRm0fSNdeA91_T447W5ShlkRvi0nZE,22932
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=g6ix7laiS8Hqq9p14nkTMARhj5KMZmyRZjZpfFEMxOM,4973
20
- ddapm_test_agent-1.32.0.dist-info/licenses/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493
21
- ddapm_test_agent-1.32.0.dist-info/licenses/LICENSE.apache2,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342
22
- ddapm_test_agent-1.32.0.dist-info/METADATA,sha256=mf922XgdhlbHNJk3P5jCiw5HBkmzfwpex-FDvqjHXa4,27493
23
- ddapm_test_agent-1.32.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- ddapm_test_agent-1.32.0.dist-info/entry_points.txt,sha256=ulayVs6YJ-0Ej2kxbwn39wOHDVXbyQgFgsbRQmXydcs,250
25
- ddapm_test_agent-1.32.0.dist-info/top_level.txt,sha256=A9jiKOrrg6VjFAk-mtlSVYN4wr0VsZe58ehGR6IW47U,17
26
- ddapm_test_agent-1.32.0.dist-info/RECORD,,
19
+ ddapm_test_agent/vcr_proxy.py,sha256=jTm9Q0LeWOUJrwlDrG632WrQB6ScWvjb3dLR7oSFa6o,6808
20
+ ddapm_test_agent-1.33.1.dist-info/licenses/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493
21
+ ddapm_test_agent-1.33.1.dist-info/licenses/LICENSE.apache2,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342
22
+ ddapm_test_agent-1.33.1.dist-info/METADATA,sha256=fPwXRZgry4efp28Ft0jEWhzxwW8rZx134I01GBFHt0c,28104
23
+ ddapm_test_agent-1.33.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ ddapm_test_agent-1.33.1.dist-info/entry_points.txt,sha256=ulayVs6YJ-0Ej2kxbwn39wOHDVXbyQgFgsbRQmXydcs,250
25
+ ddapm_test_agent-1.33.1.dist-info/top_level.txt,sha256=A9jiKOrrg6VjFAk-mtlSVYN4wr0VsZe58ehGR6IW47U,17
26
+ ddapm_test_agent-1.33.1.dist-info/RECORD,,