5050
5151import functools
5252import types
53- from typing import Collection
53+ from timeit import default_timer
54+ from typing import Callable , Collection , Iterable , Optional
55+ from urllib .parse import urlparse
5456
5557from requests .models import Response
5658from requests .sessions import Session
6769 _SUPPRESS_INSTRUMENTATION_KEY ,
6870 http_status_to_status_code ,
6971)
72+ from opentelemetry .metrics import Histogram , get_meter
7073from opentelemetry .propagate import inject
7174from opentelemetry .semconv .trace import SpanAttributes
72- from opentelemetry .trace import SpanKind , get_tracer
75+ from opentelemetry .trace import SpanKind , Tracer , get_tracer
76+ from opentelemetry .trace .span import Span
7377from opentelemetry .trace .status import Status
7478from opentelemetry .util .http import (
7579 get_excluded_urls ,
8488# pylint: disable=unused-argument
8589# pylint: disable=R0915
8690def _instrument (
87- tracer , span_callback = None , name_callback = None , excluded_urls = None
91+ tracer : Tracer ,
92+ duration_histogram : Histogram ,
93+ span_callback : Optional [Callable [[Span , Response ], str ]] = None ,
94+ name_callback : Optional [Callable [[str , str ], str ]] = None ,
95+ excluded_urls : Iterable [str ] = None ,
8896):
8997 """Enables tracing of all requests calls that go through
9098 :code:`requests.session.Session.request` (this includes
@@ -140,6 +148,7 @@ def call_wrapped():
140148 request .method , request .url , call_wrapped , get_or_create_headers
141149 )
142150
151+ # pylint: disable-msg=too-many-locals,too-many-branches
143152 def _instrumented_requests_call (
144153 method : str , url : str , call_wrapped , get_or_create_headers
145154 ):
@@ -164,6 +173,23 @@ def _instrumented_requests_call(
164173 SpanAttributes .HTTP_URL : url ,
165174 }
166175
176+ metric_labels = {
177+ SpanAttributes .HTTP_METHOD : method ,
178+ }
179+
180+ try :
181+ parsed_url = urlparse (url )
182+ metric_labels [SpanAttributes .HTTP_SCHEME ] = parsed_url .scheme
183+ if parsed_url .hostname :
184+ metric_labels [SpanAttributes .HTTP_HOST ] = parsed_url .hostname
185+ metric_labels [
186+ SpanAttributes .NET_PEER_NAME
187+ ] = parsed_url .hostname
188+ if parsed_url .port :
189+ metric_labels [SpanAttributes .NET_PEER_PORT ] = parsed_url .port
190+ except ValueError :
191+ pass
192+
167193 with tracer .start_as_current_span (
168194 span_name , kind = SpanKind .CLIENT , attributes = span_attributes
169195 ) as span , set_ip_on_next_http_connection (span ):
@@ -175,12 +201,18 @@ def _instrumented_requests_call(
175201 token = context .attach (
176202 context .set_value (_SUPPRESS_HTTP_INSTRUMENTATION_KEY , True )
177203 )
204+
205+ start_time = default_timer ()
206+
178207 try :
179208 result = call_wrapped () # *** PROCEED
180209 except Exception as exc : # pylint: disable=W0703
181210 exception = exc
182211 result = getattr (exc , "response" , None )
183212 finally :
213+ elapsed_time = max (
214+ round ((default_timer () - start_time ) * 1000 ), 0
215+ )
184216 context .detach (token )
185217
186218 if isinstance (result , Response ):
@@ -191,9 +223,23 @@ def _instrumented_requests_call(
191223 span .set_status (
192224 Status (http_status_to_status_code (result .status_code ))
193225 )
226+
227+ metric_labels [
228+ SpanAttributes .HTTP_STATUS_CODE
229+ ] = result .status_code
230+
231+ if result .raw is not None :
232+ version = getattr (result .raw , "version" , None )
233+ if version :
234+ metric_labels [SpanAttributes .HTTP_FLAVOR ] = (
235+ "1.1" if version == 11 else "1.0"
236+ )
237+
194238 if span_callback is not None :
195239 span_callback (span , result )
196240
241+ duration_histogram .record (elapsed_time , attributes = metric_labels )
242+
197243 if exception is not None :
198244 raise exception .with_traceback (exception .__traceback__ )
199245
@@ -258,8 +304,20 @@ def _instrument(self, **kwargs):
258304 tracer_provider = kwargs .get ("tracer_provider" )
259305 tracer = get_tracer (__name__ , __version__ , tracer_provider )
260306 excluded_urls = kwargs .get ("excluded_urls" )
307+ meter_provider = kwargs .get ("meter_provider" )
308+ meter = get_meter (
309+ __name__ ,
310+ __version__ ,
311+ meter_provider ,
312+ )
313+ duration_histogram = meter .create_histogram (
314+ name = "http.client.duration" ,
315+ unit = "ms" ,
316+ description = "measures the duration of the outbound HTTP request" ,
317+ )
261318 _instrument (
262319 tracer ,
320+ duration_histogram ,
263321 span_callback = kwargs .get ("span_callback" ),
264322 name_callback = kwargs .get ("name_callback" ),
265323 excluded_urls = _excluded_urls_from_env
0 commit comments