29
29
opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument()
30
30
response = requests.get(url="https://www.example.org/")
31
31
32
- Limitations
33
- -----------
34
-
35
- Note that calls that do not use the higher-level APIs but use
36
- :code:`requests.sessions.Session.send` (or an alias thereof) directly, are
37
- currently not traced. If you find any other way to trigger an untraced HTTP
38
- request, please report it via a GitHub issue with :code:`[requests: untraced
39
- API]` in the title.
40
-
41
32
API
42
33
---
43
34
"""
44
35
45
36
import functools
46
37
import types
47
- from urllib .parse import urlparse
48
38
49
39
from requests import Timeout , URLRequired
50
40
from requests .exceptions import InvalidSchema , InvalidURL , MissingSchema
51
41
from requests .sessions import Session
42
+ from requests .structures import CaseInsensitiveDict
52
43
53
44
from opentelemetry import context , propagators
54
45
from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
57
48
from opentelemetry .trace import SpanKind , get_tracer
58
49
from opentelemetry .trace .status import Status , StatusCanonicalCode
59
50
51
+ # A key to a context variable to avoid creating duplicate spans when instrumenting
52
+ # both, Session.request and Session.send, since Session.request calls into Session.send
53
+ _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY = "suppress_requests_instrumentation"
54
+
60
55
61
56
# pylint: disable=unused-argument
62
57
def _instrument (tracer_provider = None , span_callback = None ):
@@ -71,15 +66,54 @@ def _instrument(tracer_provider=None, span_callback=None):
71
66
# before v1.0.0, Dec 17, 2012, see
72
67
# https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120)
73
68
74
- wrapped = Session .request
69
+ wrapped_request = Session .request
70
+ wrapped_send = Session .send
75
71
76
- @functools .wraps (wrapped )
72
+ @functools .wraps (wrapped_request )
77
73
def instrumented_request (self , method , url , * args , ** kwargs ):
78
- if context .get_value ("suppress_instrumentation" ):
79
- return wrapped (self , method , url , * args , ** kwargs )
74
+ def get_or_create_headers ():
75
+ headers = kwargs .get ("headers" )
76
+ if headers is None :
77
+ headers = {}
78
+ kwargs ["headers" ] = headers
79
+
80
+ return headers
81
+
82
+ def call_wrapped ():
83
+ return wrapped_request (self , method , url , * args , ** kwargs )
84
+
85
+ return _instrumented_requests_call (
86
+ method , url , call_wrapped , get_or_create_headers
87
+ )
88
+
89
+ @functools .wraps (wrapped_send )
90
+ def instrumented_send (self , request , ** kwargs ):
91
+ def get_or_create_headers ():
92
+ request .headers = (
93
+ request .headers
94
+ if request .headers is not None
95
+ else CaseInsensitiveDict ()
96
+ )
97
+ return request .headers
98
+
99
+ def call_wrapped ():
100
+ return wrapped_send (self , request , ** kwargs )
101
+
102
+ return _instrumented_requests_call (
103
+ request .method , request .url , call_wrapped , get_or_create_headers
104
+ )
105
+
106
+ def _instrumented_requests_call (
107
+ method : str , url : str , call_wrapped , get_or_create_headers
108
+ ):
109
+ if context .get_value ("suppress_instrumentation" ) or context .get_value (
110
+ _SUPPRESS_REQUESTS_INSTRUMENTATION_KEY
111
+ ):
112
+ return call_wrapped ()
80
113
81
114
# See
82
115
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client
116
+ method = method .upper ()
83
117
span_name = "HTTP {}" .format (method )
84
118
85
119
exception = None
@@ -91,17 +125,19 @@ def instrumented_request(self, method, url, *args, **kwargs):
91
125
span .set_attribute ("http.method" , method .upper ())
92
126
span .set_attribute ("http.url" , url )
93
127
94
- headers = kwargs . get ( "headers" , {}) or {}
128
+ headers = get_or_create_headers ()
95
129
propagators .inject (type (headers ).__setitem__ , headers )
96
- kwargs ["headers" ] = headers
97
130
131
+ token = context .attach (
132
+ context .set_value (_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY , True )
133
+ )
98
134
try :
99
- result = wrapped (
100
- self , method , url , * args , ** kwargs
101
- ) # *** PROCEED
135
+ result = call_wrapped () # *** PROCEED
102
136
except Exception as exc : # pylint: disable=W0703
103
137
exception = exc
104
138
result = getattr (exc , "response" , None )
139
+ finally :
140
+ context .detach (token )
105
141
106
142
if exception is not None :
107
143
span .set_status (
@@ -124,24 +160,34 @@ def instrumented_request(self, method, url, *args, **kwargs):
124
160
125
161
return result
126
162
127
- instrumented_request .opentelemetry_ext_requests_applied = True
128
-
163
+ instrumented_request .opentelemetry_instrumentation_requests_applied = True
129
164
Session .request = instrumented_request
130
165
131
- # TODO: We should also instrument requests.sessions.Session.send
132
- # but to avoid doubled spans, we would need some context-local
133
- # state (i.e., only create a Span if the current context's URL is
134
- # different, then push the current URL, pop it afterwards)
166
+ instrumented_send .opentelemetry_instrumentation_requests_applied = True
167
+ Session .send = instrumented_send
135
168
136
169
137
170
def _uninstrument ():
138
- # pylint: disable=global-statement
139
171
"""Disables instrumentation of :code:`requests` through this module.
140
172
141
173
Note that this only works if no other module also patches requests."""
142
- if getattr (Session .request , "opentelemetry_ext_requests_applied" , False ):
143
- original = Session .request .__wrapped__ # pylint:disable=no-member
144
- Session .request = original
174
+ _uninstrument_from (Session )
175
+
176
+
177
+ def _uninstrument_from (instr_root , restore_as_bound_func = False ):
178
+ for instr_func_name in ("request" , "send" ):
179
+ instr_func = getattr (instr_root , instr_func_name )
180
+ if not getattr (
181
+ instr_func ,
182
+ "opentelemetry_instrumentation_requests_applied" ,
183
+ False ,
184
+ ):
185
+ continue
186
+
187
+ original = instr_func .__wrapped__ # pylint:disable=no-member
188
+ if restore_as_bound_func :
189
+ original = types .MethodType (original , instr_root )
190
+ setattr (instr_root , instr_func_name , original )
145
191
146
192
147
193
def _exception_to_canonical_code (exc : Exception ) -> StatusCanonicalCode :
@@ -179,8 +225,4 @@ def _uninstrument(self, **kwargs):
179
225
@staticmethod
180
226
def uninstrument_session (session ):
181
227
"""Disables instrumentation on the session object."""
182
- if getattr (
183
- session .request , "opentelemetry_ext_requests_applied" , False
184
- ):
185
- original = session .request .__wrapped__ # pylint:disable=no-member
186
- session .request = types .MethodType (original , session )
228
+ _uninstrument_from (session , restore_as_bound_func = True )
0 commit comments