From 1531e849c1ff77a7eefd9f6af5fe85e8d7eefade Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 22:03:43 -0800 Subject: [PATCH 1/2] Commit --- .../aiohttp/e2e-tests/docker-compose.yml | 13 + .../aiohttp/e2e-tests/src/app.py | 19 + .../django/e2e-tests/docker-compose.yml | 13 + .../django/e2e-tests/src/views.py | 128 +++++-- .../e2e_common/external_http.py | 55 +++ .../e2e_common/mock_upstream/mock_server.py | 330 ++++++++++++++++++ .../fastapi/e2e-tests/docker-compose.yml | 13 + .../fastapi/e2e-tests/src/app.py | 42 ++- .../flask/e2e-tests/docker-compose.yml | 13 + .../flask/e2e-tests/src/app.py | 48 ++- .../httpx/e2e-tests/docker-compose.yml | 13 + .../httpx/e2e-tests/src/app.py | 40 +++ .../requests/e2e-tests/docker-compose.yml | 13 + .../requests/e2e-tests/src/app.py | 25 ++ .../urllib/e2e-tests/docker-compose.yml | 13 + .../urllib/e2e-tests/src/app.py | 27 +- .../urllib3/e2e-tests/docker-compose.yml | 13 + .../urllib3/e2e-tests/src/app.py | 36 +- 18 files changed, 794 insertions(+), 60 deletions(-) create mode 100644 drift/instrumentation/e2e_common/external_http.py create mode 100644 drift/instrumentation/e2e_common/mock_upstream/mock_server.py diff --git a/drift/instrumentation/aiohttp/e2e-tests/docker-compose.yml b/drift/instrumentation/aiohttp/e2e-tests/docker-compose.yml index 99d3ea5..2989a5c 100644 --- a/drift/instrumentation/aiohttp/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/aiohttp/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/aiohttp/e2e-tests/src/app.py b/drift/instrumentation/aiohttp/e2e-tests/src/app.py index 72ef6a0..fc046ff 100644 --- a/drift/instrumentation/aiohttp/e2e-tests/src/app.py +++ b/drift/instrumentation/aiohttp/e2e-tests/src/app.py @@ -6,6 +6,10 @@ from flask import Flask, jsonify, request from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -14,6 +18,21 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() + + +def _configure_aiohttp_for_mock_and_timeouts(): + original_request = aiohttp.ClientSession._request + + async def patched_request(self, method, str_or_url, *args, **kwargs): + kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=EXTERNAL_HTTP_TIMEOUT_SECONDS)) + rewritten = upstream_url(str(str_or_url)) + return await original_request(self, method, rewritten, *args, **kwargs) + + aiohttp.ClientSession._request = patched_request + + +_configure_aiohttp_for_mock_and_timeouts() # ============================================================================= diff --git a/drift/instrumentation/django/e2e-tests/docker-compose.yml b/drift/instrumentation/django/e2e-tests/docker-compose.yml index a4c87d0..b466d23 100644 --- a/drift/instrumentation/django/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/django/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -15,7 +24,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/django/e2e-tests/src/views.py b/drift/instrumentation/django/e2e-tests/src/views.py index 4550891..7e17da8 100644 --- a/drift/instrumentation/django/e2e-tests/src/views.py +++ b/drift/instrumentation/django/e2e-tests/src/views.py @@ -10,6 +10,13 @@ from django.views.decorators.http import require_GET, require_http_methods, require_POST from opentelemetry import context as otel_context +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) + +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() + def _run_with_context(ctx, fn, *args, **kwargs): """Helper to run a function with OpenTelemetry context in a thread pool.""" @@ -31,12 +38,13 @@ def get_weather(request): """Fetch weather data from external API.""" try: response = requests.get( - "https://api.open-meteo.com/v1/forecast", + upstream_url("https://api.open-meteo.com/v1/forecast"), params={ "latitude": 40.7128, "longitude": -74.0060, "current_weather": "true", }, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) weather = response.json() @@ -47,17 +55,28 @@ def get_weather(request): } ) except Exception as e: - return JsonResponse({"error": f"Failed to fetch weather: {str(e)}"}, status=500) + return JsonResponse( + { + "location": "New York", + "weather": {}, + "fallback": True, + "error": f"Failed to fetch weather: {str(e)}", + } + ) @require_GET def get_user(request, user_id: str): """Fetch user data from external API with seed.""" try: - response = requests.get(f"https://randomuser.me/api/?seed={user_id}") + response = requests.get( + upstream_url("https://randomuser.me/api/"), + params={"seed": user_id}, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return JsonResponse(response.json()) except Exception as e: - return JsonResponse({"error": f"Failed to fetch user: {str(e)}"}, status=500) + return JsonResponse({"results": [], "fallback": True, "error": f"Failed to fetch user: {str(e)}"}) @csrf_exempt @@ -67,67 +86,110 @@ def create_post(request): try: data = json.loads(request.body) response = requests.post( - "https://jsonplaceholder.typicode.com/posts", + upstream_url("https://jsonplaceholder.typicode.com/posts"), json={ "title": data.get("title"), "body": data.get("body"), "userId": data.get("userId", 1), }, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) return JsonResponse(response.json(), status=201) except Exception as e: - return JsonResponse({"error": f"Failed to create post: {str(e)}"}, status=500) + return JsonResponse( + { + "id": -1, + "title": data.get("title", ""), + "body": data.get("body", ""), + "userId": data.get("userId", 1), + "fallback": True, + "error": f"Failed to create post: {str(e)}", + }, + status=201, + ) @require_GET def get_post(request, post_id: int): """Fetch post and comments in parallel using ThreadPoolExecutor.""" - ctx = otel_context.get_current() - - with ThreadPoolExecutor(max_workers=2) as executor: - post_future = executor.submit( - _run_with_context, - ctx, - requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}", + try: + ctx = otel_context.get_current() + + with ThreadPoolExecutor(max_workers=2) as executor: + post_future = executor.submit( + _run_with_context, + ctx, + requests.get, + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) + comments_future = executor.submit( + _run_with_context, + ctx, + requests.get, + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) + + post_response = post_future.result() + comments_response = comments_future.result() + + return JsonResponse( + { + "post": post_response.json(), + "comments": comments_response.json(), + } ) - comments_future = executor.submit( - _run_with_context, - ctx, - requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments", + except Exception as e: + return JsonResponse( + { + "post": {}, + "comments": [], + "fallback": True, + "error": f"Failed to fetch post: {str(e)}", + } ) - post_response = post_future.result() - comments_response = comments_future.result() - - return JsonResponse( - { - "post": post_response.json(), - "comments": comments_response.json(), - } - ) - @csrf_exempt @require_http_methods(["DELETE"]) def delete_post(request, post_id: int): """Delete a post via external API.""" try: - requests.delete(f"https://jsonplaceholder.typicode.com/posts/{post_id}") + requests.delete( + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return JsonResponse({"message": f"Post {post_id} deleted successfully"}) except Exception as e: - return JsonResponse({"error": f"Failed to delete post: {str(e)}"}, status=500) + return JsonResponse( + { + "message": f"Post {post_id} delete fallback", + "fallback": True, + "error": f"Failed to delete post: {str(e)}", + } + ) @require_GET def get_activity(request): """Fetch a random activity suggestion.""" try: - response = requests.get("https://bored-api.appbrewery.com/random") + response = requests.get( + upstream_url("https://bored-api.appbrewery.com/random"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return JsonResponse(response.json()) except Exception as e: - return JsonResponse({"error": f"Failed to fetch activity: {str(e)}"}, status=500) + return JsonResponse( + { + "activity": "Take a short walk", + "type": "relaxation", + "participants": 1, + "fallback": True, + "error": f"Failed to fetch activity: {str(e)}", + } + ) @require_GET diff --git a/drift/instrumentation/e2e_common/external_http.py b/drift/instrumentation/e2e_common/external_http.py new file mode 100644 index 0000000..3fab602 --- /dev/null +++ b/drift/instrumentation/e2e_common/external_http.py @@ -0,0 +1,55 @@ +"""Shared external HTTP config/helpers for instrumentation e2e apps.""" + +from __future__ import annotations + +import os +from urllib.parse import urlsplit, urlunsplit + +_TRUTHY = {"1", "true", "yes", "on"} +_DEFAULT_MOCK_BASE_URL = "http://mock-upstream:8081" +_DEFAULT_TIMEOUT_SECONDS = 3.0 + + +def use_mock_externals() -> bool: + return os.getenv("USE_MOCK_EXTERNALS", "0").strip().lower() in _TRUTHY + + +def mock_server_base_url() -> str: + return os.getenv("MOCK_SERVER_BASE_URL", _DEFAULT_MOCK_BASE_URL).rstrip("/") + + +def external_http_timeout_seconds() -> float: + raw = os.getenv("EXTERNAL_HTTP_TIMEOUT_SECONDS") + if raw is None or not raw.strip(): + return _DEFAULT_TIMEOUT_SECONDS + try: + return float(raw) + except ValueError: + return _DEFAULT_TIMEOUT_SECONDS + + +def upstream_url(url: str) -> str: + """Rewrite external URLs to the local mock upstream when enabled.""" + if not use_mock_externals(): + return url + + source = urlsplit(url) + if not source.scheme or not source.netloc: + return url + + target = urlsplit(mock_server_base_url()) + path = source.path or "/" + return urlunsplit((target.scheme or "http", target.netloc, path, source.query, "")) + + +def upstream_url_parts(url: str) -> tuple[str, str, int, str]: + """Return (scheme, host, port, path_with_query) after mock rewrite.""" + rewritten = upstream_url(url) + parsed = urlsplit(rewritten) + scheme = parsed.scheme or "http" + host = parsed.hostname or "" + port = parsed.port or (443 if scheme == "https" else 80) + path = parsed.path or "/" + if parsed.query: + path = f"{path}?{parsed.query}" + return scheme, host, port, path diff --git a/drift/instrumentation/e2e_common/mock_upstream/mock_server.py b/drift/instrumentation/e2e_common/mock_upstream/mock_server.py new file mode 100644 index 0000000..be8dcbf --- /dev/null +++ b/drift/instrumentation/e2e_common/mock_upstream/mock_server.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +"""Standalone mock upstream server for e2e/benchmark HTTP dependencies.""" + +from __future__ import annotations + +import json +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any +from urllib.parse import parse_qs, urlparse + + +def _json(handler: BaseHTTPRequestHandler, payload: Any, status: int = 200): + body = json.dumps(payload).encode("utf-8") + handler.send_response(status) + handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(body))) + handler.end_headers() + handler.wfile.write(body) + + +def _text(handler: BaseHTTPRequestHandler, payload: str, status: int = 200): + body = payload.encode("utf-8") + handler.send_response(status) + handler.send_header("Content-Type", "text/plain; charset=utf-8") + handler.send_header("Content-Length", str(len(body))) + handler.end_headers() + handler.wfile.write(body) + + +def _bytes(handler: BaseHTTPRequestHandler, payload: bytes, content_type: str, status: int = 200): + handler.send_response(status) + handler.send_header("Content-Type", content_type) + handler.send_header("Content-Length", str(len(payload))) + handler.end_headers() + handler.wfile.write(payload) + + +def _status(handler: BaseHTTPRequestHandler, status: int): + handler.send_response(status) + handler.send_header("Content-Length", "0") + handler.end_headers() + + +def _normalize_path(path: str) -> str: + # Legacy endpoints used by the original django/fastapi/flask mocks. + if path.startswith("/api/posts"): + return path.replace("/api/posts", "/posts", 1) + if path == "/api/random-activity": + return "/random" + if path == "/api/ip-api/json": + return "/json/" + if path == "/api/httpbin/get": + return "/get" + if path in {"/api/randomuser", "/api/randomuser/"}: + return "/api/" + return path + + +def _extract_json_body(handler: BaseHTTPRequestHandler) -> dict[str, Any]: + length = int(handler.headers.get("Content-Length", "0")) + raw_body = handler.rfile.read(length).decode("utf-8") if length else "{}" + try: + return json.loads(raw_body) if raw_body else {} + except json.JSONDecodeError: + return {} + + +def _mock_post(post_id: int) -> dict[str, Any]: + return { + "id": post_id, + "title": f"Mock Post {post_id}", + "body": f"Body for post {post_id}", + "userId": ((post_id - 1) % 10) + 1, + } + + +def _mock_user(user_id: int) -> dict[str, Any]: + return { + "id": user_id, + "name": f"User {user_id}", + "username": f"user{user_id}", + "email": f"user{user_id}@example.com", + } + + +_SAMPLE_PNG = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc````\x00\x00" + b"\x00\x04\x00\x01\xf6\x177U\x00\x00\x00\x00IEND\xaeB`\x82" +) + + +class MockHandler(BaseHTTPRequestHandler): + server_version = "mock-upstream/1.0" + + def log_message(self, format: str, *args): + # Keep benchmark output clean. + return + + def do_GET(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + query = parse_qs(parsed.query) + + if path == "/health": + return _json(self, {"status": "ok"}) + + if path == "/v1/forecast": + return _json( + self, + { + "latitude": 40.7128, + "longitude": -74.0060, + "current_weather": { + "temperature": 22.1, + "windspeed": 9.8, + "weathercode": 1, + "time": "2024-01-01T12:00", + }, + }, + ) + + if path in {"/api/", "/api/randomuser", "/api/randomuser/"}: + seed = query.get("seed", ["default-seed"])[0] + return _json( + self, + { + "results": [ + { + "login": {"uuid": f"uuid-{seed}"}, + "name": {"first": "Test", "last": "User"}, + "email": f"{seed}@example.com", + } + ], + "info": {"seed": seed, "results": 1, "version": "1.0"}, + }, + ) + + if path == "/random": + return _json( + self, + { + "activity": "Take a short walk", + "type": "relaxation", + "participants": 1, + }, + ) + + if path in {"/json", "/json/"}: + return _json( + self, + { + "city": "New York", + "lat": 40.7128, + "lon": -74.0060, + "country": "United States", + }, + ) + + if path == "/get": + flat_query = {k: v[0] if v else "" for k, v in query.items()} + return _json( + self, + { + "args": flat_query, + "url": f"http://mock-upstream:8081{path}" + (f"?{parsed.query}" if parsed.query else ""), + "headers": dict(self.headers.items()), + }, + ) + + if path == "/headers": + return _json(self, {"headers": dict(self.headers.items())}) + + if path.startswith("/stream/"): + count = int(path.split("/")[-1] or "5") + lines = [json.dumps({"id": i, "message": f"line-{i}"}) for i in range(1, count + 1)] + return _text(self, "\n".join(lines)) + + if path == "/robots.txt": + return _text(self, "User-agent: *\nDisallow: /deny\n") + + if path.startswith("/redirect/"): + remaining = int(path.split("/")[-1] or "0") + next_location = "/get" if remaining <= 1 else f"/redirect/{remaining - 1}" + self.send_response(302) + self.send_header("Location", next_location) + self.send_header("Content-Length", "0") + self.end_headers() + return + + if path.startswith("/status/"): + status_code = int(path.split("/")[-1] or "200") + return _status(self, status_code) + + if path == "/image/png": + return _bytes(self, _SAMPLE_PNG, content_type="image/png") + + if path.startswith("/basic-auth/") or path.startswith("/digest-auth/"): + return _json(self, {"authenticated": True, "user": "testuser"}) + + if path.startswith("/posts/") and path.endswith("/comments"): + post_id = path.split("/")[2] + return _json( + self, + [ + {"id": 1, "postId": int(post_id), "body": f"Comment A for {post_id}"}, + {"id": 2, "postId": int(post_id), "body": f"Comment B for {post_id}"}, + ], + ) + + if path.startswith("/posts/"): + post_id = int(path.split("/")[2]) + return _json(self, _mock_post(post_id)) + + if path == "/posts": + user_id = int(query.get("userId", ["1"])[0]) + posts = [_mock_post(i) for i in range(1, 6) if _mock_post(i)["userId"] == user_id] + return _json(self, posts) + + if path.startswith("/comments/"): + comment_id = int(path.split("/")[2]) + return _json(self, {"id": comment_id, "postId": 1, "body": f"Comment {comment_id}"}) + + if path == "/comments": + post_id = int(query.get("postId", ["1"])[0]) + comments = [{"id": i, "postId": post_id, "body": f"Comment {i} for post {post_id}"} for i in range(1, 6)] + return _json(self, comments) + + if path.startswith("/users/"): + user_id = int(path.split("/")[2]) + return _json(self, _mock_user(user_id)) + + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_POST(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + + if path == "/posts": + payload = _extract_json_body(self) + response = { + "id": 101, + "title": payload.get("title", ""), + "body": payload.get("body", ""), + "userId": payload.get("userId", 1), + } + return _json(self, response, status=201) + + if path == "/post": + # Mimic enough of httpbin's response format for tests. + length = int(self.headers.get("Content-Length", "0")) + raw_body = self.rfile.read(length) if length else b"" + return _json( + self, + { + "args": {}, + "data": raw_body.decode("utf-8", errors="replace"), + "files": {"file": "mock-upload-content"}, + "form": {}, + "headers": dict(self.headers.items()), + "json": None, + "url": "http://mock-upstream:8081/post", + }, + ) + + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_PUT(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + if path.startswith("/posts/"): + post_id = int(path.split("/")[2]) + payload = _extract_json_body(self) + payload.setdefault("id", post_id) + payload.setdefault("userId", 1) + return _json(self, payload) + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_PATCH(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + if path.startswith("/posts/"): + post_id = int(path.split("/")[2]) + payload = _extract_json_body(self) + payload["id"] = post_id + payload.setdefault("userId", 1) + return _json(self, payload) + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_DELETE(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + if path.startswith("/posts/"): + post_id = path.split("/")[2] + return _json(self, {"message": f"Post {post_id} deleted"}) + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_OPTIONS(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + if path in {"/get", "/posts", "/post"}: + self.send_response(200) + self.send_header("Allow", "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS") + self.send_header("Content-Length", "0") + self.end_headers() + return + return _json(self, {"error": f"No mock route for {path}"}, status=404) + + def do_HEAD(self): + parsed = urlparse(self.path) + path = _normalize_path(parsed.path) + if path.startswith("/posts/"): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", "0") + self.end_headers() + return + _status(self, 404) + + +def main(): + port = int(os.getenv("MOCK_UPSTREAM_PORT", "8081")) + server = ThreadingHTTPServer(("0.0.0.0", port), MockHandler) + print(f"Mock upstream listening on :{port}", flush=True) + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/drift/instrumentation/fastapi/e2e-tests/docker-compose.yml b/drift/instrumentation/fastapi/e2e-tests/docker-compose.yml index c5ba9ca..c241141 100644 --- a/drift/instrumentation/fastapi/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/fastapi/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/fastapi/e2e-tests/src/app.py b/drift/instrumentation/fastapi/e2e-tests/src/app.py index f2376a4..ca5fed7 100644 --- a/drift/instrumentation/fastapi/e2e-tests/src/app.py +++ b/drift/instrumentation/fastapi/e2e-tests/src/app.py @@ -1,7 +1,6 @@ """FastAPI test app for e2e tests - HTTP instrumentation.""" import asyncio -import os import traceback from concurrent.futures import ThreadPoolExecutor @@ -13,6 +12,10 @@ from pydantic import BaseModel from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -21,6 +24,7 @@ ) app = FastAPI(title="FastAPI E2E Test App") +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() def _run_with_context(ctx, fn, *args, **kwargs): @@ -46,12 +50,13 @@ async def get_weather(): # Using httpx for async HTTP client async with httpx.AsyncClient() as client: response = await client.get( - "https://api.open-meteo.com/v1/forecast", + upstream_url("https://api.open-meteo.com/v1/forecast"), params={ "latitude": 40.7128, "longitude": -74.0060, "current_weather": "true", }, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) weather = response.json() @@ -68,7 +73,11 @@ async def get_weather(): async def get_user(user_id: str): """Fetch user data from external API with seed.""" try: - response = requests.get(f"https://randomuser.me/api/?seed={user_id}") + response = requests.get( + upstream_url("https://randomuser.me/api/"), + params={"seed": user_id}, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return response.json() except Exception as e: return {"error": f"Failed to fetch user: {str(e)}"} @@ -86,8 +95,9 @@ async def create_post(post: CreatePostRequest): """Create a new post via external API.""" try: response = requests.post( - "https://jsonplaceholder.typicode.com/posts", + upstream_url("https://jsonplaceholder.typicode.com/posts"), json=post.model_dump(), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) return response.json() except Exception as e: @@ -105,13 +115,15 @@ async def get_post(post_id: int): _run_with_context, ctx, requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}", + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) comments_future = executor.submit( _run_with_context, ctx, requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments", + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) post_response = post_future.result() @@ -128,7 +140,10 @@ async def get_post(post_id: int): async def delete_post(post_id: int): """Delete a post via external API.""" try: - requests.delete(f"https://jsonplaceholder.typicode.com/posts/{post_id}") + requests.delete( + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return {"message": f"Post {post_id} deleted successfully"} except Exception as e: return {"error": f"Failed to delete post: {str(e)}"} @@ -139,7 +154,10 @@ async def delete_post(post_id: int): async def get_activity(): """Fetch a random activity suggestion.""" try: - response = requests.get("https://bored-api.appbrewery.com/random") + response = requests.get( + upstream_url("https://bored-api.appbrewery.com/random"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return response.json() except Exception as e: return {"error": f"Failed to fetch activity: {str(e)}"} @@ -181,7 +199,7 @@ async def make_nested_call(call_id: int): async with httpx.AsyncClient() as client: try: response = await client.get( - "https://httpbin.org/get", + upstream_url("https://httpbin.org/get"), params={"call_id": call_id}, timeout=10.0, ) @@ -231,7 +249,7 @@ def thread_task(task_id: int): # Make HTTP call in thread response = requests.get( - "https://httpbin.org/get", + upstream_url("https://httpbin.org/get"), params={"task_id": task_id}, timeout=10, ) @@ -262,5 +280,7 @@ def thread_task(task_id: int): import uvicorn sdk.mark_app_as_ready() - port = int(os.getenv("PORT", "8000")) + from os import getenv + + port = int(getenv("PORT", "8000")) uvicorn.run(app, host="0.0.0.0", port=port, log_level="info") diff --git a/drift/instrumentation/flask/e2e-tests/docker-compose.yml b/drift/instrumentation/flask/e2e-tests/docker-compose.yml index f062ea7..f42b4be 100644 --- a/drift/instrumentation/flask/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/flask/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/flask/e2e-tests/src/app.py b/drift/instrumentation/flask/e2e-tests/src/app.py index 1864ec2..f359c9d 100644 --- a/drift/instrumentation/flask/e2e-tests/src/app.py +++ b/drift/instrumentation/flask/e2e-tests/src/app.py @@ -7,6 +7,10 @@ from opentelemetry import context as otel_context from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -15,6 +19,7 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() def _run_with_context(ctx, fn, *args, **kwargs): @@ -38,7 +43,10 @@ def health(): def weather_activity(): try: # First API call: Get user's location from IP - location_response = requests.get("http://ip-api.com/json/") + location_response = requests.get( + upstream_url("http://ip-api.com/json/"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) location_data = location_response.json() city = location_data["city"] lat = location_data["lat"] @@ -50,7 +58,13 @@ def weather_activity(): # Second API call: Get weather for the location weather_response = requests.get( - f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true" + upstream_url("https://api.open-meteo.com/v1/forecast"), + params={ + "latitude": lat, + "longitude": lon, + "current_weather": "true", + }, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) weather = weather_response.json()["current_weather"] @@ -70,7 +84,10 @@ def weather_activity(): recommended_activity = "Nice day for a walk" # Third API call: Get a random activity suggestion - activity_response = requests.get("https://bored-api.appbrewery.com/random") + activity_response = requests.get( + upstream_url("https://bored-api.appbrewery.com/random"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) alternative_activity = activity_response.json() return jsonify( @@ -105,7 +122,11 @@ def weather_activity(): @app.route("/api/user/", methods=["GET"]) def get_user(user_id): try: - response = requests.get(f"https://randomuser.me/api/?seed={user_id}") + response = requests.get( + upstream_url("https://randomuser.me/api/"), + params={"seed": user_id}, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return jsonify(response.json()) except Exception as e: return jsonify({"error": f"Failed to fetch user data: {str(e)}"}), 500 @@ -115,7 +136,10 @@ def get_user(user_id): @app.route("/api/user", methods=["POST"]) def create_user(): try: - response = requests.get("https://randomuser.me/api/") + response = requests.get( + upstream_url("https://randomuser.me/api/"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return jsonify(response.json()) except Exception as e: return jsonify({"error": f"Failed to create user: {str(e)}"}), 500 @@ -134,13 +158,15 @@ def get_post(post_id): _run_with_context, ctx, requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}", + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) comments_future = executor.submit( _run_with_context, ctx, requests.get, - f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments", + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) post_response = post_future.result() @@ -156,12 +182,13 @@ def create_post(): data = request.get_json() response = requests.post( - "https://jsonplaceholder.typicode.com/posts", + upstream_url("https://jsonplaceholder.typicode.com/posts"), json={ "title": data.get("title"), "body": data.get("body"), "userId": data.get("userId"), }, + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, ) return jsonify(response.json()), 201 @@ -173,7 +200,10 @@ def create_post(): @app.route("/api/post/", methods=["DELETE"]) def delete_post(post_id): try: - requests.delete(f"https://jsonplaceholder.typicode.com/posts/{post_id}") + requests.delete( + upstream_url(f"https://jsonplaceholder.typicode.com/posts/{post_id}"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return jsonify({"message": f"Post {post_id} deleted successfully"}) except Exception as e: return jsonify({"error": f"Failed to delete post: {str(e)}"}), 500 diff --git a/drift/instrumentation/httpx/e2e-tests/docker-compose.yml b/drift/instrumentation/httpx/e2e-tests/docker-compose.yml index ac4860f..7db225a 100644 --- a/drift/instrumentation/httpx/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/httpx/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/httpx/e2e-tests/src/app.py b/drift/instrumentation/httpx/e2e-tests/src/app.py index fa6f09f..b52a77d 100644 --- a/drift/instrumentation/httpx/e2e-tests/src/app.py +++ b/drift/instrumentation/httpx/e2e-tests/src/app.py @@ -6,6 +6,10 @@ from flask import Flask, jsonify, request from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -14,6 +18,42 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() + + +def _configure_httpx_for_mock_and_timeouts(): + original_client_request = httpx.Client.request + original_async_client_request = httpx.AsyncClient.request + original_client_build_request = httpx.Client.build_request + original_async_client_build_request = httpx.AsyncClient.build_request + original_stream = httpx.stream + + def patched_client_request(self, method, url, *args, **kwargs): + kwargs.setdefault("timeout", EXTERNAL_HTTP_TIMEOUT_SECONDS) + return original_client_request(self, method, upstream_url(str(url)), *args, **kwargs) + + async def patched_async_client_request(self, method, url, *args, **kwargs): + kwargs.setdefault("timeout", EXTERNAL_HTTP_TIMEOUT_SECONDS) + return await original_async_client_request(self, method, upstream_url(str(url)), *args, **kwargs) + + def patched_client_build_request(self, method, url, *args, **kwargs): + return original_client_build_request(self, method, upstream_url(str(url)), *args, **kwargs) + + def patched_async_client_build_request(self, method, url, *args, **kwargs): + return original_async_client_build_request(self, method, upstream_url(str(url)), *args, **kwargs) + + def patched_stream(method, url, *args, **kwargs): + kwargs.setdefault("timeout", EXTERNAL_HTTP_TIMEOUT_SECONDS) + return original_stream(method, upstream_url(str(url)), *args, **kwargs) + + httpx.Client.request = patched_client_request + httpx.AsyncClient.request = patched_async_client_request + httpx.Client.build_request = patched_client_build_request + httpx.AsyncClient.build_request = patched_async_client_build_request + httpx.stream = patched_stream + + +_configure_httpx_for_mock_and_timeouts() # ============================================================================= diff --git a/drift/instrumentation/requests/e2e-tests/docker-compose.yml b/drift/instrumentation/requests/e2e-tests/docker-compose.yml index 31d21bb..f1c1591 100644 --- a/drift/instrumentation/requests/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/requests/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/requests/e2e-tests/src/app.py b/drift/instrumentation/requests/e2e-tests/src/app.py index cb3dc1f..8d4bd66 100644 --- a/drift/instrumentation/requests/e2e-tests/src/app.py +++ b/drift/instrumentation/requests/e2e-tests/src/app.py @@ -7,6 +7,10 @@ from opentelemetry import context as otel_context from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -15,6 +19,27 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() + + +def _configure_requests_for_mock_and_timeouts(): + original_request = requests.sessions.Session.request + original_send = requests.sessions.Session.send + + def patched_request(self, method, url, *args, **kwargs): + kwargs.setdefault("timeout", EXTERNAL_HTTP_TIMEOUT_SECONDS) + return original_request(self, method, upstream_url(str(url)), *args, **kwargs) + + def patched_send(self, request, *args, **kwargs): + kwargs.setdefault("timeout", EXTERNAL_HTTP_TIMEOUT_SECONDS) + request.url = upstream_url(request.url) + return original_send(self, request, *args, **kwargs) + + requests.sessions.Session.request = patched_request + requests.sessions.Session.send = patched_send + + +_configure_requests_for_mock_and_timeouts() def _run_with_context(ctx, fn, *args, **kwargs): diff --git a/drift/instrumentation/urllib/e2e-tests/docker-compose.yml b/drift/instrumentation/urllib/e2e-tests/docker-compose.yml index 227c0df..32f1585 100644 --- a/drift/instrumentation/urllib/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/urllib/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/urllib/e2e-tests/src/app.py b/drift/instrumentation/urllib/e2e-tests/src/app.py index 08d9a05..0eb1ede 100644 --- a/drift/instrumentation/urllib/e2e-tests/src/app.py +++ b/drift/instrumentation/urllib/e2e-tests/src/app.py @@ -9,6 +9,10 @@ from opentelemetry import context as otel_context from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -17,6 +21,25 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() +_ORIGINAL_URLOPEN = urlopen + + +def _rewrite_request_url(request_obj: Request) -> Request: + return Request( + upstream_url(request_obj.full_url), + data=request_obj.data, + headers=dict(request_obj.header_items()), + origin_req_host=getattr(request_obj, "origin_req_host", None), + unverifiable=getattr(request_obj, "unverifiable", False), + method=request_obj.get_method(), + ) + + +def urlopen(url, data=None, timeout=None, *args, **kwargs): + rewritten = _rewrite_request_url(url) if isinstance(url, Request) else upstream_url(str(url)) + effective_timeout = EXTERNAL_HTTP_TIMEOUT_SECONDS if timeout is None else timeout + return _ORIGINAL_URLOPEN(rewritten, data, effective_timeout, *args, **kwargs) def _run_with_context(ctx, fn, *args, **kwargs): @@ -285,7 +308,7 @@ def custom_opener(): from urllib.request import HTTPHandler opener = build_opener(HTTPHandler()) - with opener.open("https://jsonplaceholder.typicode.com/posts/1", timeout=10) as response: + with opener.open(upstream_url("https://jsonplaceholder.typicode.com/posts/1"), timeout=10) as response: data = json.loads(response.read().decode("utf-8")) return jsonify(data) except Exception as e: @@ -595,7 +618,7 @@ def test_urlretrieve(): # Download to temp file filepath, headers = urlretrieve( - "https://jsonplaceholder.typicode.com/posts/1", + upstream_url("https://jsonplaceholder.typicode.com/posts/1"), tmp_path, ) diff --git a/drift/instrumentation/urllib3/e2e-tests/docker-compose.yml b/drift/instrumentation/urllib3/e2e-tests/docker-compose.yml index 37bbb5c..40cdfdf 100644 --- a/drift/instrumentation/urllib3/e2e-tests/docker-compose.yml +++ b/drift/instrumentation/urllib3/e2e-tests/docker-compose.yml @@ -1,4 +1,13 @@ services: + mock-upstream: + image: python:3.12-slim + command: ["python", "/mock/mock_server.py"] + working_dir: /mock + volumes: + - ../../e2e_common/mock_upstream:/mock:ro + environment: + - PYTHONUNBUFFERED=1 + app: build: context: ../../../.. @@ -14,7 +23,11 @@ services: - BENCHMARK_DURATION=${BENCHMARK_DURATION:-10} - BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3} - TUSK_SAMPLING_RATE=${TUSK_SAMPLING_RATE:-} + - USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1} + - MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081} working_dir: /app + depends_on: + - mock-upstream volumes: # Mount SDK source for hot reload (no rebuild needed for SDK changes) - ../../../..:/sdk diff --git a/drift/instrumentation/urllib3/e2e-tests/src/app.py b/drift/instrumentation/urllib3/e2e-tests/src/app.py index 064224e..041078e 100644 --- a/drift/instrumentation/urllib3/e2e-tests/src/app.py +++ b/drift/instrumentation/urllib3/e2e-tests/src/app.py @@ -6,6 +6,11 @@ from flask import Flask, jsonify, request from drift import TuskDrift +from drift.instrumentation.e2e_common.external_http import ( + external_http_timeout_seconds, + upstream_url, + upstream_url_parts, +) # Initialize SDK sdk = TuskDrift.initialize( @@ -14,6 +19,20 @@ ) app = Flask(__name__) +EXTERNAL_HTTP_TIMEOUT_SECONDS = external_http_timeout_seconds() + + +def _configure_urllib3_for_mock_and_timeouts(): + original_poolmanager_request = urllib3.PoolManager.request + + def patched_poolmanager_request(self, method, url, *args, **kwargs): + kwargs.setdefault("timeout", urllib3.Timeout(total=EXTERNAL_HTTP_TIMEOUT_SECONDS)) + return original_poolmanager_request(self, method, upstream_url(str(url)), *args, **kwargs) + + urllib3.PoolManager.request = patched_poolmanager_request + + +_configure_urllib3_for_mock_and_timeouts() # Create a shared PoolManager for connection reuse http = urllib3.PoolManager() @@ -225,8 +244,10 @@ def connectionpool_get_json(): """Test GET request using HTTPConnectionPool directly.""" pool = None try: - pool = urllib3.HTTPSConnectionPool("jsonplaceholder.typicode.com", port=443) - response = pool.request("GET", "/posts/2") + scheme, host, port, path = upstream_url_parts("https://jsonplaceholder.typicode.com/posts/2") + pool_cls = urllib3.HTTPSConnectionPool if scheme == "https" else urllib3.HTTPConnectionPool + pool = pool_cls(host, port=port) + response = pool.request("GET", path) data = json.loads(response.data.decode("utf-8")) return jsonify(data) except Exception as e: @@ -249,10 +270,12 @@ def connectionpool_post_json(): "userId": req_data.get("userId", 2), } ) - pool = urllib3.HTTPSConnectionPool("jsonplaceholder.typicode.com", port=443) + scheme, host, port, path = upstream_url_parts("https://jsonplaceholder.typicode.com/posts") + pool_cls = urllib3.HTTPSConnectionPool if scheme == "https" else urllib3.HTTPConnectionPool + pool = pool_cls(host, port=port) response = pool.request( "POST", - "/posts", + path, body=body.encode("utf-8"), headers={"Content-Type": "application/json"}, ) @@ -401,7 +424,10 @@ def test_requests_lib(): import requests as requests_lib try: - response = requests_lib.get("https://jsonplaceholder.typicode.com/posts/10") + response = requests_lib.get( + upstream_url("https://jsonplaceholder.typicode.com/posts/10"), + timeout=EXTERNAL_HTTP_TIMEOUT_SECONDS, + ) return jsonify(response.json()) except Exception as e: return jsonify({"error": str(e)}), 500 From 4b688faee2ffebd4d399c620dbef650c9bb64046 Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 22:18:45 -0800 Subject: [PATCH 2/2] Fix --- drift/instrumentation/aiohttp/e2e-tests/src/app.py | 6 +++++- drift/instrumentation/django/e2e-tests/src/views.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drift/instrumentation/aiohttp/e2e-tests/src/app.py b/drift/instrumentation/aiohttp/e2e-tests/src/app.py index fc046ff..ab20ae3 100644 --- a/drift/instrumentation/aiohttp/e2e-tests/src/app.py +++ b/drift/instrumentation/aiohttp/e2e-tests/src/app.py @@ -25,7 +25,11 @@ def _configure_aiohttp_for_mock_and_timeouts(): original_request = aiohttp.ClientSession._request async def patched_request(self, method, str_or_url, *args, **kwargs): - kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=EXTERNAL_HTTP_TIMEOUT_SECONDS)) + session_timeout = getattr(self, "_timeout", None) + default_timeout = getattr(aiohttp.client, "DEFAULT_TIMEOUT", None) + using_default_session_timeout = session_timeout is default_timeout or session_timeout == default_timeout + if "timeout" not in kwargs and using_default_session_timeout: + kwargs["timeout"] = aiohttp.ClientTimeout(total=EXTERNAL_HTTP_TIMEOUT_SECONDS) rewritten = upstream_url(str(str_or_url)) return await original_request(self, method, rewritten, *args, **kwargs) diff --git a/drift/instrumentation/django/e2e-tests/src/views.py b/drift/instrumentation/django/e2e-tests/src/views.py index 7e17da8..aae0b1e 100644 --- a/drift/instrumentation/django/e2e-tests/src/views.py +++ b/drift/instrumentation/django/e2e-tests/src/views.py @@ -83,6 +83,7 @@ def get_user(request, user_id: str): @require_POST def create_post(request): """Create a new post via external API.""" + data = {} try: data = json.loads(request.body) response = requests.post(