mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-28 12:43:39 +08:00
HTTP semantic convention stability migration for httpx (#2631)
This commit is contained in:
@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
([#2616](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2616))
|
([#2616](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2616))
|
||||||
- `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge
|
- `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge
|
||||||
([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638))
|
([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638))
|
||||||
|
- `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions
|
||||||
|
([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631))
|
||||||
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
|
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
|
||||||
([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630))
|
([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630))
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental
|
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental
|
||||||
| [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.27 | No | experimental
|
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental
|
||||||
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | experimental
|
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration
|
||||||
| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | experimental
|
| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | experimental
|
||||||
| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No | experimental
|
| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No | experimental
|
||||||
| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | experimental
|
| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | experimental
|
||||||
|
@ -196,6 +196,19 @@ from types import TracebackType
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation._semconv import (
|
||||||
|
_get_schema_url,
|
||||||
|
_HTTPStabilityMode,
|
||||||
|
_OpenTelemetrySemanticConventionStability,
|
||||||
|
_OpenTelemetryStabilitySignalType,
|
||||||
|
_report_new,
|
||||||
|
_set_http_host,
|
||||||
|
_set_http_method,
|
||||||
|
_set_http_network_protocol_version,
|
||||||
|
_set_http_peer_port_client,
|
||||||
|
_set_http_status_code,
|
||||||
|
_set_http_url,
|
||||||
|
)
|
||||||
from opentelemetry.instrumentation.httpx.package import _instruments
|
from opentelemetry.instrumentation.httpx.package import _instruments
|
||||||
from opentelemetry.instrumentation.httpx.version import __version__
|
from opentelemetry.instrumentation.httpx.version import __version__
|
||||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
@ -204,11 +217,15 @@ from opentelemetry.instrumentation.utils import (
|
|||||||
is_http_instrumentation_enabled,
|
is_http_instrumentation_enabled,
|
||||||
)
|
)
|
||||||
from opentelemetry.propagate import inject
|
from opentelemetry.propagate import inject
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
||||||
|
from opentelemetry.semconv.attributes.network_attributes import (
|
||||||
|
NETWORK_PEER_ADDRESS,
|
||||||
|
NETWORK_PEER_PORT,
|
||||||
|
)
|
||||||
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
|
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
|
||||||
from opentelemetry.trace.span import Span
|
from opentelemetry.trace.span import Span
|
||||||
from opentelemetry.trace.status import Status
|
from opentelemetry.trace.status import StatusCode
|
||||||
from opentelemetry.util.http import remove_url_credentials
|
from opentelemetry.util.http import remove_url_credentials, sanitize_method
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -242,25 +259,11 @@ class ResponseInfo(typing.NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
def _get_default_span_name(method: str) -> str:
|
def _get_default_span_name(method: str) -> str:
|
||||||
return method.strip()
|
method = sanitize_method(method.upper().strip())
|
||||||
|
if method == "_OTHER":
|
||||||
|
method = "HTTP"
|
||||||
|
|
||||||
|
return method
|
||||||
def _apply_status_code(span: Span, status_code: int) -> None:
|
|
||||||
if not span.is_recording():
|
|
||||||
return
|
|
||||||
|
|
||||||
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
|
|
||||||
span.set_status(Status(http_status_to_status_code(status_code)))
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_attributes(method: bytes, url: URL) -> typing.Dict[str, str]:
|
|
||||||
_method = method.decode().upper()
|
|
||||||
_url = str(httpx.URL(url))
|
|
||||||
span_attributes = {
|
|
||||||
SpanAttributes.HTTP_METHOD: _method,
|
|
||||||
SpanAttributes.HTTP_URL: _url,
|
|
||||||
}
|
|
||||||
return span_attributes
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_headers(headers: typing.Optional[Headers]) -> httpx.Headers:
|
def _prepare_headers(headers: typing.Optional[Headers]) -> httpx.Headers:
|
||||||
@ -299,6 +302,84 @@ def _inject_propagation_headers(headers, args, kwargs):
|
|||||||
kwargs["headers"] = _headers.raw
|
kwargs["headers"] = _headers.raw
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_response(
|
||||||
|
response: typing.Union[
|
||||||
|
httpx.Response, typing.Tuple[int, Headers, httpx.SyncByteStream, dict]
|
||||||
|
]
|
||||||
|
) -> typing.Tuple[int, Headers, httpx.SyncByteStream, dict, str]:
|
||||||
|
if isinstance(response, httpx.Response):
|
||||||
|
status_code = response.status_code
|
||||||
|
headers = response.headers
|
||||||
|
stream = response.stream
|
||||||
|
extensions = response.extensions
|
||||||
|
http_version = response.http_version
|
||||||
|
else:
|
||||||
|
status_code, headers, stream, extensions = response
|
||||||
|
http_version = extensions.get("http_version", b"HTTP/1.1").decode(
|
||||||
|
"ascii", errors="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (status_code, headers, stream, extensions, http_version)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_request_client_attributes_to_span(
|
||||||
|
span_attributes: dict,
|
||||||
|
url: typing.Union[str, URL, httpx.URL],
|
||||||
|
method_original: str,
|
||||||
|
span_name: str,
|
||||||
|
semconv: _HTTPStabilityMode,
|
||||||
|
):
|
||||||
|
url = httpx.URL(url)
|
||||||
|
# http semconv transition: http.method -> http.request.method
|
||||||
|
_set_http_method(span_attributes, method_original, span_name, semconv)
|
||||||
|
# http semconv transition: http.url -> url.full
|
||||||
|
_set_http_url(span_attributes, str(url), semconv)
|
||||||
|
|
||||||
|
if _report_new(semconv):
|
||||||
|
if url.host:
|
||||||
|
# http semconv transition: http.host -> server.address
|
||||||
|
_set_http_host(span_attributes, url.host, semconv)
|
||||||
|
# http semconv transition: net.sock.peer.addr -> network.peer.address
|
||||||
|
span_attributes[NETWORK_PEER_ADDRESS] = url.host
|
||||||
|
if url.port:
|
||||||
|
# http semconv transition: net.sock.peer.port -> network.peer.port
|
||||||
|
_set_http_peer_port_client(span_attributes, url.port, semconv)
|
||||||
|
span_attributes[NETWORK_PEER_PORT] = url.port
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_response_client_attributes_to_span(
|
||||||
|
span: Span,
|
||||||
|
status_code: int,
|
||||||
|
http_version: str,
|
||||||
|
semconv: _HTTPStabilityMode,
|
||||||
|
):
|
||||||
|
# http semconv transition: http.status_code -> http.response.status_code
|
||||||
|
# TODO: use _set_status when it's stable for http clients
|
||||||
|
span_attributes = {}
|
||||||
|
_set_http_status_code(
|
||||||
|
span_attributes,
|
||||||
|
status_code,
|
||||||
|
semconv,
|
||||||
|
)
|
||||||
|
http_status_code = http_status_to_status_code(status_code)
|
||||||
|
span.set_status(http_status_code)
|
||||||
|
|
||||||
|
if http_status_code == StatusCode.ERROR and _report_new(semconv):
|
||||||
|
# http semconv transition: new error.type
|
||||||
|
span_attributes[ERROR_TYPE] = str(status_code)
|
||||||
|
|
||||||
|
if http_version and _report_new(semconv):
|
||||||
|
# http semconv transition: http.flavor -> network.protocol.version
|
||||||
|
_set_http_network_protocol_version(
|
||||||
|
span_attributes,
|
||||||
|
http_version.replace("HTTP/", ""),
|
||||||
|
semconv,
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, val in span_attributes.items():
|
||||||
|
span.set_attribute(key, val)
|
||||||
|
|
||||||
|
|
||||||
class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
||||||
"""Sync transport class that will trace all requests made with a client.
|
"""Sync transport class that will trace all requests made with a client.
|
||||||
|
|
||||||
@ -318,12 +399,17 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
|||||||
request_hook: typing.Optional[RequestHook] = None,
|
request_hook: typing.Optional[RequestHook] = None,
|
||||||
response_hook: typing.Optional[ResponseHook] = None,
|
response_hook: typing.Optional[ResponseHook] = None,
|
||||||
):
|
):
|
||||||
|
_OpenTelemetrySemanticConventionStability._initialize()
|
||||||
|
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
|
)
|
||||||
|
|
||||||
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="https://opentelemetry.io/schemas/1.11.0",
|
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
|
||||||
)
|
)
|
||||||
self._request_hook = request_hook
|
self._request_hook = request_hook
|
||||||
self._response_hook = response_hook
|
self._response_hook = response_hook
|
||||||
@ -340,6 +426,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self._transport.__exit__(exc_type, exc_value, traceback)
|
self._transport.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
# pylint: disable=R0914
|
||||||
def handle_request(
|
def handle_request(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
@ -355,39 +442,64 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
|
|||||||
method, url, headers, stream, extensions = _extract_parameters(
|
method, url, headers, stream, extensions = _extract_parameters(
|
||||||
args, kwargs
|
args, kwargs
|
||||||
)
|
)
|
||||||
span_attributes = _prepare_attributes(method, url)
|
method_original = method.decode()
|
||||||
|
span_name = _get_default_span_name(method_original)
|
||||||
|
span_attributes = {}
|
||||||
|
# apply http client response attributes according to semconv
|
||||||
|
_apply_request_client_attributes_to_span(
|
||||||
|
span_attributes,
|
||||||
|
url,
|
||||||
|
method_original,
|
||||||
|
span_name,
|
||||||
|
self._sem_conv_opt_in_mode,
|
||||||
|
)
|
||||||
|
|
||||||
request_info = RequestInfo(method, url, headers, stream, extensions)
|
request_info = RequestInfo(method, url, headers, stream, extensions)
|
||||||
span_name = _get_default_span_name(
|
|
||||||
span_attributes[SpanAttributes.HTTP_METHOD]
|
|
||||||
)
|
|
||||||
|
|
||||||
with self._tracer.start_as_current_span(
|
with self._tracer.start_as_current_span(
|
||||||
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
|
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
|
||||||
) as span:
|
) as span:
|
||||||
if self._request_hook is not None:
|
exception = None
|
||||||
|
if callable(self._request_hook):
|
||||||
self._request_hook(span, request_info)
|
self._request_hook(span, request_info)
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
response = self._transport.handle_request(*args, **kwargs)
|
|
||||||
if isinstance(response, httpx.Response):
|
|
||||||
response: httpx.Response = response
|
|
||||||
status_code = response.status_code
|
|
||||||
headers = response.headers
|
|
||||||
stream = response.stream
|
|
||||||
extensions = response.extensions
|
|
||||||
else:
|
|
||||||
status_code, headers, stream, extensions = response
|
|
||||||
|
|
||||||
_apply_status_code(span, status_code)
|
try:
|
||||||
|
response = self._transport.handle_request(*args, **kwargs)
|
||||||
|
except Exception as exc: # pylint: disable=W0703
|
||||||
|
exception = exc
|
||||||
|
response = getattr(exc, "response", None)
|
||||||
|
|
||||||
if self._response_hook is not None:
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
self._response_hook(
|
status_code, headers, stream, extensions, http_version = (
|
||||||
span,
|
_extract_response(response)
|
||||||
request_info,
|
|
||||||
ResponseInfo(status_code, headers, stream, extensions),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if span.is_recording():
|
||||||
|
# apply http client response attributes according to semconv
|
||||||
|
_apply_response_client_attributes_to_span(
|
||||||
|
span,
|
||||||
|
status_code,
|
||||||
|
http_version,
|
||||||
|
self._sem_conv_opt_in_mode,
|
||||||
|
)
|
||||||
|
if callable(self._response_hook):
|
||||||
|
self._response_hook(
|
||||||
|
span,
|
||||||
|
request_info,
|
||||||
|
ResponseInfo(status_code, headers, stream, extensions),
|
||||||
|
)
|
||||||
|
|
||||||
|
if exception:
|
||||||
|
if span.is_recording() and _report_new(
|
||||||
|
self._sem_conv_opt_in_mode
|
||||||
|
):
|
||||||
|
span.set_attribute(
|
||||||
|
ERROR_TYPE, type(exception).__qualname__
|
||||||
|
)
|
||||||
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@ -413,12 +525,17 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
|||||||
request_hook: typing.Optional[AsyncRequestHook] = None,
|
request_hook: typing.Optional[AsyncRequestHook] = None,
|
||||||
response_hook: typing.Optional[AsyncResponseHook] = None,
|
response_hook: typing.Optional[AsyncResponseHook] = None,
|
||||||
):
|
):
|
||||||
|
_OpenTelemetrySemanticConventionStability._initialize()
|
||||||
|
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||||
|
_OpenTelemetryStabilitySignalType.HTTP,
|
||||||
|
)
|
||||||
|
|
||||||
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="https://opentelemetry.io/schemas/1.11.0",
|
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
|
||||||
)
|
)
|
||||||
self._request_hook = request_hook
|
self._request_hook = request_hook
|
||||||
self._response_hook = response_hook
|
self._response_hook = response_hook
|
||||||
@ -435,6 +552,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
|||||||
) -> None:
|
) -> None:
|
||||||
await self._transport.__aexit__(exc_type, exc_value, traceback)
|
await self._transport.__aexit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
# pylint: disable=R0914
|
||||||
async def handle_async_request(self, *args, **kwargs) -> typing.Union[
|
async def handle_async_request(self, *args, **kwargs) -> typing.Union[
|
||||||
typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict],
|
typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict],
|
||||||
httpx.Response,
|
httpx.Response,
|
||||||
@ -446,41 +564,66 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
|
|||||||
method, url, headers, stream, extensions = _extract_parameters(
|
method, url, headers, stream, extensions = _extract_parameters(
|
||||||
args, kwargs
|
args, kwargs
|
||||||
)
|
)
|
||||||
span_attributes = _prepare_attributes(method, url)
|
method_original = method.decode()
|
||||||
|
span_name = _get_default_span_name(method_original)
|
||||||
span_name = _get_default_span_name(
|
span_attributes = {}
|
||||||
span_attributes[SpanAttributes.HTTP_METHOD]
|
# apply http client response attributes according to semconv
|
||||||
|
_apply_request_client_attributes_to_span(
|
||||||
|
span_attributes,
|
||||||
|
url,
|
||||||
|
method_original,
|
||||||
|
span_name,
|
||||||
|
self._sem_conv_opt_in_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
request_info = RequestInfo(method, url, headers, stream, extensions)
|
request_info = RequestInfo(method, url, headers, stream, extensions)
|
||||||
|
|
||||||
with self._tracer.start_as_current_span(
|
with self._tracer.start_as_current_span(
|
||||||
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
|
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
|
||||||
) as span:
|
) as span:
|
||||||
if self._request_hook is not None:
|
exception = None
|
||||||
|
if callable(self._request_hook):
|
||||||
await self._request_hook(span, request_info)
|
await self._request_hook(span, request_info)
|
||||||
|
|
||||||
_inject_propagation_headers(headers, args, kwargs)
|
_inject_propagation_headers(headers, args, kwargs)
|
||||||
|
|
||||||
response = await self._transport.handle_async_request(
|
try:
|
||||||
*args, **kwargs
|
response = await self._transport.handle_async_request(
|
||||||
)
|
*args, **kwargs
|
||||||
if isinstance(response, httpx.Response):
|
|
||||||
response: httpx.Response = response
|
|
||||||
status_code = response.status_code
|
|
||||||
headers = response.headers
|
|
||||||
stream = response.stream
|
|
||||||
extensions = response.extensions
|
|
||||||
else:
|
|
||||||
status_code, headers, stream, extensions = response
|
|
||||||
|
|
||||||
_apply_status_code(span, status_code)
|
|
||||||
|
|
||||||
if self._response_hook is not None:
|
|
||||||
await self._response_hook(
|
|
||||||
span,
|
|
||||||
request_info,
|
|
||||||
ResponseInfo(status_code, headers, stream, extensions),
|
|
||||||
)
|
)
|
||||||
|
except Exception as exc: # pylint: disable=W0703
|
||||||
|
exception = exc
|
||||||
|
response = getattr(exc, "response", None)
|
||||||
|
|
||||||
|
if isinstance(response, (httpx.Response, tuple)):
|
||||||
|
status_code, headers, stream, extensions, http_version = (
|
||||||
|
_extract_response(response)
|
||||||
|
)
|
||||||
|
|
||||||
|
if span.is_recording():
|
||||||
|
# apply http client response attributes according to semconv
|
||||||
|
_apply_response_client_attributes_to_span(
|
||||||
|
span,
|
||||||
|
status_code,
|
||||||
|
http_version,
|
||||||
|
self._sem_conv_opt_in_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
if callable(self._response_hook):
|
||||||
|
await self._response_hook(
|
||||||
|
span,
|
||||||
|
request_info,
|
||||||
|
ResponseInfo(status_code, headers, stream, extensions),
|
||||||
|
)
|
||||||
|
|
||||||
|
if exception:
|
||||||
|
if span.is_recording() and _report_new(
|
||||||
|
self._sem_conv_opt_in_mode
|
||||||
|
):
|
||||||
|
span.set_attribute(
|
||||||
|
ERROR_TYPE, type(exception).__qualname__
|
||||||
|
)
|
||||||
|
raise exception.with_traceback(exception.__traceback__)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -14,3 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
_instruments = ("httpx >= 0.18.0",)
|
_instruments = ("httpx >= 0.18.0",)
|
||||||
|
|
||||||
|
_supports_metrics = False
|
||||||
|
|
||||||
|
_semconv_status = "migration"
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
import typing
|
||||||
@ -22,6 +24,10 @@ import respx
|
|||||||
|
|
||||||
import opentelemetry.instrumentation.httpx
|
import opentelemetry.instrumentation.httpx
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.instrumentation._semconv import (
|
||||||
|
OTEL_SEMCONV_STABILITY_OPT_IN,
|
||||||
|
_OpenTelemetrySemanticConventionStability,
|
||||||
|
)
|
||||||
from opentelemetry.instrumentation.httpx import (
|
from opentelemetry.instrumentation.httpx import (
|
||||||
AsyncOpenTelemetryTransport,
|
AsyncOpenTelemetryTransport,
|
||||||
HTTPXClientInstrumentor,
|
HTTPXClientInstrumentor,
|
||||||
@ -30,6 +36,21 @@ from opentelemetry.instrumentation.httpx import (
|
|||||||
from opentelemetry.instrumentation.utils import suppress_http_instrumentation
|
from opentelemetry.instrumentation.utils import suppress_http_instrumentation
|
||||||
from opentelemetry.propagate import get_global_textmap, set_global_textmap
|
from opentelemetry.propagate import get_global_textmap, set_global_textmap
|
||||||
from opentelemetry.sdk import resources
|
from opentelemetry.sdk import resources
|
||||||
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
||||||
|
from opentelemetry.semconv.attributes.http_attributes import (
|
||||||
|
HTTP_REQUEST_METHOD,
|
||||||
|
HTTP_RESPONSE_STATUS_CODE,
|
||||||
|
)
|
||||||
|
from opentelemetry.semconv.attributes.network_attributes import (
|
||||||
|
NETWORK_PEER_ADDRESS,
|
||||||
|
NETWORK_PEER_PORT,
|
||||||
|
NETWORK_PROTOCOL_VERSION,
|
||||||
|
)
|
||||||
|
from opentelemetry.semconv.attributes.server_attributes import (
|
||||||
|
SERVER_ADDRESS,
|
||||||
|
SERVER_PORT,
|
||||||
|
)
|
||||||
|
from opentelemetry.semconv.attributes.url_attributes import URL_FULL
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.test.mock_textmap import MockTextMapPropagator
|
from opentelemetry.test.mock_textmap import MockTextMapPropagator
|
||||||
from opentelemetry.test.test_base import TestBase
|
from opentelemetry.test.test_base import TestBase
|
||||||
@ -100,6 +121,9 @@ async def _async_no_update_request_hook(span: "Span", request: "RequestInfo"):
|
|||||||
return 123
|
return 123
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
|
|
||||||
# Using this wrapper class to have a base class for the tests while also not
|
# Using this wrapper class to have a base class for the tests while also not
|
||||||
# angering pylint or mypy when calling methods not in the class when only
|
# angering pylint or mypy when calling methods not in the class when only
|
||||||
# subclassing abc.ABC.
|
# subclassing abc.ABC.
|
||||||
@ -112,15 +136,39 @@ class BaseTestCases:
|
|||||||
request_hook = staticmethod(_request_hook)
|
request_hook = staticmethod(_request_hook)
|
||||||
no_update_request_hook = staticmethod(_no_update_request_hook)
|
no_update_request_hook = staticmethod(_no_update_request_hook)
|
||||||
|
|
||||||
|
# TODO: make this more explicit to tests
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
test_name = ""
|
||||||
|
if hasattr(self, "_testMethodName"):
|
||||||
|
test_name = self._testMethodName
|
||||||
|
sem_conv_mode = "default"
|
||||||
|
if "new_semconv" in test_name:
|
||||||
|
sem_conv_mode = "http"
|
||||||
|
elif "both_semconv" in test_name:
|
||||||
|
sem_conv_mode = "http/dup"
|
||||||
|
self.env_patch = mock.patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.env_patch.start()
|
||||||
|
_OpenTelemetrySemanticConventionStability._initialized = False
|
||||||
respx.start()
|
respx.start()
|
||||||
respx.get(self.URL).mock(httpx.Response(200, text="Hello!"))
|
respx.get(self.URL).mock(
|
||||||
|
httpx.Response(
|
||||||
|
200,
|
||||||
|
text="Hello!",
|
||||||
|
extensions={"http_version": b"HTTP/1.1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
self.env_patch.stop()
|
||||||
respx.stop()
|
respx.stop()
|
||||||
|
|
||||||
def assert_span(
|
def assert_span(
|
||||||
@ -169,6 +217,87 @@ class BaseTestCases:
|
|||||||
span, opentelemetry.instrumentation.httpx
|
span, opentelemetry.instrumentation.httpx
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_basic_new_semconv(self):
|
||||||
|
url = "http://mock:8080/status/200"
|
||||||
|
respx.get(url).mock(
|
||||||
|
httpx.Response(
|
||||||
|
200,
|
||||||
|
text="Hello!",
|
||||||
|
extensions={"http_version": b"HTTP/1.1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = self.perform_request(url)
|
||||||
|
self.assertEqual(result.text, "Hello!")
|
||||||
|
span = self.assert_span()
|
||||||
|
|
||||||
|
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||||
|
self.assertEqual(span.name, "GET")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
span.instrumentation_scope.schema_url,
|
||||||
|
SpanAttributes.SCHEMA_URL,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes,
|
||||||
|
{
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
URL_FULL: url,
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
NETWORK_PEER_ADDRESS: "mock",
|
||||||
|
HTTP_RESPONSE_STATUS_CODE: 200,
|
||||||
|
NETWORK_PROTOCOL_VERSION: "1.1",
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
NETWORK_PEER_PORT: 8080,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
|
||||||
|
|
||||||
|
self.assertEqualSpanInstrumentationInfo(
|
||||||
|
span, opentelemetry.instrumentation.httpx
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_basic_both_semconv(self):
|
||||||
|
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!"))
|
||||||
|
result = self.perform_request(url)
|
||||||
|
self.assertEqual(result.text, "Hello!")
|
||||||
|
span = self.assert_span()
|
||||||
|
|
||||||
|
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||||
|
self.assertEqual(span.name, "GET")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
span.instrumentation_scope.schema_url,
|
||||||
|
SpanAttributes.SCHEMA_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes,
|
||||||
|
{
|
||||||
|
SpanAttributes.HTTP_METHOD: "GET",
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
SpanAttributes.HTTP_URL: url,
|
||||||
|
URL_FULL: url,
|
||||||
|
SpanAttributes.HTTP_HOST: "mock",
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
NETWORK_PEER_ADDRESS: "mock",
|
||||||
|
SpanAttributes.NET_PEER_PORT: 8080,
|
||||||
|
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||||
|
HTTP_RESPONSE_STATUS_CODE: 200,
|
||||||
|
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||||
|
NETWORK_PROTOCOL_VERSION: "1.1",
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
NETWORK_PEER_PORT: 8080,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
|
||||||
|
|
||||||
|
self.assertEqualSpanInstrumentationInfo(
|
||||||
|
span, opentelemetry.instrumentation.httpx
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
@ -191,6 +320,48 @@ class BaseTestCases:
|
|||||||
trace.StatusCode.ERROR,
|
trace.StatusCode.ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_not_foundbasic_new_semconv(self):
|
||||||
|
url_404 = "http://mock/status/404"
|
||||||
|
|
||||||
|
with respx.mock:
|
||||||
|
respx.get(url_404).mock(httpx.Response(404))
|
||||||
|
result = self.perform_request(url_404)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 404)
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404
|
||||||
|
)
|
||||||
|
# new in semconv
|
||||||
|
self.assertEqual(span.attributes.get(ERROR_TYPE), "404")
|
||||||
|
|
||||||
|
self.assertIs(
|
||||||
|
span.status.status_code,
|
||||||
|
trace.StatusCode.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_not_foundbasic_both_semconv(self):
|
||||||
|
url_404 = "http://mock/status/404"
|
||||||
|
|
||||||
|
with respx.mock:
|
||||||
|
respx.get(url_404).mock(httpx.Response(404))
|
||||||
|
result = self.perform_request(url_404)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 404)
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 404
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404
|
||||||
|
)
|
||||||
|
self.assertEqual(span.attributes.get(ERROR_TYPE), "404")
|
||||||
|
|
||||||
|
self.assertIs(
|
||||||
|
span.status.status_code,
|
||||||
|
trace.StatusCode.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def test_suppress_instrumentation(self):
|
def test_suppress_instrumentation(self):
|
||||||
with suppress_http_instrumentation():
|
with suppress_http_instrumentation():
|
||||||
result = self.perform_request(self.URL)
|
result = self.perform_request(self.URL)
|
||||||
@ -245,6 +416,83 @@ class BaseTestCases:
|
|||||||
|
|
||||||
span = self.assert_span()
|
span = self.assert_span()
|
||||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||||
|
self.assertIn("Exception", span.status.description)
|
||||||
|
self.assertEqual(
|
||||||
|
span.events[0].attributes["exception.type"], "Exception"
|
||||||
|
)
|
||||||
|
self.assertIsNone(span.attributes.get(ERROR_TYPE))
|
||||||
|
|
||||||
|
def test_requests_basic_exception_new_semconv(self):
|
||||||
|
with respx.mock, self.assertRaises(Exception):
|
||||||
|
respx.get(self.URL).mock(side_effect=Exception)
|
||||||
|
self.perform_request(self.URL)
|
||||||
|
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||||
|
self.assertIn("Exception", span.status.description)
|
||||||
|
self.assertEqual(
|
||||||
|
span.events[0].attributes["exception.type"], "Exception"
|
||||||
|
)
|
||||||
|
self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception")
|
||||||
|
|
||||||
|
def test_requests_basic_exception_both_semconv(self):
|
||||||
|
with respx.mock, self.assertRaises(Exception):
|
||||||
|
respx.get(self.URL).mock(side_effect=Exception)
|
||||||
|
self.perform_request(self.URL)
|
||||||
|
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||||
|
self.assertIn("Exception", span.status.description)
|
||||||
|
self.assertEqual(
|
||||||
|
span.events[0].attributes["exception.type"], "Exception"
|
||||||
|
)
|
||||||
|
self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception")
|
||||||
|
|
||||||
|
def test_requests_timeout_exception_new_semconv(self):
|
||||||
|
url = "http://mock:8080/exception"
|
||||||
|
with respx.mock, self.assertRaises(httpx.TimeoutException):
|
||||||
|
respx.get(url).mock(side_effect=httpx.TimeoutException)
|
||||||
|
self.perform_request(url)
|
||||||
|
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes,
|
||||||
|
{
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
URL_FULL: url,
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
NETWORK_PEER_PORT: 8080,
|
||||||
|
NETWORK_PEER_ADDRESS: "mock",
|
||||||
|
ERROR_TYPE: "TimeoutException",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||||
|
|
||||||
|
def test_requests_timeout_exception_both_semconv(self):
|
||||||
|
url = "http://mock:8080/exception"
|
||||||
|
with respx.mock, self.assertRaises(httpx.TimeoutException):
|
||||||
|
respx.get(url).mock(side_effect=httpx.TimeoutException)
|
||||||
|
self.perform_request(url)
|
||||||
|
|
||||||
|
span = self.assert_span()
|
||||||
|
self.assertEqual(
|
||||||
|
span.attributes,
|
||||||
|
{
|
||||||
|
SpanAttributes.HTTP_METHOD: "GET",
|
||||||
|
HTTP_REQUEST_METHOD: "GET",
|
||||||
|
SpanAttributes.HTTP_URL: url,
|
||||||
|
URL_FULL: url,
|
||||||
|
SpanAttributes.HTTP_HOST: "mock",
|
||||||
|
SERVER_ADDRESS: "mock",
|
||||||
|
NETWORK_PEER_ADDRESS: "mock",
|
||||||
|
SpanAttributes.NET_PEER_PORT: 8080,
|
||||||
|
SERVER_PORT: 8080,
|
||||||
|
NETWORK_PEER_PORT: 8080,
|
||||||
|
ERROR_TYPE: "TimeoutException",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||||
|
|
||||||
def test_requests_timeout_exception(self):
|
def test_requests_timeout_exception(self):
|
||||||
with respx.mock, self.assertRaises(httpx.TimeoutException):
|
with respx.mock, self.assertRaises(httpx.TimeoutException):
|
||||||
@ -373,6 +621,28 @@ class BaseTestCases:
|
|||||||
self.assertFalse(mock_span.set_attribute.called)
|
self.assertFalse(mock_span.set_attribute.called)
|
||||||
self.assertFalse(mock_span.set_status.called)
|
self.assertFalse(mock_span.set_status.called)
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_not_recording_not_set_attribute_in_exception_new_semconv(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
respx.get(self.URL).mock(side_effect=httpx.TimeoutException)
|
||||||
|
with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span:
|
||||||
|
transport = self.create_transport(
|
||||||
|
tracer_provider=trace.NoOpTracerProvider()
|
||||||
|
)
|
||||||
|
client = self.create_client(transport)
|
||||||
|
mock_span.is_recording.return_value = False
|
||||||
|
try:
|
||||||
|
self.perform_request(self.URL, client=client)
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assert_span(None, 0)
|
||||||
|
self.assertFalse(mock_span.is_recording())
|
||||||
|
self.assertTrue(mock_span.is_recording.called)
|
||||||
|
self.assertFalse(mock_span.set_attribute.called)
|
||||||
|
self.assertFalse(mock_span.set_status.called)
|
||||||
|
|
||||||
class BaseInstrumentorTest(BaseTest, metaclass=abc.ABCMeta):
|
class BaseInstrumentorTest(BaseTest, metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_client(
|
def create_client(
|
||||||
|
Reference in New Issue
Block a user