Skip to content

feat: httpx instrumentation#16

Merged
jy-tan merged 2 commits intomainfrom
httpx
Jan 8, 2026
Merged

feat: httpx instrumentation#16
jy-tan merged 2 commits intomainfrom
httpx

Conversation

@jy-tan
Copy link
Contributor

@jy-tan jy-tan commented Jan 8, 2026

Summary

Adds instrumentation for the httpx HTTP client library, supporting both synchronous (httpx.Client) and asynchronous (httpx.AsyncClient) clients.

Changes

New files:

  • drift/instrumentation/httpx/__init__.py - Package exports
  • drift/instrumentation/httpx/instrumentation.py - Main instrumentation

Modified files:

  • drift/core/drift_sdk.py - Auto-initialize httpx instrumentation when the library is available
  • drift/instrumentation/requests/instrumentation.py - Set calling_library_context during network calls
  • drift/instrumentation/socket/instrumentation.py - Fix unpatched dependency detection logic

Features

  • RECORD mode: Captures httpx request/response data as CLIENT spans with proper schema merges
  • REPLAY mode: Returns mocked responses via find_mock_response_async/find_mock_response_sync
  • Transform support: Integrates with HttpTransformEngine for drop/redact/mask transforms
  • Async support: Full async/await support for httpx.AsyncClient

Socket instrumentation fix

The socket instrumentation detects unpatched dependencies by monitoring raw TCP calls during REPLAY mode. Previously, it only allowed ProtobufCommunicator (SDK's internal communication) and would incorrectly flag instrumented HTTP libraries as "unpatched".

The fix changes the check from:

if calling_library != "ProtobufCommunicator":  # ❌ Flags httpx/requests

to:

if calling_library is None:  # ✅ Only flags truly unpatched libraries

Both httpx and requests instrumentations now set calling_library_context when making network calls, preventing false positives.

Testing

  • FastAPI e2e test passes (10/10 tests)
  • httpx spans verified in recorded traces (HttpxInstrumentation CLIENT spans)
  • No socket instrumentation warnings in REPLAY mode

@jy-tan jy-tan merged commit f022c94 into main Jan 8, 2026
5 checks passed
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 5 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="drift/instrumentation/httpx/instrumentation.py">

<violation number="1" location="drift/instrumentation/httpx/instrumentation.py:175">
P1: `span.end()` is called twice when request is dropped by transform - once here and again in the outer finally block. Remove this call since the finally block already handles span termination.</violation>

<violation number="2" location="drift/instrumentation/httpx/instrumentation.py:199">
P2: Dead code: duration calculation result is discarded. Either remove this line or assign the result to a variable for use (e.g., passing to `_finalize_span`).</violation>

<violation number="3" location="drift/instrumentation/httpx/instrumentation.py:412">
P2: Potential TypeError when headers=None in async mock lookup; guard with `kwargs.get("headers") or {}` before dict().</violation>

<violation number="4" location="drift/instrumentation/httpx/instrumentation.py:656">
P2: Potential TypeError during span finalization when headers=None; guard with `request_kwargs.get("headers") or {}` before dict().</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

json.dumps({"bodyProcessingError": "dropped"}),
)
span.set_status(Status(OTelStatusCode.ERROR, "Dropped by transform"))
span.end()
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: span.end() is called twice when request is dropped by transform - once here and again in the outer finally block. Remove this call since the finally block already handles span termination.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drift/instrumentation/httpx/instrumentation.py, line 175:

<comment>`span.end()` is called twice when request is dropped by transform - once here and again in the outer finally block. Remove this call since the finally block already handles span termination.</comment>

<file context>
@@ -0,0 +1,992 @@
+                        json.dumps({"bodyProcessingError": "dropped"}),
+                    )
+                    span.set_status(Status(OTelStatusCode.ERROR, "Dropped by transform"))
+                    span.end()
+
+                    raise RequestDroppedByTransform(
</file context>
Fix with Cubic

finally:
calling_library_context.reset(calling_lib_token)
# Finalize span with request/response data
(time.time_ns() - start_time_ns) / 1_000_000
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Dead code: duration calculation result is discarded. Either remove this line or assign the result to a variable for use (e.g., passing to _finalize_span).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drift/instrumentation/httpx/instrumentation.py, line 199:

<comment>Dead code: duration calculation result is discarded. Either remove this line or assign the result to a variable for use (e.g., passing to `_finalize_span`).</comment>

<file context>
@@ -0,0 +1,992 @@
+                finally:
+                    calling_library_context.reset(calling_lib_token)
+                    # Finalize span with request/response data
+                    (time.time_ns() - start_time_ns) / 1_000_000
+                    instrumentation._finalize_span(
+                        span,
</file context>
Suggested change
(time.time_ns() - start_time_ns) / 1_000_000
duration_ms = (time.time_ns() - start_time_ns) / 1_000_000
Fix with Cubic

parsed_url = urlparse(url)

# ===== BUILD INPUT VALUE =====
headers = dict(request_kwargs.get("headers", {}))
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Potential TypeError during span finalization when headers=None; guard with request_kwargs.get("headers") or {} before dict().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drift/instrumentation/httpx/instrumentation.py, line 656:

<comment>Potential TypeError during span finalization when headers=None; guard with `request_kwargs.get("headers") or {}` before dict().</comment>

<file context>
@@ -0,0 +1,992 @@
+            parsed_url = urlparse(url)
+
+            # ===== BUILD INPUT VALUE =====
+            headers = dict(request_kwargs.get("headers", {}))
+            params = dict(request_kwargs.get("params", {})) if request_kwargs.get("params") else {}
+
</file context>
Suggested change
headers = dict(request_kwargs.get("headers", {}))
headers = dict(request_kwargs.get("headers") or {})
Fix with Cubic

parsed_url = urlparse(url)

# Extract request data
headers = dict(kwargs.get("headers", {}))
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Potential TypeError when headers=None in async mock lookup; guard with kwargs.get("headers") or {} before dict().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drift/instrumentation/httpx/instrumentation.py, line 412:

<comment>Potential TypeError when headers=None in async mock lookup; guard with `kwargs.get("headers") or {}` before dict().</comment>

<file context>
@@ -0,0 +1,992 @@
+            parsed_url = urlparse(url)
+
+            # Extract request data
+            headers = dict(kwargs.get("headers", {}))
+            params = dict(kwargs.get("params", {})) if kwargs.get("params") else {}
+
</file context>
Suggested change
headers = dict(kwargs.get("headers", {}))
headers = dict(kwargs.get("headers") or {})
Fix with Cubic

@jy-tan jy-tan deleted the httpx branch January 8, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant