Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-fastapi`: Drop support for FastAPI versions earlier than `0.92`
([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012))

### Added
- `opentelemetry-instrumentation-aiohttp-client` Add support for HTTP metrics
([#3517](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3517))


## Version 1.33.0/0.54b0 (2025-05-09)

### Added
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
| Instrumentation | Supported Packages | Metrics support | Semconv status |
| --------------- | ------------------ | --------------- | -------------- |
| [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No | development
| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No | migration
| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | Yes | migration
| [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | Yes | development
| [opentelemetry-instrumentation-aiokafka](./opentelemetry-instrumentation-aiokafka) | aiokafka >= 0.8, < 1.0 | No | development
| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No | development
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def response_hook(span: Span, params: typing.Union[

import types
import typing
from timeit import default_timer
from typing import Collection

import aiohttp
Expand All @@ -99,10 +100,16 @@ def response_hook(span: Span, params: typing.Union[
from opentelemetry import context as context_api
from opentelemetry import trace
from opentelemetry.instrumentation._semconv import (
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
_client_duration_attrs_new,
_client_duration_attrs_old,
_filter_semconv_duration_attrs,
_get_schema_url,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_report_new,
_report_old,
_set_http_method,
_set_http_url,
_set_status,
Expand All @@ -115,8 +122,13 @@ def response_hook(span: Span, params: typing.Union[
is_instrumentation_enabled,
unwrap,
)
from opentelemetry.metrics import MeterProvider, get_meter
from opentelemetry.propagate import inject
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.metrics.http_metrics import (
HTTP_CLIENT_REQUEST_DURATION,
)
from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.http import remove_url_credentials, sanitize_method
Expand Down Expand Up @@ -172,11 +184,14 @@ def _set_http_status_code_attribute(
)


# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
def create_trace_config(
url_filter: _UrlFilterT = None,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
tracer_provider: TracerProvider = None,
meter_provider: MeterProvider = None,
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
) -> aiohttp.TraceConfig:
"""Create an aiohttp-compatible trace configuration.
Expand Down Expand Up @@ -205,6 +220,7 @@ def create_trace_config(
:param Callable request_hook: Optional callback that can modify span name and request params.
:param Callable response_hook: Optional callback that can modify span name and response params.
:param tracer_provider: optional TracerProvider from which to get a Tracer
:param meter_provider: optional Meter provider to use

:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
:rtype: :py:class:`aiohttp.TraceConfig`
Expand All @@ -214,20 +230,70 @@ def create_trace_config(
# Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work
# around this issue.

schema_url = _get_schema_url(sem_conv_opt_in_mode)

tracer = get_tracer(
__name__,
__version__,
tracer_provider,
schema_url=_get_schema_url(sem_conv_opt_in_mode),
schema_url=schema_url,
)

meter = get_meter(
__name__,
__version__,
meter_provider,
schema_url,
)

# TODO: Use this when we have durations for aiohttp-client
start_time = 0

duration_histogram_old = None
if _report_old(sem_conv_opt_in_mode):
duration_histogram_old = meter.create_histogram(
name=MetricInstruments.HTTP_CLIENT_DURATION,
unit="ms",
description="measures the duration of the outbound HTTP request",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
)
duration_histogram_new = None
if _report_new(sem_conv_opt_in_mode):
duration_histogram_new = meter.create_histogram(
name=HTTP_CLIENT_REQUEST_DURATION,
unit="s",
description="Duration of HTTP client requests.",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
)

metric_attributes = {}

def _end_trace(trace_config_ctx: types.SimpleNamespace):
elapsed_time = max(default_timer() - trace_config_ctx.start_time, 0)
context_api.detach(trace_config_ctx.token)
trace_config_ctx.span.end()

if trace_config_ctx.duration_histogram_old is not None:
duration_attrs_old = _filter_semconv_duration_attrs(
metric_attributes,
_client_duration_attrs_old,
_client_duration_attrs_new,
_StabilityMode.DEFAULT,
)
trace_config_ctx.duration_histogram_old.record(
max(round(elapsed_time * 1000), 0),
attributes=duration_attrs_old,
)
if trace_config_ctx.duration_histogram_new is not None:
duration_attrs_new = _filter_semconv_duration_attrs(
metric_attributes,
_client_duration_attrs_old,
_client_duration_attrs_new,
_StabilityMode.HTTP,
)
trace_config_ctx.duration_histogram_new.record(
elapsed_time, attributes=duration_attrs_new
)

async def on_request_start(
unused_session: aiohttp.ClientSession,
trace_config_ctx: types.SimpleNamespace,
Expand All @@ -237,6 +303,7 @@ async def on_request_start(
trace_config_ctx.span = None
return

trace_config_ctx.start_time = default_timer()
method = params.method
request_span_name = _get_span_name(method)
request_url = (
Expand All @@ -252,6 +319,12 @@ async def on_request_start(
sanitize_method(method),
sem_conv_opt_in_mode,
)
_set_http_method(
metric_attributes,
method,
sanitize_method(method),
sem_conv_opt_in_mode,
)
_set_http_url(span_attributes, request_url, sem_conv_opt_in_mode)

trace_config_ctx.span = trace_config_ctx.tracer.start_span(
Expand Down Expand Up @@ -298,6 +371,7 @@ async def on_request_exception(
exc_type = type(params.exception).__qualname__
if _report_new(sem_conv_opt_in_mode):
trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type)
metric_attributes[ERROR_TYPE] = exc_type

trace_config_ctx.span.set_status(
Status(StatusCode.ERROR, exc_type)
Expand All @@ -312,7 +386,12 @@ async def on_request_exception(
def _trace_config_ctx_factory(**kwargs):
kwargs.setdefault("trace_request_ctx", {})
return types.SimpleNamespace(
tracer=tracer, url_filter=url_filter, **kwargs
tracer=tracer,
url_filter=url_filter,
start_time=start_time,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
**kwargs,
)

trace_config = aiohttp.TraceConfig(
Expand All @@ -328,6 +407,7 @@ def _trace_config_ctx_factory(**kwargs):

def _instrument(
tracer_provider: TracerProvider = None,
meter_provider: MeterProvider = None,
url_filter: _UrlFilterT = None,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
Expand Down Expand Up @@ -357,6 +437,7 @@ def instrumented_init(wrapped, instance, args, kwargs):
request_hook=request_hook,
response_hook=response_hook,
tracer_provider=tracer_provider,
meter_provider=meter_provider,
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
)
trace_config._is_instrumented_by_opentelemetry = True
Expand Down Expand Up @@ -401,6 +482,7 @@ def _instrument(self, **kwargs):
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global
``meter_provider``: a MeterProvider, defaults to global
``url_filter``: A callback to process the requested URL prior to adding
it as a span attribute. This can be useful to remove sensitive data
such as API keys or user personal information.
Expand All @@ -415,6 +497,7 @@ def _instrument(self, **kwargs):
)
_instrument(
tracer_provider=kwargs.get("tracer_provider"),
meter_provider=kwargs.get("meter_provider"),
url_filter=kwargs.get("url_filter"),
request_hook=kwargs.get("request_hook"),
response_hook=kwargs.get("response_hook"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@

_instruments = ("aiohttp ~= 3.0",)

_supports_metrics = False
_supports_metrics = True

_semconv_status = "migration"
Loading