33import base64
44import json
55import logging
6+ from contextvars import ContextVar
7+ from functools import partial
68from typing import Any
79from urllib .parse import parse_qs , urlencode , urlparse
810
@@ -55,6 +57,10 @@ def __init__(self, message: str, method: str, url: str):
5557# When these are active, urllib3 should skip creating duplicate spans
5658HIGHER_LEVEL_HTTP_INSTRUMENTATIONS = {"RequestsInstrumentation" }
5759
60+ # Set to True inside the PoolManager patch so that the HTTPConnectionPool patch
61+ # can detect the call originated from PoolManager and skip duplicate span creation.
62+ _inside_poolmanager = ContextVar ("_inside_poolmanager" , default = False )
63+
5864HEADER_SCHEMA_MERGES = {
5965 "headers" : SchemaMerge (match_importance = 0.0 ),
6066}
@@ -167,6 +173,9 @@ def patched_urlopen(
167173 # Set calling_library_context to suppress socket instrumentation warnings
168174 # for internal socket calls made by urllib3
169175 context_token = calling_library_context .set ("urllib3" )
176+ # Signal to the HTTPConnectionPool patch that this call originated
177+ # from PoolManager so it should skip creating a duplicate span.
178+ pm_token = _inside_poolmanager .set (True )
170179 try :
171180
172181 def original_call ():
@@ -189,6 +198,7 @@ def original_call():
189198 span_kind = OTelSpanKind .CLIENT ,
190199 )
191200 finally :
201+ _inside_poolmanager .reset (pm_token )
192202 calling_library_context .reset (context_token )
193203
194204 module .PoolManager .urlopen = patched_urlopen
@@ -231,46 +241,36 @@ def patched_urlopen(
231241 port_str = f":{ port } " if port and port not in (80 , 443 ) else ""
232242 full_url = f"{ scheme } ://{ host } { port_str } { url } "
233243
234- # Pass through if SDK is disabled
244+ _passthrough = partial (
245+ original_urlopen ,
246+ pool_self ,
247+ method ,
248+ url ,
249+ body = body ,
250+ headers = headers ,
251+ retries = retries ,
252+ redirect = redirect ,
253+ assert_same_host = assert_same_host ,
254+ timeout = timeout ,
255+ pool_timeout = pool_timeout ,
256+ release_conn = release_conn ,
257+ chunked = chunked ,
258+ body_pos = body_pos ,
259+ preload_content = preload_content ,
260+ decode_content = decode_content ,
261+ ** response_kw ,
262+ )
263+
235264 if sdk .mode == TuskDriftMode .DISABLED :
236- return original_urlopen (
237- pool_self ,
238- method ,
239- url ,
240- body = body ,
241- headers = headers ,
242- retries = retries ,
243- redirect = redirect ,
244- assert_same_host = assert_same_host ,
245- timeout = timeout ,
246- pool_timeout = pool_timeout ,
247- release_conn = release_conn ,
248- chunked = chunked ,
249- body_pos = body_pos ,
250- preload_content = preload_content ,
251- decode_content = decode_content ,
252- ** response_kw ,
253- )
265+ return _passthrough ()
266+
267+ # PoolManager.urlopen already created the span for this request;
268+ # skip to avoid a duplicate child span.
269+ if _inside_poolmanager .get ():
270+ return _passthrough ()
254271
255272 if instrumentation_self ._is_already_instrumented_by_higher_level ():
256- return original_urlopen (
257- pool_self ,
258- method ,
259- url ,
260- body = body ,
261- headers = headers ,
262- retries = retries ,
263- redirect = redirect ,
264- assert_same_host = assert_same_host ,
265- timeout = timeout ,
266- pool_timeout = pool_timeout ,
267- release_conn = release_conn ,
268- chunked = chunked ,
269- body_pos = body_pos ,
270- preload_content = preload_content ,
271- decode_content = decode_content ,
272- ** response_kw ,
273- )
273+ return _passthrough ()
274274
275275 # Set calling_library_context to suppress socket instrumentation warnings
276276 # for internal socket calls made by urllib3
0 commit comments