Skip to content

Commit dff9461

Browse files
authored
Add /test/session/tracerflares endpoint to return all tracer-flares received by the test agent for a given session token. (#146)
... received by the test agent for a given session token.
1 parent 3f74a38 commit dff9461

File tree

5 files changed

+107
-24
lines changed

5 files changed

+107
-24
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,31 @@ Mimics the tracer_flare endpoint of the agent. Returns OK if the flare contains
408408

409409
Logs a line everytime it's called and stores the tracer flare details in the request under `"_tracer_flare"`.
410410

411+
### /test/session/tracerflares
412+
413+
Return all tracer-flares that have been received by the agent for the given session token.
414+
415+
#### [optional] `?test_session_token=`
416+
#### [optional] `X-Datadog-Test-Session-Token`
417+
418+
Returns the tracer-flares in the following json format:
419+
420+
```json
421+
[
422+
{
423+
"source": "...",
424+
"case_id": "...",
425+
"email": "...",
426+
"hostname": "...",
427+
"flare_file": "...",
428+
}
429+
]
430+
```
431+
432+
`flare_file` is the base64 encoded content of the tracer-flare payload.
433+
434+
If there was an error parsing the tracer-flare form, that will be recorded under `error`.
435+
411436
## Development
412437

413438
### Prerequisites

ddapm_test_agent/agent.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import argparse
2-
from asyncio import StreamReader
32
import atexit
43
import base64
54
from collections import OrderedDict
@@ -10,7 +9,6 @@
109
import pprint
1110
import socket
1211
import sys
13-
from typing import Any
1412
from typing import Awaitable
1513
from typing import Callable
1614
from typing import Dict
@@ -24,7 +22,6 @@
2422
from urllib.parse import urlunparse
2523

2624
from aiohttp import ClientSession
27-
from aiohttp import MultipartReader
2825
from aiohttp import web
2926
from aiohttp.web import Request
3027
from aiohttp.web import middleware
@@ -54,6 +51,8 @@
5451
from .trace_checks import CheckTraceDDService
5552
from .trace_checks import CheckTracePeerService
5653
from .trace_checks import CheckTraceStallAsync
54+
from .tracerflare import TracerFlareEvent
55+
from .tracerflare import v1_decode as v1_tracerflare_decode
5756
from .tracestats import decode_v06 as tracestats_decode_v06
5857
from .tracestats import v06StatsPayload
5958

@@ -381,6 +380,19 @@ async def _apmtelemetry_by_session(self, token: Optional[str]) -> List[Telemetry
381380
# TODO: Sort the events?
382381
return events
383382

383+
async def _tracerflares_by_session(self, token: Optional[str]) -> List[TracerFlareEvent]:
384+
"""Return the tracer-flare events that belong to the given session token.
385+
386+
If token is None or if the token was used to manually start a session
387+
with /session-start then return all tracer-flare events that were sent
388+
since the last /session-start request was made.
389+
"""
390+
events: List[TracerFlareEvent] = []
391+
for req in self._requests_by_session(token):
392+
if req.match_info.handler == self.handle_v1_tracer_flare:
393+
events.append(await v1_tracerflare_decode(req.headers, await req.read()))
394+
return events
395+
384396
async def _tracestats_by_session(self, token: Optional[str]) -> List[v06StatsPayload]:
385397
stats: List[v06StatsPayload] = []
386398
for req in self._requests_by_session(token):
@@ -517,21 +529,7 @@ async def handle_v2_apmtelemetry(self, request: Request) -> web.Response:
517529
return web.HTTPOk()
518530

519531
async def handle_v1_tracer_flare(self, request: Request) -> web.Response:
520-
# reconstruct stream from previously cached bytes
521-
stream = StreamReader()
522-
stream.feed_data(self._request_data(request))
523-
stream.feed_eof()
524-
525-
tracer_flare: Dict[str, Any] = {}
526-
527-
async for part in MultipartReader(request.headers, stream):
528-
if part.name is not None:
529-
if part.name == "flare_file":
530-
tracer_flare[part.name] = await part.read() # zipfile
531-
else:
532-
tracer_flare[part.name] = await part.text()
533-
534-
request["_tracer_flare"] = tracer_flare
532+
tracer_flare: TracerFlareEvent = await v1_tracerflare_decode(request.headers, self._request_data(request))
535533

536534
expectedFields = ["source", "case_id", "email", "hostname", "flare_file"]
537535
missingFields = [k for k in expectedFields if k not in tracer_flare]
@@ -782,6 +780,11 @@ async def handle_session_apmtelemetry(self, request: Request) -> web.Response:
782780
events = await self._apmtelemetry_by_session(token)
783781
return web.json_response(events)
784782

783+
async def handle_session_tracerflares(self, request: Request) -> web.Response:
784+
token = request["session_token"]
785+
events = await self._tracerflares_by_session(token)
786+
return web.json_response(events)
787+
785788
async def handle_session_tracestats(self, request: Request) -> web.Response:
786789
token = request["session_token"]
787790
stats = await self._tracestats_by_session(token)
@@ -1029,6 +1032,7 @@ def make_app(
10291032
web.get("/test/session/snapshot", agent.handle_snapshot),
10301033
web.get("/test/session/traces", agent.handle_session_traces),
10311034
web.get("/test/session/apmtelemetry", agent.handle_session_apmtelemetry),
1035+
web.get("/test/session/tracerflares", agent.handle_session_tracerflares),
10321036
web.get("/test/session/stats", agent.handle_session_tracestats),
10331037
web.get("/test/session/requests", agent.handle_session_requests),
10341038
web.post("/test/session/responses/config", agent.handle_v07_remoteconfig_create),

ddapm_test_agent/tracerflare.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from asyncio import StreamReader
2+
import base64
3+
from typing import Dict
4+
from typing import Mapping
5+
6+
from aiohttp import MultipartReader
7+
8+
9+
TracerFlareEvent = Dict[str, str]
10+
11+
12+
async def v1_decode(headers: Mapping[str, str], data: bytes) -> TracerFlareEvent:
13+
"""Decode v1 tracer flare form as a dict"""
14+
tracer_flare: TracerFlareEvent = {}
15+
try:
16+
stream = StreamReader()
17+
stream.feed_data(data)
18+
stream.feed_eof()
19+
async for part in MultipartReader(headers, stream):
20+
if part.name is not None:
21+
if part.name == "flare_file":
22+
tracer_flare[part.name] = base64.b64encode(await part.read()).decode("ascii")
23+
else:
24+
tracer_flare[part.name] = await part.text()
25+
except Exception as err:
26+
tracer_flare["error"] = str(err)
27+
return tracer_flare
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
features:
2+
- |
3+
Add ``/test/session/tracerflares`` endpoint to return all tracer-flares received
4+
by the test agent for a given session token.

tests/test_tracerflare.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33

44
async def test_tracerflare(agent):
5+
expected_output = {
6+
"source": "tracer_test",
7+
"case_id": "12345",
8+
"email": "[email protected]",
9+
"hostname": "my.hostname",
10+
"flare_file": "UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==",
11+
}
512
form = FormData()
613
form.add_field("source", "tracer_test")
714
form.add_field("case_id", "12345")
@@ -15,9 +22,17 @@ async def test_tracerflare(agent):
1522
)
1623
resp = await agent.post("/tracer_flare/v1", data=form)
1724
assert resp.status == 200
25+
flares = await agent.get("/test/session/tracerflares")
26+
assert await flares.json() == [expected_output]
1827

1928

2029
async def test_tracerflare_missing_case_id(agent):
30+
expected_output = {
31+
"source": "tracer_test",
32+
"email": "[email protected]",
33+
"hostname": "my.hostname",
34+
"flare_file": "UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==",
35+
}
2136
form = FormData()
2237
form.add_field("source", "tracer_test")
2338
form.add_field("email", "[email protected]")
@@ -30,13 +45,21 @@ async def test_tracerflare_missing_case_id(agent):
3045
)
3146
resp = await agent.post("/tracer_flare/v1", data=form)
3247
assert resp.status == 400
48+
flares = await agent.get("/test/session/tracerflares")
49+
assert await flares.json() == [expected_output]
3350

3451

35-
async def test_tracerflare_missing_file(agent):
36-
form = FormData()
37-
form.add_field("source", "tracer_test")
38-
form.add_field("case_id", "12345")
39-
form.add_field("email", "[email protected]")
40-
form.add_field("hostname", "my.hostname")
52+
async def test_tracerflare_not_multipart(agent):
53+
expected_output = {
54+
"error": "multipart/* content type expected",
55+
}
56+
form = {
57+
"source": "tracer_test",
58+
"case_id": "12345",
59+
"email": "[email protected]",
60+
"hostname": "my.hostname",
61+
}
4162
resp = await agent.post("/tracer_flare/v1", data=form)
4263
assert resp.status == 400
64+
flares = await agent.get("/test/session/tracerflares")
65+
assert await flares.json() == [expected_output]

0 commit comments

Comments
 (0)