@@ -90,7 +90,9 @@ def response_hook(span: Span, params: typing.Union[
90
90
91
91
import types
92
92
import typing
93
+ from timeit import default_timer
93
94
from typing import Collection
95
+ from urllib .parse import urlparse
94
96
95
97
import aiohttp
96
98
import wrapt
@@ -99,11 +101,20 @@ def response_hook(span: Span, params: typing.Union[
99
101
from opentelemetry import context as context_api
100
102
from opentelemetry import trace
101
103
from 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 ,
102
109
_get_schema_url ,
103
110
_OpenTelemetrySemanticConventionStability ,
104
111
_OpenTelemetryStabilitySignalType ,
105
112
_report_new ,
113
+ _report_old ,
114
+ _set_http_host_client ,
106
115
_set_http_method ,
116
+ _set_http_net_peer_name_client ,
117
+ _set_http_peer_port_client ,
107
118
_set_http_url ,
108
119
_set_status ,
109
120
_StabilityMode ,
@@ -115,8 +126,13 @@ def response_hook(span: Span, params: typing.Union[
115
126
is_instrumentation_enabled ,
116
127
unwrap ,
117
128
)
129
+ from opentelemetry .metrics import MeterProvider , get_meter
118
130
from opentelemetry .propagate import inject
119
131
from 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
+ )
120
136
from opentelemetry .trace import Span , SpanKind , TracerProvider , get_tracer
121
137
from opentelemetry .trace .status import Status , StatusCode
122
138
from opentelemetry .util .http import remove_url_credentials , sanitize_method
@@ -172,11 +188,14 @@ def _set_http_status_code_attribute(
172
188
)
173
189
174
190
191
+ # pylint: disable=too-many-locals
192
+ # pylint: disable=too-many-statements
175
193
def create_trace_config (
176
194
url_filter : _UrlFilterT = None ,
177
195
request_hook : _RequestHookT = None ,
178
196
response_hook : _ResponseHookT = None ,
179
197
tracer_provider : TracerProvider = None ,
198
+ meter_provider : MeterProvider = None ,
180
199
sem_conv_opt_in_mode : _StabilityMode = _StabilityMode .DEFAULT ,
181
200
) -> aiohttp .TraceConfig :
182
201
"""Create an aiohttp-compatible trace configuration.
@@ -205,6 +224,7 @@ def create_trace_config(
205
224
:param Callable request_hook: Optional callback that can modify span name and request params.
206
225
:param Callable response_hook: Optional callback that can modify span name and response params.
207
226
:param tracer_provider: optional TracerProvider from which to get a Tracer
227
+ :param meter_provider: optional Meter provider to use
208
228
209
229
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
210
230
:rtype: :py:class:`aiohttp.TraceConfig`
@@ -214,20 +234,70 @@ def create_trace_config(
214
234
# Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work
215
235
# around this issue.
216
236
237
+ schema_url = _get_schema_url (sem_conv_opt_in_mode )
238
+
217
239
tracer = get_tracer (
218
240
__name__ ,
219
241
__version__ ,
220
242
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 ,
222
251
)
223
252
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
+
225
272
metric_attributes = {}
226
273
227
274
def _end_trace (trace_config_ctx : types .SimpleNamespace ):
275
+ elapsed_time = max (default_timer () - trace_config_ctx .start_time , 0 )
228
276
context_api .detach (trace_config_ctx .token )
229
277
trace_config_ctx .span .end ()
230
278
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
+
231
301
async def on_request_start (
232
302
unused_session : aiohttp .ClientSession ,
233
303
trace_config_ctx : types .SimpleNamespace ,
@@ -237,6 +307,7 @@ async def on_request_start(
237
307
trace_config_ctx .span = None
238
308
return
239
309
310
+ trace_config_ctx .start_time = default_timer ()
240
311
method = params .method
241
312
request_span_name = _get_span_name (method )
242
313
request_url = (
@@ -252,8 +323,44 @@ async def on_request_start(
252
323
sanitize_method (method ),
253
324
sem_conv_opt_in_mode ,
254
325
)
326
+ _set_http_method (
327
+ metric_attributes ,
328
+ method ,
329
+ sanitize_method (method ),
330
+ sem_conv_opt_in_mode ,
331
+ )
255
332
_set_http_url (span_attributes , request_url , sem_conv_opt_in_mode )
256
333
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
+
257
364
trace_config_ctx .span = trace_config_ctx .tracer .start_span (
258
365
request_span_name , kind = SpanKind .CLIENT , attributes = span_attributes
259
366
)
@@ -298,6 +405,7 @@ async def on_request_exception(
298
405
exc_type = type (params .exception ).__qualname__
299
406
if _report_new (sem_conv_opt_in_mode ):
300
407
trace_config_ctx .span .set_attribute (ERROR_TYPE , exc_type )
408
+ metric_attributes [ERROR_TYPE ] = exc_type
301
409
302
410
trace_config_ctx .span .set_status (
303
411
Status (StatusCode .ERROR , exc_type )
@@ -312,7 +420,12 @@ async def on_request_exception(
312
420
def _trace_config_ctx_factory (** kwargs ):
313
421
kwargs .setdefault ("trace_request_ctx" , {})
314
422
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 ,
316
429
)
317
430
318
431
trace_config = aiohttp .TraceConfig (
@@ -328,6 +441,7 @@ def _trace_config_ctx_factory(**kwargs):
328
441
329
442
def _instrument (
330
443
tracer_provider : TracerProvider = None ,
444
+ meter_provider : MeterProvider = None ,
331
445
url_filter : _UrlFilterT = None ,
332
446
request_hook : _RequestHookT = None ,
333
447
response_hook : _ResponseHookT = None ,
@@ -357,6 +471,7 @@ def instrumented_init(wrapped, instance, args, kwargs):
357
471
request_hook = request_hook ,
358
472
response_hook = response_hook ,
359
473
tracer_provider = tracer_provider ,
474
+ meter_provider = meter_provider ,
360
475
sem_conv_opt_in_mode = sem_conv_opt_in_mode ,
361
476
)
362
477
trace_config ._is_instrumented_by_opentelemetry = True
@@ -401,6 +516,7 @@ def _instrument(self, **kwargs):
401
516
Args:
402
517
**kwargs: Optional arguments
403
518
``tracer_provider``: a TracerProvider, defaults to global
519
+ ``meter_provider``: a MeterProvider, defaults to global
404
520
``url_filter``: A callback to process the requested URL prior to adding
405
521
it as a span attribute. This can be useful to remove sensitive data
406
522
such as API keys or user personal information.
@@ -415,6 +531,7 @@ def _instrument(self, **kwargs):
415
531
)
416
532
_instrument (
417
533
tracer_provider = kwargs .get ("tracer_provider" ),
534
+ meter_provider = kwargs .get ("meter_provider" ),
418
535
url_filter = kwargs .get ("url_filter" ),
419
536
request_hook = kwargs .get ("request_hook" ),
420
537
response_hook = kwargs .get ("response_hook" ),
0 commit comments