@@ -90,7 +90,9 @@ def response_hook(span: Span, params: typing.Union[
9090
9191import types
9292import typing
93+ from timeit import default_timer
9394from typing import Collection
95+ from urllib .parse import urlparse
9496
9597import aiohttp
9698import wrapt
@@ -99,11 +101,20 @@ def response_hook(span: Span, params: typing.Union[
99101from opentelemetry import context as context_api
100102from opentelemetry import trace
101103from opentelemetry .instrumentation ._semconv import (
104+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
105+ HTTP_DURATION_HISTOGRAM_BUCKETS_OLD ,
106+ _client_duration_attrs_new ,
107+ _client_duration_attrs_old ,
108+ _filter_semconv_duration_attrs ,
102109 _get_schema_url ,
103110 _OpenTelemetrySemanticConventionStability ,
104111 _OpenTelemetryStabilitySignalType ,
105112 _report_new ,
113+ _report_old ,
114+ _set_http_host_client ,
106115 _set_http_method ,
116+ _set_http_net_peer_name_client ,
117+ _set_http_peer_port_client ,
107118 _set_http_url ,
108119 _set_status ,
109120 _StabilityMode ,
@@ -115,8 +126,13 @@ def response_hook(span: Span, params: typing.Union[
115126 is_instrumentation_enabled ,
116127 unwrap ,
117128)
129+ from opentelemetry .metrics import MeterProvider , get_meter
118130from opentelemetry .propagate import inject
119131from opentelemetry .semconv .attributes .error_attributes import ERROR_TYPE
132+ from opentelemetry .semconv .metrics import MetricInstruments
133+ from opentelemetry .semconv .metrics .http_metrics import (
134+ HTTP_CLIENT_REQUEST_DURATION ,
135+ )
120136from opentelemetry .trace import Span , SpanKind , TracerProvider , get_tracer
121137from opentelemetry .trace .status import Status , StatusCode
122138from opentelemetry .util .http import remove_url_credentials , sanitize_method
@@ -172,11 +188,14 @@ def _set_http_status_code_attribute(
172188 )
173189
174190
191+ # pylint: disable=too-many-locals
192+ # pylint: disable=too-many-statements
175193def create_trace_config (
176194 url_filter : _UrlFilterT = None ,
177195 request_hook : _RequestHookT = None ,
178196 response_hook : _ResponseHookT = None ,
179197 tracer_provider : TracerProvider = None ,
198+ meter_provider : MeterProvider = None ,
180199 sem_conv_opt_in_mode : _StabilityMode = _StabilityMode .DEFAULT ,
181200) -> aiohttp .TraceConfig :
182201 """Create an aiohttp-compatible trace configuration.
@@ -205,6 +224,7 @@ def create_trace_config(
205224 :param Callable request_hook: Optional callback that can modify span name and request params.
206225 :param Callable response_hook: Optional callback that can modify span name and response params.
207226 :param tracer_provider: optional TracerProvider from which to get a Tracer
227+ :param meter_provider: optional Meter provider to use
208228
209229 :return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
210230 :rtype: :py:class:`aiohttp.TraceConfig`
@@ -214,20 +234,70 @@ def create_trace_config(
214234 # Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work
215235 # around this issue.
216236
237+ schema_url = _get_schema_url (sem_conv_opt_in_mode )
238+
217239 tracer = get_tracer (
218240 __name__ ,
219241 __version__ ,
220242 tracer_provider ,
221- schema_url = _get_schema_url (sem_conv_opt_in_mode ),
243+ schema_url = schema_url ,
244+ )
245+
246+ meter = get_meter (
247+ __name__ ,
248+ __version__ ,
249+ meter_provider ,
250+ schema_url ,
222251 )
223252
224- # TODO: Use this when we have durations for aiohttp-client
253+ start_time = 0
254+
255+ duration_histogram_old = None
256+ if _report_old (sem_conv_opt_in_mode ):
257+ duration_histogram_old = meter .create_histogram (
258+ name = MetricInstruments .HTTP_CLIENT_DURATION ,
259+ unit = "ms" ,
260+ description = "measures the duration of the outbound HTTP request" ,
261+ explicit_bucket_boundaries_advisory = HTTP_DURATION_HISTOGRAM_BUCKETS_OLD ,
262+ )
263+ duration_histogram_new = None
264+ if _report_new (sem_conv_opt_in_mode ):
265+ duration_histogram_new = meter .create_histogram (
266+ name = HTTP_CLIENT_REQUEST_DURATION ,
267+ unit = "s" ,
268+ description = "Duration of HTTP client requests." ,
269+ explicit_bucket_boundaries_advisory = HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
270+ )
271+
225272 metric_attributes = {}
226273
227274 def _end_trace (trace_config_ctx : types .SimpleNamespace ):
275+ elapsed_time = max (default_timer () - trace_config_ctx .start_time , 0 )
228276 context_api .detach (trace_config_ctx .token )
229277 trace_config_ctx .span .end ()
230278
279+ if trace_config_ctx .duration_histogram_old is not None :
280+ duration_attrs_old = _filter_semconv_duration_attrs (
281+ metric_attributes ,
282+ _client_duration_attrs_old ,
283+ _client_duration_attrs_new ,
284+ _StabilityMode .DEFAULT ,
285+ )
286+ trace_config_ctx .duration_histogram_old .record (
287+ max (round (elapsed_time * 1000 ), 0 ),
288+ attributes = duration_attrs_old ,
289+ )
290+ if trace_config_ctx .duration_histogram_new is not None :
291+ duration_attrs_new = _filter_semconv_duration_attrs (
292+ metric_attributes ,
293+ _client_duration_attrs_old ,
294+ _client_duration_attrs_new ,
295+ _StabilityMode .HTTP ,
296+ )
297+ trace_config_ctx .duration_histogram_new .record (
298+ elapsed_time , attributes = duration_attrs_new
299+ )
300+
231301 async def on_request_start (
232302 unused_session : aiohttp .ClientSession ,
233303 trace_config_ctx : types .SimpleNamespace ,
@@ -237,6 +307,7 @@ async def on_request_start(
237307 trace_config_ctx .span = None
238308 return
239309
310+ trace_config_ctx .start_time = default_timer ()
240311 method = params .method
241312 request_span_name = _get_span_name (method )
242313 request_url = (
@@ -252,8 +323,44 @@ async def on_request_start(
252323 sanitize_method (method ),
253324 sem_conv_opt_in_mode ,
254325 )
326+ _set_http_method (
327+ metric_attributes ,
328+ method ,
329+ sanitize_method (method ),
330+ sem_conv_opt_in_mode ,
331+ )
255332 _set_http_url (span_attributes , request_url , sem_conv_opt_in_mode )
256333
334+ try :
335+ parsed_url = urlparse (request_url )
336+ if parsed_url .hostname :
337+ _set_http_host_client (
338+ metric_attributes ,
339+ parsed_url .hostname ,
340+ sem_conv_opt_in_mode ,
341+ )
342+ _set_http_net_peer_name_client (
343+ metric_attributes ,
344+ parsed_url .hostname ,
345+ sem_conv_opt_in_mode ,
346+ )
347+ if _report_new (sem_conv_opt_in_mode ):
348+ _set_http_host_client (
349+ span_attributes ,
350+ parsed_url .hostname ,
351+ sem_conv_opt_in_mode ,
352+ )
353+ if parsed_url .port :
354+ _set_http_peer_port_client (
355+ metric_attributes , parsed_url .port , sem_conv_opt_in_mode
356+ )
357+ if _report_new (sem_conv_opt_in_mode ):
358+ _set_http_peer_port_client (
359+ span_attributes , parsed_url .port , sem_conv_opt_in_mode
360+ )
361+ except ValueError :
362+ pass
363+
257364 trace_config_ctx .span = trace_config_ctx .tracer .start_span (
258365 request_span_name , kind = SpanKind .CLIENT , attributes = span_attributes
259366 )
@@ -298,6 +405,7 @@ async def on_request_exception(
298405 exc_type = type (params .exception ).__qualname__
299406 if _report_new (sem_conv_opt_in_mode ):
300407 trace_config_ctx .span .set_attribute (ERROR_TYPE , exc_type )
408+ metric_attributes [ERROR_TYPE ] = exc_type
301409
302410 trace_config_ctx .span .set_status (
303411 Status (StatusCode .ERROR , exc_type )
@@ -312,7 +420,12 @@ async def on_request_exception(
312420 def _trace_config_ctx_factory (** kwargs ):
313421 kwargs .setdefault ("trace_request_ctx" , {})
314422 return types .SimpleNamespace (
315- tracer = tracer , url_filter = url_filter , ** kwargs
423+ tracer = tracer ,
424+ url_filter = url_filter ,
425+ start_time = start_time ,
426+ duration_histogram_old = duration_histogram_old ,
427+ duration_histogram_new = duration_histogram_new ,
428+ ** kwargs ,
316429 )
317430
318431 trace_config = aiohttp .TraceConfig (
@@ -328,6 +441,7 @@ def _trace_config_ctx_factory(**kwargs):
328441
329442def _instrument (
330443 tracer_provider : TracerProvider = None ,
444+ meter_provider : MeterProvider = None ,
331445 url_filter : _UrlFilterT = None ,
332446 request_hook : _RequestHookT = None ,
333447 response_hook : _ResponseHookT = None ,
@@ -357,6 +471,7 @@ def instrumented_init(wrapped, instance, args, kwargs):
357471 request_hook = request_hook ,
358472 response_hook = response_hook ,
359473 tracer_provider = tracer_provider ,
474+ meter_provider = meter_provider ,
360475 sem_conv_opt_in_mode = sem_conv_opt_in_mode ,
361476 )
362477 trace_config ._is_instrumented_by_opentelemetry = True
@@ -401,6 +516,7 @@ def _instrument(self, **kwargs):
401516 Args:
402517 **kwargs: Optional arguments
403518 ``tracer_provider``: a TracerProvider, defaults to global
519+ ``meter_provider``: a MeterProvider, defaults to global
404520 ``url_filter``: A callback to process the requested URL prior to adding
405521 it as a span attribute. This can be useful to remove sensitive data
406522 such as API keys or user personal information.
@@ -415,6 +531,7 @@ def _instrument(self, **kwargs):
415531 )
416532 _instrument (
417533 tracer_provider = kwargs .get ("tracer_provider" ),
534+ meter_provider = kwargs .get ("meter_provider" ),
418535 url_filter = kwargs .get ("url_filter" ),
419536 request_hook = kwargs .get ("request_hook" ),
420537 response_hook = kwargs .get ("response_hook" ),
0 commit comments