A test agent that emulates Datadog APM endpoints for testing client libraries. It receives traces, telemetry, metrics, and other data from tracers, providing endpoints for validation, snapshot testing, and local development.
# Activate virtual environment
source .venv/bin/activate
# Run the test agent
ddapm-test-agent --port=8126
# Run with web UI
ddapm-test-agent --port=8126 --web-ui-port=8080
# Run all tests
python -m pytest tests/ -v
# Run specific test file
python -m pytest tests/test_agent.py -v
# Run a single test
python -m pytest tests/test_agent.py::test_trace -v
# Create a release note
reno new <feature-name>
# Format snapshots
ddapm-test-agent-fmt path/to/snapshots
# Lint snapshots (check mode)
ddapm-test-agent-fmt --check path/to/snapshots
ddapm_test_agent/ # Main package
agent.py # Core application, routes, middleware (~95KB - main entry point)
trace.py # Trace decoding and handling (v0.4, v0.5, v0.7 formats)
trace_snapshot.py # Snapshot comparison logic
trace_checks.py # Trace validation checks
checks.py # Check framework infrastructure
apmtelemetry.py # APM telemetry event handling
tracestats.py # Trace statistics handling
remoteconfig.py # Remote configuration server
logs.py # OTLP logs handling
metrics.py # OTLP metrics handling
vcr_proxy.py # VCR cassette recording/playback for 3rd party APIs
llmobs_event_platform.py # LLM Observability API endpoints
client.py # Test client utilities
fmt.py # Snapshot formatting CLI
cmd.py # CLI entry points
tests/
conftest.py # Pytest fixtures (agent, payloads, helpers)
test_agent.py # Core agent endpoint tests
test_snapshot.py # Snapshot functionality tests
test_trace.py # Trace handling tests
test_session.py # Session management tests
test_<module>.py # Module-specific tests
trace_utils.py # Test utilities for trace generation
releasenotes/
notes/ # Reno release notes (YAML)
- Formatter: black (line length 120)
- Import sorting: isort (single line, google style)
- Linting: flake8
- Type checking: mypy (strict settings enabled in setup.cfg)
All functions require complete type annotations. The mypy config enforces:
disallow_incomplete_defs = truedisallow_untyped_decorators = truewarn_return_any = true
from typing import Any, Awaitable, Callable, Dict, List, Optional
async def handle_request(self, request: Request) -> web.Response:
...
def decode_payload(data: bytes, content_type: str) -> List[Dict[str, Any]]:
...
Imports are single-line, alphabetically ordered within groups:
# Standard library
import asyncio
import json
import logging
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
# Third party
from aiohttp import web
from aiohttp.web import Request
import msgpack
# Local
from ddapm_test_agent.trace import Span
from ddapm_test_agent.trace import Trace
Tests use pytest with aiohttp's pytest plugin. The agent fixture provides a test client.
- No docstrings in simple test functions (docstrings allowed for complex parametrized tests)
- Concise assertions with helpful error messages
- Use existing fixtures from
conftest.py - Async tests for HTTP endpoints
async def test_trace_put_v04(
agent,
v04_reference_http_trace_payload_headers,
v04_reference_http_trace_payload_data,
):
resp = await agent.put(
"/v0.4/traces",
headers=v04_reference_http_trace_payload_headers,
data=v04_reference_http_trace_payload_data,
)
assert resp.status == 200, await resp.text()
async def test_info(agent):
resp = await agent.get("/info")
assert resp.status == 200
data = await resp.json()
assert "version" in data
assert "endpoints" in data
agent- aiohttp test client connected to the test agentv04_reference_http_trace_payload_data- Sample v0.4 trace payload (msgpack)v04_reference_http_trace_payload_headers- Headers for trace requestsdo_reference_v04_http_trace- Helper function to send trace requestssnapshot_dir- Temporary directory for snapshot teststestagent- Full subprocess test agent for integration tests
# All tests
python -m pytest tests/ -v
# Specific file
python -m pytest tests/test_agent.py -v
# Single test
python -m pytest tests/test_snapshot.py::test_snapshot_single_trace -v
# With coverage
python -m pytest tests/ --cov=ddapm_test_agent
Requests can be associated with a test session using tokens:
- Query param:
?test_session_token=my_test - Header:
X-Datadog-Test-Session-Token: my_test
The agent supports multiple trace formats:
- v0.4: Standard msgpack format (most common)
- v0.5: Optimized format with string interning
- v0.7: Latest format with additional features
- v1: Legacy format
Characterization testing for traces:
- Send traces to the agent
- Call
/test/session/snapshotto generate/compare snapshots - Snapshots are normalized JSON files
Record and replay 3rd party API calls:
- Endpoint:
/vcr/{provider}/... - Supports: OpenAI, Azure OpenAI, Anthropic, AWS Bedrock, etc.
- Cassettes stored in
vcr-cassettes/directory
- Add handler method to appropriate module (or
agent.pyfor core endpoints) - Register route in
make_app()or module'sget_routes()method - Add tests following existing patterns
- Create release note:
reno new <feature-name>
async def handle_my_endpoint(self, request: Request) -> web.Response:
token = _session_token(request)
body = await request.json()
# Process request...
return web.json_response({"status": "ok"})
# In make_app() function
app.router.add_route("GET", "/my/endpoint", agent.handle_my_endpoint)
app.router.add_route("POST", "/my/endpoint", agent.handle_my_endpoint)
Use reno for release notes:
reno new my-feature-name
Edit the generated file in releasenotes/notes/:
---
features:
- |
Add new endpoint for X functionality.
Keep release notes concise (1-3 sentences).
import msgpack
# msgpack (most trace payloads)
data = msgpack.unpackb(await request.read(), raw=False)
# JSON
data = await request.json()
# With content-type detection
content_type = request.content_type
if "msgpack" in content_type:
data = msgpack.unpackb(await request.read(), raw=False)
else:
data = await request.json()
def _requests_by_session(self, token: Optional[str]) -> List[Request]:
if token is None:
return self._requests
return [r for r in self._requests if r.get("token") == token]
from aiohttp.web import middleware
@middleware
async def my_middleware(request: Request, handler: _Handler) -> web.Response:
# Pre-processing
response = await handler(request)
# Post-processing
return response
Key configuration options (see README.md for full list):
PORT- HTTP port (default: 8126)SNAPSHOT_DIR- Snapshot storage directorySNAPSHOT_CI- Enable CI mode (fail if snapshot missing)ENABLED_CHECKS- Comma-separated list of trace checksDD_AGENT_URL- Proxy to real Datadog agentLOG_LEVEL- Logging level (DEBUG, INFO, WARNING, ERROR)VCR_CASSETTES_DIRECTORY- VCR cassette storage path