feat(instrumentation-httpx): Add support for HTTP metrics (#3513)
* Add support for metrics * Updated changelog * Update package.py * generate and lint * Update * Update --------- Co-authored-by: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
This commit is contained in:
parent
701d65b022
commit
ccf9cabeee
|
|
@ -60,8 +60,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012))
|
([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `opentelemetry-instrumentation-aiohttp-client` Add support for HTTP metrics
|
- `opentelemetry-instrumentation-aiohttp-client` Add support for HTTP metrics
|
||||||
([#3517](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3517))
|
([#3517](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3517))
|
||||||
|
- `opentelemetry-instrumentation-httpx` Add support for HTTP metrics
|
||||||
|
([#3513](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3513))
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92 | Yes | migration
|
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92 | Yes | migration
|
||||||
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
|
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
|
||||||
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | development
|
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | development
|
||||||
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration
|
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | Yes | migration
|
||||||
| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | development
|
| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | development
|
||||||
| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0, < 3.0,kafka-python-ng >= 2.0, < 3.0 | No | development
|
| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0, < 3.0,kafka-python-ng >= 2.0, < 3.0 | No | development
|
||||||
| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | development
|
| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | development
|
||||||
|
|
|
||||||
|
|
@ -209,22 +209,32 @@ import logging
|
||||||
import typing
|
import typing
|
||||||
from asyncio import iscoroutinefunction
|
from asyncio import iscoroutinefunction
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from timeit import default_timer
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from wrapt import wrap_function_wrapper
|
from wrapt import wrap_function_wrapper
|
||||||
|
|
||||||
from opentelemetry.instrumentation._semconv import (
|
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,
|
_get_schema_url,
|
||||||
_OpenTelemetrySemanticConventionStability,
|
_OpenTelemetrySemanticConventionStability,
|
||||||
_OpenTelemetryStabilitySignalType,
|
_OpenTelemetryStabilitySignalType,
|
||||||
_report_new,
|
_report_new,
|
||||||
|
_report_old,
|
||||||
_set_http_host_client,
|
_set_http_host_client,
|
||||||
_set_http_method,
|
_set_http_method,
|
||||||
|
_set_http_net_peer_name_client,
|
||||||
_set_http_network_protocol_version,
|
_set_http_network_protocol_version,
|
||||||
_set_http_peer_port_client,
|
_set_http_peer_port_client,
|
||||||
|
_set_http_scheme,
|
||||||
_set_http_status_code,
|
_set_http_status_code,
|
||||||
_set_http_url,
|
_set_http_url,
|
||||||
|
_set_status,
|
||||||
_StabilityMode,
|
_StabilityMode,
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.httpx.package import _instruments
|
from opentelemetry.instrumentation.httpx.package import _instruments
|
||||||
|
|
@ -235,12 +245,17 @@ from opentelemetry.instrumentation.utils import (
|
||||||
is_http_instrumentation_enabled,
|
is_http_instrumentation_enabled,
|
||||||
unwrap,
|
unwrap,
|
||||||
)
|
)
|
||||||
|
from opentelemetry.metrics import Histogram, MeterProvider, get_meter
|
||||||
from opentelemetry.propagate import inject
|
from opentelemetry.propagate import inject
|
||||||
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
||||||
from opentelemetry.semconv.attributes.network_attributes import (
|
from opentelemetry.semconv.attributes.network_attributes import (
|
||||||
NETWORK_PEER_ADDRESS,
|
NETWORK_PEER_ADDRESS,
|
||||||
NETWORK_PEER_PORT,
|
NETWORK_PEER_PORT,
|
||||||
)
|
)
|
||||||
|
from opentelemetry.semconv.metrics import MetricInstruments
|
||||||
|
from opentelemetry.semconv.metrics.http_metrics import (
|
||||||
|
HTTP_CLIENT_REQUEST_DURATION,
|
||||||
|
)
|
||||||
from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer
|
from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer
|
||||||
from opentelemetry.trace.span import Span
|
from opentelemetry.trace.span import Span
|
||||||
from opentelemetry.trace.status import StatusCode
|
from opentelemetry.trace.status import StatusCode
|
||||||
|
|
@ -352,6 +367,7 @@ def _extract_response(
|
||||||
|
|
||||||
def _apply_request_client_attributes_to_span(
|
def _apply_request_client_attributes_to_span(
|
||||||
span_attributes: dict[str, typing.Any],
|
span_attributes: dict[str, typing.Any],
|
||||||
|
metric_attributes: dict[str, typing.Any],
|
||||||
url: str | httpx.URL,
|
url: str | httpx.URL,
|
||||||
method_original: str,
|
method_original: str,
|
||||||
semconv: _StabilityMode,
|
semconv: _StabilityMode,
|
||||||
|
|
@ -364,23 +380,44 @@ def _apply_request_client_attributes_to_span(
|
||||||
sanitize_method(method_original),
|
sanitize_method(method_original),
|
||||||
semconv,
|
semconv,
|
||||||
)
|
)
|
||||||
|
|
||||||
# http semconv transition: http.url -> url.full
|
# http semconv transition: http.url -> url.full
|
||||||
_set_http_url(span_attributes, str(url), semconv)
|
_set_http_url(span_attributes, str(url), semconv)
|
||||||
|
|
||||||
|
# Set HTTP method in metric labels
|
||||||
|
_set_http_method(
|
||||||
|
metric_attributes,
|
||||||
|
method_original,
|
||||||
|
sanitize_method(method_original),
|
||||||
|
semconv,
|
||||||
|
)
|
||||||
|
|
||||||
|
if _report_old(semconv):
|
||||||
|
# TODO: Support opt-in for url.scheme in new semconv
|
||||||
|
_set_http_scheme(metric_attributes, url.scheme, semconv)
|
||||||
|
|
||||||
if _report_new(semconv):
|
if _report_new(semconv):
|
||||||
if url.host:
|
if url.host:
|
||||||
# http semconv transition: http.host -> server.address
|
# http semconv transition: http.host -> server.address
|
||||||
_set_http_host_client(span_attributes, url.host, semconv)
|
_set_http_host_client(span_attributes, url.host, semconv)
|
||||||
|
# Add metric labels
|
||||||
|
_set_http_host_client(metric_attributes, url.host, semconv)
|
||||||
|
_set_http_net_peer_name_client(
|
||||||
|
metric_attributes, url.host, semconv
|
||||||
|
)
|
||||||
# http semconv transition: net.sock.peer.addr -> network.peer.address
|
# http semconv transition: net.sock.peer.addr -> network.peer.address
|
||||||
span_attributes[NETWORK_PEER_ADDRESS] = url.host
|
span_attributes[NETWORK_PEER_ADDRESS] = url.host
|
||||||
if url.port:
|
if url.port:
|
||||||
# http semconv transition: net.sock.peer.port -> network.peer.port
|
# http semconv transition: net.sock.peer.port -> network.peer.port
|
||||||
_set_http_peer_port_client(span_attributes, url.port, semconv)
|
_set_http_peer_port_client(span_attributes, url.port, semconv)
|
||||||
span_attributes[NETWORK_PEER_PORT] = url.port
|
span_attributes[NETWORK_PEER_PORT] = url.port
|
||||||
|
# Add metric labels
|
||||||
|
_set_http_peer_port_client(metric_attributes, url.port, semconv)
|
||||||
|
|
||||||
|
|
||||||
def _apply_response_client_attributes_to_span(
|
def _apply_response_client_attributes_to_span(
|
||||||
span: Span,
|
span: Span,
|
||||||
|
metric_attributes: dict[str, typing.Any],
|
||||||
status_code: int,
|
status_code: int,
|
||||||
http_version: str,
|
http_version: str,
|
||||||
semconv: _StabilityMode,
|
semconv: _StabilityMode,
|
||||||
|
|
@ -396,12 +433,28 @@ def _apply_response_client_attributes_to_span(
|
||||||
http_status_code = http_status_to_status_code(status_code)
|
http_status_code = http_status_to_status_code(status_code)
|
||||||
span.set_status(http_status_code)
|
span.set_status(http_status_code)
|
||||||
|
|
||||||
|
# Set HTTP status code in metric attributes
|
||||||
|
_set_status(
|
||||||
|
span,
|
||||||
|
metric_attributes,
|
||||||
|
status_code,
|
||||||
|
str(status_code),
|
||||||
|
server_span=False,
|
||||||
|
sem_conv_opt_in_mode=semconv,
|
||||||
|
)
|
||||||
|
|
||||||
if http_status_code == StatusCode.ERROR and _report_new(semconv):
|
if http_status_code == StatusCode.ERROR and _report_new(semconv):
|
||||||
# http semconv transition: new error.type
|
# http semconv transition: new error.type
|
||||||
span_attributes[ERROR_TYPE] = str(status_code)
|
span_attributes[ERROR_TYPE] = str(status_code)
|
||||||
|
|
||||||
if http_version and _report_new(semconv):
|
if http_version and _report_new(semconv):
|
||||||
# http semconv transition: http.flavor -> network.protocol.version
|
# http semconv transition: http.flavor -> network.protocol.version
|
||||||
|
_set_http_network_protocol_version(
|
||||||
|
metric_attributes,
|
||||||
|
http_version.replace("HTTP/", ""),
|
||||||
|
semconv,
|
||||||
|
)
|
||||||
|
if _report_new(semconv):
|
||||||
_set_http_network_protocol_version(
|
_set_http_network_protocol_version(
|
||||||
span_attributes,
|
span_attributes,
|
||||||
http_version.replace("HTTP/", ""),
|
http_version.replace("HTTP/", ""),
|
||||||
|
|
@ -418,6 +471,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
Args:
|
Args:
|
||||||
transport: SyncHTTPTransport instance to wrap
|
transport: SyncHTTPTransport instance to wrap
|
||||||
tracer_provider: Tracer provider to use
|
tracer_provider: Tracer provider to use
|
||||||
|
meter_provider: Meter provider to use
|
||||||
request_hook: A hook that receives the span and request that is called
|
request_hook: A hook that receives the span and request that is called
|
||||||
right after the span is created
|
right after the span is created
|
||||||
response_hook: A hook that receives the span, request, and response
|
response_hook: A hook that receives the span, request, and response
|
||||||
|
|
@ -428,6 +482,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
self,
|
self,
|
||||||
transport: httpx.BaseTransport,
|
transport: httpx.BaseTransport,
|
||||||
tracer_provider: TracerProvider | None = None,
|
tracer_provider: TracerProvider | None = None,
|
||||||
|
meter_provider: MeterProvider | None = None,
|
||||||
request_hook: RequestHook | None = None,
|
request_hook: RequestHook | None = None,
|
||||||
response_hook: ResponseHook | None = None,
|
response_hook: ResponseHook | None = None,
|
||||||
):
|
):
|
||||||
|
|
@ -435,13 +490,37 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
_OpenTelemetryStabilitySignalType.HTTP,
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
)
|
)
|
||||||
|
schema_url = _get_schema_url(self._sem_conv_opt_in_mode)
|
||||||
|
|
||||||
self._transport = transport
|
self._transport = transport
|
||||||
self._tracer = get_tracer(
|
self._tracer = get_tracer(
|
||||||
__name__,
|
__name__,
|
||||||
instrumenting_library_version=__version__,
|
instrumenting_library_version=__version__,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=tracer_provider,
|
||||||
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
|
schema_url=schema_url,
|
||||||
|
)
|
||||||
|
meter = get_meter(
|
||||||
|
__name__,
|
||||||
|
__version__,
|
||||||
|
meter_provider,
|
||||||
|
schema_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._duration_histogram_old = None
|
||||||
|
if _report_old(self._sem_conv_opt_in_mode):
|
||||||
|
self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_new = None
|
||||||
|
if _report_new(self._sem_conv_opt_in_mode):
|
||||||
|
self._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,
|
||||||
)
|
)
|
||||||
self._request_hook = request_hook
|
self._request_hook = request_hook
|
||||||
self._response_hook = response_hook
|
self._response_hook = response_hook
|
||||||
|
|
@ -477,9 +556,11 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
method_original = method.decode()
|
method_original = method.decode()
|
||||||
span_name = _get_default_span_name(method_original)
|
span_name = _get_default_span_name(method_original)
|
||||||
span_attributes = {}
|
span_attributes = {}
|
||||||
|
metric_attributes = {}
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_request_client_attributes_to_span(
|
_apply_request_client_attributes_to_span(
|
||||||
span_attributes,
|
span_attributes,
|
||||||
|
metric_attributes,
|
||||||
url,
|
url,
|
||||||
method_original,
|
method_original,
|
||||||
self._sem_conv_opt_in_mode,
|
self._sem_conv_opt_in_mode,
|
||||||
|
|
@ -496,11 +577,15 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
|
|
||||||
|
start_time = default_timer()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._transport.handle_request(*args, **kwargs)
|
response = self._transport.handle_request(*args, **kwargs)
|
||||||
except Exception as exc: # pylint: disable=W0703
|
except Exception as exc: # pylint: disable=W0703
|
||||||
exception = exc
|
exception = exc
|
||||||
response = getattr(exc, "response", None)
|
response = getattr(exc, "response", None)
|
||||||
|
finally:
|
||||||
|
elapsed_time = max(default_timer() - start_time, 0)
|
||||||
|
|
||||||
if isinstance(response, (httpx.Response, tuple)):
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
status_code, headers, stream, extensions, http_version = (
|
status_code, headers, stream, extensions, http_version = (
|
||||||
|
|
@ -511,6 +596,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_response_client_attributes_to_span(
|
_apply_response_client_attributes_to_span(
|
||||||
span,
|
span,
|
||||||
|
metric_attributes,
|
||||||
status_code,
|
status_code,
|
||||||
http_version,
|
http_version,
|
||||||
self._sem_conv_opt_in_mode,
|
self._sem_conv_opt_in_mode,
|
||||||
|
|
@ -529,8 +615,33 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
span.set_attribute(
|
span.set_attribute(
|
||||||
ERROR_TYPE, type(exception).__qualname__
|
ERROR_TYPE, type(exception).__qualname__
|
||||||
)
|
)
|
||||||
|
metric_attributes[ERROR_TYPE] = type(
|
||||||
|
exception
|
||||||
|
).__qualname__
|
||||||
raise exception.with_traceback(exception.__traceback__)
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
|
if self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_old.record(
|
||||||
|
max(round(elapsed_time * 1000), 0),
|
||||||
|
attributes=duration_attrs_old,
|
||||||
|
)
|
||||||
|
if self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_new.record(
|
||||||
|
elapsed_time, attributes=duration_attrs_new
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
|
@ -543,6 +654,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
Args:
|
Args:
|
||||||
transport: AsyncHTTPTransport instance to wrap
|
transport: AsyncHTTPTransport instance to wrap
|
||||||
tracer_provider: Tracer provider to use
|
tracer_provider: Tracer provider to use
|
||||||
|
meter_provider: Meter provider to use
|
||||||
request_hook: A hook that receives the span and request that is called
|
request_hook: A hook that receives the span and request that is called
|
||||||
right after the span is created
|
right after the span is created
|
||||||
response_hook: A hook that receives the span, request, and response
|
response_hook: A hook that receives the span, request, and response
|
||||||
|
|
@ -553,6 +665,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
self,
|
self,
|
||||||
transport: httpx.AsyncBaseTransport,
|
transport: httpx.AsyncBaseTransport,
|
||||||
tracer_provider: TracerProvider | None = None,
|
tracer_provider: TracerProvider | None = None,
|
||||||
|
meter_provider: MeterProvider | None = None,
|
||||||
request_hook: AsyncRequestHook | None = None,
|
request_hook: AsyncRequestHook | None = None,
|
||||||
response_hook: AsyncResponseHook | None = None,
|
response_hook: AsyncResponseHook | None = None,
|
||||||
):
|
):
|
||||||
|
|
@ -560,14 +673,40 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
_OpenTelemetryStabilitySignalType.HTTP,
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
)
|
)
|
||||||
|
schema_url = _get_schema_url(self._sem_conv_opt_in_mode)
|
||||||
|
|
||||||
self._transport = transport
|
self._transport = transport
|
||||||
self._tracer = get_tracer(
|
self._tracer = get_tracer(
|
||||||
__name__,
|
__name__,
|
||||||
instrumenting_library_version=__version__,
|
instrumenting_library_version=__version__,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=tracer_provider,
|
||||||
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
|
schema_url=schema_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
meter = get_meter(
|
||||||
|
__name__,
|
||||||
|
__version__,
|
||||||
|
meter_provider,
|
||||||
|
schema_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._duration_histogram_old = None
|
||||||
|
if _report_old(self._sem_conv_opt_in_mode):
|
||||||
|
self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_new = None
|
||||||
|
if _report_new(self._sem_conv_opt_in_mode):
|
||||||
|
self._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,
|
||||||
|
)
|
||||||
|
|
||||||
self._request_hook = request_hook
|
self._request_hook = request_hook
|
||||||
self._response_hook = response_hook
|
self._response_hook = response_hook
|
||||||
|
|
||||||
|
|
@ -600,9 +739,11 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
method_original = method.decode()
|
method_original = method.decode()
|
||||||
span_name = _get_default_span_name(method_original)
|
span_name = _get_default_span_name(method_original)
|
||||||
span_attributes = {}
|
span_attributes = {}
|
||||||
|
metric_attributes = {}
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_request_client_attributes_to_span(
|
_apply_request_client_attributes_to_span(
|
||||||
span_attributes,
|
span_attributes,
|
||||||
|
metric_attributes,
|
||||||
url,
|
url,
|
||||||
method_original,
|
method_original,
|
||||||
self._sem_conv_opt_in_mode,
|
self._sem_conv_opt_in_mode,
|
||||||
|
|
@ -619,6 +760,8 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
|
|
||||||
|
start_time = default_timer()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._transport.handle_async_request(
|
response = await self._transport.handle_async_request(
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
|
|
@ -626,6 +769,8 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
except Exception as exc: # pylint: disable=W0703
|
except Exception as exc: # pylint: disable=W0703
|
||||||
exception = exc
|
exception = exc
|
||||||
response = getattr(exc, "response", None)
|
response = getattr(exc, "response", None)
|
||||||
|
finally:
|
||||||
|
elapsed_time = max(default_timer() - start_time, 0)
|
||||||
|
|
||||||
if isinstance(response, (httpx.Response, tuple)):
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
status_code, headers, stream, extensions, http_version = (
|
status_code, headers, stream, extensions, http_version = (
|
||||||
|
|
@ -636,6 +781,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_response_client_attributes_to_span(
|
_apply_response_client_attributes_to_span(
|
||||||
span,
|
span,
|
||||||
|
metric_attributes,
|
||||||
status_code,
|
status_code,
|
||||||
http_version,
|
http_version,
|
||||||
self._sem_conv_opt_in_mode,
|
self._sem_conv_opt_in_mode,
|
||||||
|
|
@ -655,8 +801,34 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
||||||
span.set_attribute(
|
span.set_attribute(
|
||||||
ERROR_TYPE, type(exception).__qualname__
|
ERROR_TYPE, type(exception).__qualname__
|
||||||
)
|
)
|
||||||
|
metric_attributes[ERROR_TYPE] = type(
|
||||||
|
exception
|
||||||
|
).__qualname__
|
||||||
|
|
||||||
raise exception.with_traceback(exception.__traceback__)
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
|
if self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_old.record(
|
||||||
|
max(round(elapsed_time * 1000), 0),
|
||||||
|
attributes=duration_attrs_old,
|
||||||
|
)
|
||||||
|
if self._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,
|
||||||
|
)
|
||||||
|
self._duration_histogram_new.record(
|
||||||
|
elapsed_time, attributes=duration_attrs_new
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def aclose(self) -> None:
|
async def aclose(self) -> None:
|
||||||
|
|
@ -679,6 +851,7 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Optional arguments
|
**kwargs: Optional arguments
|
||||||
``tracer_provider``: a TracerProvider, defaults to global
|
``tracer_provider``: a TracerProvider, defaults to global
|
||||||
|
``meter_provider``: a MeterProvider, defaults to global
|
||||||
``request_hook``: A ``httpx.Client`` hook that receives the span and request
|
``request_hook``: A ``httpx.Client`` hook that receives the span and request
|
||||||
that is called right after the span is created
|
that is called right after the span is created
|
||||||
``response_hook``: A ``httpx.Client`` hook that receives the span, request,
|
``response_hook``: A ``httpx.Client`` hook that receives the span, request,
|
||||||
|
|
@ -687,6 +860,7 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
|
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
|
||||||
"""
|
"""
|
||||||
tracer_provider = kwargs.get("tracer_provider")
|
tracer_provider = kwargs.get("tracer_provider")
|
||||||
|
meter_provider = kwargs.get("meter_provider")
|
||||||
request_hook = kwargs.get("request_hook")
|
request_hook = kwargs.get("request_hook")
|
||||||
response_hook = kwargs.get("response_hook")
|
response_hook = kwargs.get("response_hook")
|
||||||
async_request_hook = kwargs.get("async_request_hook")
|
async_request_hook = kwargs.get("async_request_hook")
|
||||||
|
|
@ -706,11 +880,36 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
_OpenTelemetryStabilitySignalType.HTTP,
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
)
|
)
|
||||||
|
schema_url = _get_schema_url(sem_conv_opt_in_mode)
|
||||||
|
|
||||||
tracer = get_tracer(
|
tracer = get_tracer(
|
||||||
__name__,
|
__name__,
|
||||||
instrumenting_library_version=__version__,
|
instrumenting_library_version=__version__,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=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,
|
||||||
|
)
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrap_function_wrapper(
|
wrap_function_wrapper(
|
||||||
|
|
@ -719,6 +918,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
self._handle_request_wrapper,
|
self._handle_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
request_hook=request_hook,
|
request_hook=request_hook,
|
||||||
response_hook=response_hook,
|
response_hook=response_hook,
|
||||||
|
|
@ -730,6 +931,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
self._handle_async_request_wrapper,
|
self._handle_async_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
async_request_hook=async_request_hook,
|
async_request_hook=async_request_hook,
|
||||||
async_response_hook=async_response_hook,
|
async_response_hook=async_response_hook,
|
||||||
|
|
@ -747,6 +950,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
args: tuple[typing.Any, ...],
|
args: tuple[typing.Any, ...],
|
||||||
kwargs: dict[str, typing.Any],
|
kwargs: dict[str, typing.Any],
|
||||||
tracer: Tracer,
|
tracer: Tracer,
|
||||||
|
duration_histogram_old: Histogram,
|
||||||
|
duration_histogram_new: Histogram,
|
||||||
sem_conv_opt_in_mode: _StabilityMode,
|
sem_conv_opt_in_mode: _StabilityMode,
|
||||||
request_hook: RequestHook,
|
request_hook: RequestHook,
|
||||||
response_hook: ResponseHook,
|
response_hook: ResponseHook,
|
||||||
|
|
@ -760,9 +965,11 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
method_original = method.decode()
|
method_original = method.decode()
|
||||||
span_name = _get_default_span_name(method_original)
|
span_name = _get_default_span_name(method_original)
|
||||||
span_attributes = {}
|
span_attributes = {}
|
||||||
|
metric_attributes = {}
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_request_client_attributes_to_span(
|
_apply_request_client_attributes_to_span(
|
||||||
span_attributes,
|
span_attributes,
|
||||||
|
metric_attributes,
|
||||||
url,
|
url,
|
||||||
method_original,
|
method_original,
|
||||||
sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode,
|
||||||
|
|
@ -779,11 +986,15 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
|
|
||||||
|
start_time = default_timer()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = wrapped(*args, **kwargs)
|
response = wrapped(*args, **kwargs)
|
||||||
except Exception as exc: # pylint: disable=W0703
|
except Exception as exc: # pylint: disable=W0703
|
||||||
exception = exc
|
exception = exc
|
||||||
response = getattr(exc, "response", None)
|
response = getattr(exc, "response", None)
|
||||||
|
finally:
|
||||||
|
elapsed_time = max(default_timer() - start_time, 0)
|
||||||
|
|
||||||
if isinstance(response, (httpx.Response, tuple)):
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
status_code, headers, stream, extensions, http_version = (
|
status_code, headers, stream, extensions, http_version = (
|
||||||
|
|
@ -794,6 +1005,7 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_response_client_attributes_to_span(
|
_apply_response_client_attributes_to_span(
|
||||||
span,
|
span,
|
||||||
|
metric_attributes,
|
||||||
status_code,
|
status_code,
|
||||||
http_version,
|
http_version,
|
||||||
sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode,
|
||||||
|
|
@ -810,8 +1022,33 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
span.set_attribute(
|
span.set_attribute(
|
||||||
ERROR_TYPE, type(exception).__qualname__
|
ERROR_TYPE, type(exception).__qualname__
|
||||||
)
|
)
|
||||||
|
metric_attributes[ERROR_TYPE] = type(
|
||||||
|
exception
|
||||||
|
).__qualname__
|
||||||
raise exception.with_traceback(exception.__traceback__)
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
|
if 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,
|
||||||
|
)
|
||||||
|
duration_histogram_old.record(
|
||||||
|
max(round(elapsed_time * 1000), 0),
|
||||||
|
attributes=duration_attrs_old,
|
||||||
|
)
|
||||||
|
if 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,
|
||||||
|
)
|
||||||
|
duration_histogram_new.record(
|
||||||
|
elapsed_time, attributes=duration_attrs_new
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -821,6 +1058,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
args: tuple[typing.Any, ...],
|
args: tuple[typing.Any, ...],
|
||||||
kwargs: dict[str, typing.Any],
|
kwargs: dict[str, typing.Any],
|
||||||
tracer: Tracer,
|
tracer: Tracer,
|
||||||
|
duration_histogram_old: Histogram,
|
||||||
|
duration_histogram_new: Histogram,
|
||||||
sem_conv_opt_in_mode: _StabilityMode,
|
sem_conv_opt_in_mode: _StabilityMode,
|
||||||
async_request_hook: AsyncRequestHook,
|
async_request_hook: AsyncRequestHook,
|
||||||
async_response_hook: AsyncResponseHook,
|
async_response_hook: AsyncResponseHook,
|
||||||
|
|
@ -834,9 +1073,11 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
method_original = method.decode()
|
method_original = method.decode()
|
||||||
span_name = _get_default_span_name(method_original)
|
span_name = _get_default_span_name(method_original)
|
||||||
span_attributes = {}
|
span_attributes = {}
|
||||||
|
metric_attributes = {}
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_request_client_attributes_to_span(
|
_apply_request_client_attributes_to_span(
|
||||||
span_attributes,
|
span_attributes,
|
||||||
|
metric_attributes,
|
||||||
url,
|
url,
|
||||||
method_original,
|
method_original,
|
||||||
sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode,
|
||||||
|
|
@ -853,11 +1094,15 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
|
|
||||||
|
start_time = default_timer()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await wrapped(*args, **kwargs)
|
response = await wrapped(*args, **kwargs)
|
||||||
except Exception as exc: # pylint: disable=W0703
|
except Exception as exc: # pylint: disable=W0703
|
||||||
exception = exc
|
exception = exc
|
||||||
response = getattr(exc, "response", None)
|
response = getattr(exc, "response", None)
|
||||||
|
finally:
|
||||||
|
elapsed_time = max(default_timer() - start_time, 0)
|
||||||
|
|
||||||
if isinstance(response, (httpx.Response, tuple)):
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
status_code, headers, stream, extensions, http_version = (
|
status_code, headers, stream, extensions, http_version = (
|
||||||
|
|
@ -868,6 +1113,7 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
# apply http client response attributes according to semconv
|
# apply http client response attributes according to semconv
|
||||||
_apply_response_client_attributes_to_span(
|
_apply_response_client_attributes_to_span(
|
||||||
span,
|
span,
|
||||||
|
metric_attributes,
|
||||||
status_code,
|
status_code,
|
||||||
http_version,
|
http_version,
|
||||||
sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode,
|
||||||
|
|
@ -887,13 +1133,37 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
)
|
)
|
||||||
raise exception.with_traceback(exception.__traceback__)
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
|
if 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,
|
||||||
|
)
|
||||||
|
duration_histogram_old.record(
|
||||||
|
max(round(elapsed_time * 1000), 0),
|
||||||
|
attributes=duration_attrs_old,
|
||||||
|
)
|
||||||
|
if 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,
|
||||||
|
)
|
||||||
|
duration_histogram_new.record(
|
||||||
|
elapsed_time, attributes=duration_attrs_new
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
@classmethod
|
@classmethod
|
||||||
def instrument_client(
|
def instrument_client(
|
||||||
cls,
|
cls,
|
||||||
client: httpx.Client | httpx.AsyncClient,
|
client: httpx.Client | httpx.AsyncClient,
|
||||||
tracer_provider: TracerProvider | None = None,
|
tracer_provider: TracerProvider | None = None,
|
||||||
|
meter_provider: MeterProvider | None = None,
|
||||||
request_hook: RequestHook | AsyncRequestHook | None = None,
|
request_hook: RequestHook | AsyncRequestHook | None = None,
|
||||||
response_hook: ResponseHook | AsyncResponseHook | None = None,
|
response_hook: ResponseHook | AsyncResponseHook | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -902,6 +1172,7 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
Args:
|
Args:
|
||||||
client: The httpx Client or AsyncClient instance
|
client: The httpx Client or AsyncClient instance
|
||||||
tracer_provider: A TracerProvider, defaults to global
|
tracer_provider: A TracerProvider, defaults to global
|
||||||
|
meter_provider: A MeterProvider, defaults to global
|
||||||
request_hook: A hook that receives the span and request that is called
|
request_hook: A hook that receives the span and request that is called
|
||||||
right after the span is created
|
right after the span is created
|
||||||
response_hook: A hook that receives the span, request, and response
|
response_hook: A hook that receives the span, request, and response
|
||||||
|
|
@ -918,11 +1189,34 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
_OpenTelemetryStabilitySignalType.HTTP,
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
)
|
)
|
||||||
|
schema_url = _get_schema_url(sem_conv_opt_in_mode)
|
||||||
tracer = get_tracer(
|
tracer = get_tracer(
|
||||||
__name__,
|
__name__,
|
||||||
instrumenting_library_version=__version__,
|
instrumenting_library_version=__version__,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=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,
|
||||||
|
)
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
if iscoroutinefunction(request_hook):
|
if iscoroutinefunction(request_hook):
|
||||||
|
|
@ -946,6 +1240,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
cls._handle_request_wrapper,
|
cls._handle_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
request_hook=request_hook,
|
request_hook=request_hook,
|
||||||
response_hook=response_hook,
|
response_hook=response_hook,
|
||||||
|
|
@ -959,6 +1255,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
cls._handle_request_wrapper,
|
cls._handle_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
request_hook=request_hook,
|
request_hook=request_hook,
|
||||||
response_hook=response_hook,
|
response_hook=response_hook,
|
||||||
|
|
@ -972,6 +1270,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
cls._handle_async_request_wrapper,
|
cls._handle_async_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
async_request_hook=async_request_hook,
|
async_request_hook=async_request_hook,
|
||||||
async_response_hook=async_response_hook,
|
async_response_hook=async_response_hook,
|
||||||
|
|
@ -985,6 +1285,8 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
|
||||||
partial(
|
partial(
|
||||||
cls._handle_async_request_wrapper,
|
cls._handle_async_request_wrapper,
|
||||||
tracer=tracer,
|
tracer=tracer,
|
||||||
|
duration_histogram_old=duration_histogram_old,
|
||||||
|
duration_histogram_new=duration_histogram_new,
|
||||||
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
|
||||||
async_request_hook=async_request_hook,
|
async_request_hook=async_request_hook,
|
||||||
async_response_hook=async_response_hook,
|
async_response_hook=async_response_hook,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
|
|
||||||
_instruments = ("httpx >= 0.18.0",)
|
_instruments = ("httpx >= 0.18.0",)
|
||||||
|
|
||||||
_supports_metrics = False
|
_supports_metrics = True
|
||||||
|
|
||||||
_semconv_status = "migration"
|
_semconv_status = "migration"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ from wrapt import ObjectProxy
|
||||||
import opentelemetry.instrumentation.httpx
|
import opentelemetry.instrumentation.httpx
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.instrumentation._semconv import (
|
from opentelemetry.instrumentation._semconv import (
|
||||||
|
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
|
||||||
|
HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
|
||||||
OTEL_SEMCONV_STABILITY_OPT_IN,
|
OTEL_SEMCONV_STABILITY_OPT_IN,
|
||||||
_OpenTelemetrySemanticConventionStability,
|
_OpenTelemetrySemanticConventionStability,
|
||||||
)
|
)
|
||||||
|
|
@ -67,6 +69,7 @@ if typing.TYPE_CHECKING:
|
||||||
ResponseHook,
|
ResponseHook,
|
||||||
ResponseInfo,
|
ResponseInfo,
|
||||||
)
|
)
|
||||||
|
from opentelemetry.metrics import MeterProvider
|
||||||
from opentelemetry.sdk.trace.export import SpanExporter
|
from opentelemetry.sdk.trace.export import SpanExporter
|
||||||
from opentelemetry.trace import TracerProvider
|
from opentelemetry.trace import TracerProvider
|
||||||
from opentelemetry.trace.span import Span
|
from opentelemetry.trace.span import Span
|
||||||
|
|
@ -187,6 +190,11 @@ class BaseTestCases:
|
||||||
return span_list[0]
|
return span_list[0]
|
||||||
return span_list
|
return span_list
|
||||||
|
|
||||||
|
def assert_metrics(self, num_metrics: int = 1):
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), num_metrics)
|
||||||
|
return metrics
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def perform_request(
|
def perform_request(
|
||||||
self,
|
self,
|
||||||
|
|
@ -220,6 +228,29 @@ class BaseTestCases:
|
||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_basic_metrics(self):
|
||||||
|
self.perform_request(self.URL)
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(duration_data_point.count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(duration_data_point.attributes),
|
||||||
|
{
|
||||||
|
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||||
|
SpanAttributes.HTTP_METHOD: "GET",
|
||||||
|
SpanAttributes.HTTP_SCHEME: "http",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(duration_data_point.count, 1)
|
||||||
|
self.assertTrue(duration_data_point.min >= 0)
|
||||||
|
self.assertTrue(duration_data_point.max >= 0)
|
||||||
|
self.assertTrue(duration_data_point.sum >= 0)
|
||||||
|
self.assertEqual(
|
||||||
|
duration_data_point.explicit_bounds,
|
||||||
|
HTTP_DURATION_HISTOGRAM_BUCKETS_OLD,
|
||||||
|
)
|
||||||
|
|
||||||
def test_nonstandard_http_method(self):
|
def test_nonstandard_http_method(self):
|
||||||
respx.route(method="NONSTANDARD").mock(
|
respx.route(method="NONSTANDARD").mock(
|
||||||
return_value=httpx.Response(405)
|
return_value=httpx.Response(405)
|
||||||
|
|
@ -243,6 +274,19 @@ class BaseTestCases:
|
||||||
self.assertEqualSpanInstrumentationScope(
|
self.assertEqualSpanInstrumentationScope(
|
||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(duration_data_point.count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(duration_data_point.attributes),
|
||||||
|
{
|
||||||
|
SpanAttributes.HTTP_STATUS_CODE: 405,
|
||||||
|
SpanAttributes.HTTP_METHOD: "_OTHER",
|
||||||
|
SpanAttributes.HTTP_SCHEME: "http",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_nonstandard_http_method_new_semconv(self):
|
def test_nonstandard_http_method_new_semconv(self):
|
||||||
respx.route(method="NONSTANDARD").mock(
|
respx.route(method="NONSTANDARD").mock(
|
||||||
|
|
@ -272,6 +316,25 @@ class BaseTestCases:
|
||||||
self.assertEqualSpanInstrumentationScope(
|
self.assertEqualSpanInstrumentationScope(
|
||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(duration_data_point.count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(duration_data_point.attributes),
|
||||||
|
{
|
||||||
|
HTTP_REQUEST_METHOD: "_OTHER",
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
HTTP_RESPONSE_STATUS_CODE: 405,
|
||||||
|
NETWORK_PROTOCOL_VERSION: "1.1",
|
||||||
|
ERROR_TYPE: "405",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration_data_point.explicit_bounds,
|
||||||
|
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
|
||||||
|
)
|
||||||
|
|
||||||
def test_basic_new_semconv(self):
|
def test_basic_new_semconv(self):
|
||||||
url = "http://mock:8080/status/200"
|
url = "http://mock:8080/status/200"
|
||||||
|
|
@ -313,6 +376,22 @@ class BaseTestCases:
|
||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(duration_data_point.count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(duration_data_point.attributes),
|
||||||
|
{
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
HTTP_RESPONSE_STATUS_CODE: 200,
|
||||||
|
NETWORK_PROTOCOL_VERSION: "1.1",
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_basic_both_semconv(self):
|
def test_basic_both_semconv(self):
|
||||||
url = "http://mock:8080/status/200" # 8080 because httpx returns None for common ports (http, https, wss)
|
url = "http://mock:8080/status/200" # 8080 because httpx returns None for common ports (http, https, wss)
|
||||||
respx.get(url).mock(httpx.Response(200, text="Hello!"))
|
respx.get(url).mock(httpx.Response(200, text="Hello!"))
|
||||||
|
|
@ -354,6 +433,36 @@ class BaseTestCases:
|
||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 2)
|
||||||
|
# Old convention
|
||||||
|
self.assertEqual(
|
||||||
|
dict(metrics[0].data.data_points[0].attributes),
|
||||||
|
{
|
||||||
|
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||||
|
SpanAttributes.HTTP_HOST: "mock",
|
||||||
|
SpanAttributes.HTTP_METHOD: "GET",
|
||||||
|
SpanAttributes.HTTP_SCHEME: "http",
|
||||||
|
SpanAttributes.NET_PEER_NAME: "mock",
|
||||||
|
SpanAttributes.NET_PEER_PORT: 8080,
|
||||||
|
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(metrics[0].name, "http.client.duration")
|
||||||
|
# New convention
|
||||||
|
self.assertEqual(
|
||||||
|
dict(metrics[1].data.data_points[0].attributes),
|
||||||
|
{
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
HTTP_RESPONSE_STATUS_CODE: 200,
|
||||||
|
NETWORK_PROTOCOL_VERSION: "1.1",
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(metrics[1].name, "http.client.request.duration")
|
||||||
|
|
||||||
def test_basic_multiple(self):
|
def test_basic_multiple(self):
|
||||||
self.perform_request(self.URL)
|
self.perform_request(self.URL)
|
||||||
self.perform_request(self.URL)
|
self.perform_request(self.URL)
|
||||||
|
|
@ -375,6 +484,16 @@ class BaseTestCases:
|
||||||
span.status.status_code,
|
span.status.status_code,
|
||||||
trace.StatusCode.ERROR,
|
trace.StatusCode.ERROR,
|
||||||
)
|
)
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(
|
||||||
|
duration_data_point.attributes.get(
|
||||||
|
SpanAttributes.HTTP_STATUS_CODE
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
def test_not_foundbasic_new_semconv(self):
|
def test_not_foundbasic_new_semconv(self):
|
||||||
url_404 = "http://mock/status/404"
|
url_404 = "http://mock/status/404"
|
||||||
|
|
@ -395,6 +514,17 @@ class BaseTestCases:
|
||||||
span.status.status_code,
|
span.status.status_code,
|
||||||
trace.StatusCode.ERROR,
|
trace.StatusCode.ERROR,
|
||||||
)
|
)
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 1)
|
||||||
|
duration_data_point = metrics[0].data.data_points[0]
|
||||||
|
self.assertEqual(
|
||||||
|
duration_data_point.attributes.get(HTTP_RESPONSE_STATUS_CODE),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
duration_data_point.attributes.get(ERROR_TYPE), "404"
|
||||||
|
)
|
||||||
|
|
||||||
def test_not_foundbasic_both_semconv(self):
|
def test_not_foundbasic_both_semconv(self):
|
||||||
url_404 = "http://mock/status/404"
|
url_404 = "http://mock/status/404"
|
||||||
|
|
@ -417,6 +547,30 @@ class BaseTestCases:
|
||||||
span.status.status_code,
|
span.status.status_code,
|
||||||
trace.StatusCode.ERROR,
|
trace.StatusCode.ERROR,
|
||||||
)
|
)
|
||||||
|
# Validate metrics
|
||||||
|
metrics = self.get_sorted_metrics()
|
||||||
|
self.assertEqual(len(metrics), 2)
|
||||||
|
# Old convention
|
||||||
|
self.assertEqual(
|
||||||
|
metrics[0]
|
||||||
|
.data.data_points[0]
|
||||||
|
.attributes.get(SpanAttributes.HTTP_STATUS_CODE),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
metrics[0].data.data_points[0].attributes.get(ERROR_TYPE), None
|
||||||
|
)
|
||||||
|
# New convention
|
||||||
|
self.assertEqual(
|
||||||
|
metrics[1]
|
||||||
|
.data.data_points[0]
|
||||||
|
.attributes.get(HTTP_RESPONSE_STATUS_CODE),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
metrics[1].data.data_points[0].attributes.get(ERROR_TYPE),
|
||||||
|
"404",
|
||||||
|
)
|
||||||
|
|
||||||
def test_suppress_instrumentation(self):
|
def test_suppress_instrumentation(self):
|
||||||
with suppress_http_instrumentation():
|
with suppress_http_instrumentation():
|
||||||
|
|
@ -584,6 +738,7 @@ class BaseTestCases:
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self,
|
self,
|
||||||
tracer_provider: typing.Optional["TracerProvider"] = None,
|
tracer_provider: typing.Optional["TracerProvider"] = None,
|
||||||
|
meter_provider: typing.Optional["MeterProvider"] = None,
|
||||||
request_hook: typing.Optional["RequestHook"] = None,
|
request_hook: typing.Optional["RequestHook"] = None,
|
||||||
response_hook: typing.Optional["ResponseHook"] = None,
|
response_hook: typing.Optional["ResponseHook"] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
@ -623,6 +778,18 @@ class BaseTestCases:
|
||||||
span = self.assert_span(exporter=exporter)
|
span = self.assert_span(exporter=exporter)
|
||||||
self.assertIs(span.resource, resource)
|
self.assertIs(span.resource, resource)
|
||||||
|
|
||||||
|
def test_custom_meter_provider(self):
|
||||||
|
meter_provider, memory_reader = self.create_meter_provider()
|
||||||
|
transport = self.create_transport(meter_provider=meter_provider)
|
||||||
|
client = self.create_client(transport)
|
||||||
|
self.perform_request(self.URL, client=client)
|
||||||
|
metrics = memory_reader.get_metrics_data().resource_metrics[0]
|
||||||
|
self.assertEqual(len(metrics.scope_metrics), 1)
|
||||||
|
data_point = (
|
||||||
|
metrics.scope_metrics[0].metrics[0].data.data_points[0]
|
||||||
|
)
|
||||||
|
self.assertEqual(data_point.count, 1)
|
||||||
|
|
||||||
def test_response_hook(self):
|
def test_response_hook(self):
|
||||||
transport = self.create_transport(
|
transport = self.create_transport(
|
||||||
tracer_provider=self.tracer_provider,
|
tracer_provider=self.tracer_provider,
|
||||||
|
|
@ -1096,6 +1263,7 @@ class TestSyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self,
|
self,
|
||||||
tracer_provider: typing.Optional["TracerProvider"] = None,
|
tracer_provider: typing.Optional["TracerProvider"] = None,
|
||||||
|
meter_provider: typing.Optional["MeterProvider"] = None,
|
||||||
request_hook: typing.Optional["RequestHook"] = None,
|
request_hook: typing.Optional["RequestHook"] = None,
|
||||||
response_hook: typing.Optional["ResponseHook"] = None,
|
response_hook: typing.Optional["ResponseHook"] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
@ -1104,6 +1272,7 @@ class TestSyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
telemetry_transport = SyncOpenTelemetryTransport(
|
telemetry_transport = SyncOpenTelemetryTransport(
|
||||||
transport,
|
transport,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=tracer_provider,
|
||||||
|
meter_provider=meter_provider,
|
||||||
request_hook=request_hook,
|
request_hook=request_hook,
|
||||||
response_hook=response_hook,
|
response_hook=response_hook,
|
||||||
)
|
)
|
||||||
|
|
@ -1127,6 +1296,11 @@ class TestSyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
return self.client.request(method, url, headers=headers)
|
return self.client.request(method, url, headers=headers)
|
||||||
return client.request(method, url, headers=headers)
|
return client.request(method, url, headers=headers)
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
self.perform_request(self.URL)
|
||||||
|
self.assert_span(num_spans=1)
|
||||||
|
self.assert_metrics(num_metrics=1)
|
||||||
|
|
||||||
def test_credential_removal(self):
|
def test_credential_removal(self):
|
||||||
new_url = "http://username:password@mock/status/200"
|
new_url = "http://username:password@mock/status/200"
|
||||||
self.perform_request(new_url)
|
self.perform_request(new_url)
|
||||||
|
|
@ -1148,6 +1322,7 @@ class TestAsyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self,
|
self,
|
||||||
tracer_provider: typing.Optional["TracerProvider"] = None,
|
tracer_provider: typing.Optional["TracerProvider"] = None,
|
||||||
|
meter_provider: typing.Optional["MeterProvider"] = None,
|
||||||
request_hook: typing.Optional["AsyncRequestHook"] = None,
|
request_hook: typing.Optional["AsyncRequestHook"] = None,
|
||||||
response_hook: typing.Optional["AsyncResponseHook"] = None,
|
response_hook: typing.Optional["AsyncResponseHook"] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
@ -1156,6 +1331,7 @@ class TestAsyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
telemetry_transport = AsyncOpenTelemetryTransport(
|
telemetry_transport = AsyncOpenTelemetryTransport(
|
||||||
transport,
|
transport,
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=tracer_provider,
|
||||||
|
meter_provider=meter_provider,
|
||||||
request_hook=request_hook,
|
request_hook=request_hook,
|
||||||
response_hook=response_hook,
|
response_hook=response_hook,
|
||||||
)
|
)
|
||||||
|
|
@ -1195,6 +1371,7 @@ class TestAsyncIntegration(BaseTestCases.BaseManualTest):
|
||||||
self.URL, client=self.create_client(self.transport)
|
self.URL, client=self.create_client(self.transport)
|
||||||
)
|
)
|
||||||
self.assert_span(num_spans=2)
|
self.assert_span(num_spans=2)
|
||||||
|
self.assert_metrics(num_metrics=1)
|
||||||
|
|
||||||
def test_credential_removal(self):
|
def test_credential_removal(self):
|
||||||
new_url = "http://username:password@mock/status/200"
|
new_url = "http://username:password@mock/status/200"
|
||||||
|
|
@ -1291,6 +1468,7 @@ class TestAsyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest):
|
||||||
self.perform_request(self.URL, client=self.client)
|
self.perform_request(self.URL, client=self.client)
|
||||||
self.perform_request(self.URL, client=self.client2)
|
self.perform_request(self.URL, client=self.client2)
|
||||||
self.assert_span(num_spans=2)
|
self.assert_span(num_spans=2)
|
||||||
|
self.assert_metrics(num_metrics=1)
|
||||||
|
|
||||||
def test_async_response_hook_does_nothing_if_not_coroutine(self):
|
def test_async_response_hook_does_nothing_if_not_coroutine(self):
|
||||||
HTTPXClientInstrumentor().instrument(
|
HTTPXClientInstrumentor().instrument(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue