HTTP semantic convention stability migration for httpx (#2631)
This commit is contained in:
		
							parent
							
								
									c9bad6269c
								
							
						
					
					
						commit
						7da7f554ea
					
				|  | @ -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)) | ||||
| - `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge | ||||
|   ([#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+. | ||||
|   ([#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-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration | ||||
| | [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-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No | experimental | ||||
| | [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | experimental | ||||
|  |  | |||
|  | @ -196,6 +196,19 @@ from types import TracebackType | |||
| 
 | ||||
| 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.version import __version__ | ||||
| from opentelemetry.instrumentation.instrumentor import BaseInstrumentor | ||||
|  | @ -204,11 +217,15 @@ from opentelemetry.instrumentation.utils import ( | |||
|     is_http_instrumentation_enabled, | ||||
| ) | ||||
| 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.span import Span | ||||
| from opentelemetry.trace.status import Status | ||||
| from opentelemetry.util.http import remove_url_credentials | ||||
| from opentelemetry.trace.status import StatusCode | ||||
| from opentelemetry.util.http import remove_url_credentials, sanitize_method | ||||
| 
 | ||||
| _logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -242,25 +259,11 @@ class ResponseInfo(typing.NamedTuple): | |||
| 
 | ||||
| 
 | ||||
| def _get_default_span_name(method: str) -> str: | ||||
|     return method.strip() | ||||
|     method = sanitize_method(method.upper().strip()) | ||||
|     if method == "_OTHER": | ||||
|         method = "HTTP" | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|     return method | ||||
| 
 | ||||
| 
 | ||||
| def _prepare_headers(headers: typing.Optional[Headers]) -> httpx.Headers: | ||||
|  | @ -299,6 +302,84 @@ def _inject_propagation_headers(headers, args, kwargs): | |||
|         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): | ||||
|     """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, | ||||
|         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._tracer = get_tracer( | ||||
|             __name__, | ||||
|             instrumenting_library_version=__version__, | ||||
|             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._response_hook = response_hook | ||||
|  | @ -340,6 +426,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport): | |||
|     ) -> None: | ||||
|         self._transport.__exit__(exc_type, exc_value, traceback) | ||||
| 
 | ||||
|     # pylint: disable=R0914 | ||||
|     def handle_request( | ||||
|         self, | ||||
|         *args, | ||||
|  | @ -355,39 +442,64 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport): | |||
|         method, url, headers, stream, extensions = _extract_parameters( | ||||
|             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) | ||||
|         span_name = _get_default_span_name( | ||||
|             span_attributes[SpanAttributes.HTTP_METHOD] | ||||
|         ) | ||||
| 
 | ||||
|         with self._tracer.start_as_current_span( | ||||
|             span_name, kind=SpanKind.CLIENT, attributes=span_attributes | ||||
|         ) as span: | ||||
|             if self._request_hook is not None: | ||||
|             exception = None | ||||
|             if callable(self._request_hook): | ||||
|                 self._request_hook(span, request_info) | ||||
| 
 | ||||
|             _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: | ||||
|                 self._response_hook( | ||||
|                     span, | ||||
|                     request_info, | ||||
|                     ResponseInfo(status_code, headers, stream, extensions), | ||||
|             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): | ||||
|                     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 | ||||
| 
 | ||||
|     def close(self) -> None: | ||||
|  | @ -413,12 +525,17 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport): | |||
|         request_hook: typing.Optional[AsyncRequestHook] = 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._tracer = get_tracer( | ||||
|             __name__, | ||||
|             instrumenting_library_version=__version__, | ||||
|             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._response_hook = response_hook | ||||
|  | @ -435,6 +552,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport): | |||
|     ) -> None: | ||||
|         await self._transport.__aexit__(exc_type, exc_value, traceback) | ||||
| 
 | ||||
|     # pylint: disable=R0914 | ||||
|     async def handle_async_request(self, *args, **kwargs) -> typing.Union[ | ||||
|         typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict], | ||||
|         httpx.Response, | ||||
|  | @ -446,41 +564,66 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport): | |||
|         method, url, headers, stream, extensions = _extract_parameters( | ||||
|             args, kwargs | ||||
|         ) | ||||
|         span_attributes = _prepare_attributes(method, url) | ||||
| 
 | ||||
|         span_name = _get_default_span_name( | ||||
|             span_attributes[SpanAttributes.HTTP_METHOD] | ||||
|         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) | ||||
| 
 | ||||
|         with self._tracer.start_as_current_span( | ||||
|             span_name, kind=SpanKind.CLIENT, attributes=span_attributes | ||||
|         ) as span: | ||||
|             if self._request_hook is not None: | ||||
|             exception = None | ||||
|             if callable(self._request_hook): | ||||
|                 await self._request_hook(span, request_info) | ||||
| 
 | ||||
|             _inject_propagation_headers(headers, 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), | ||||
|             try: | ||||
|                 response = await self._transport.handle_async_request( | ||||
|                     *args, **kwargs | ||||
|                 ) | ||||
|             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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,3 +14,7 @@ | |||
| 
 | ||||
| 
 | ||||
| _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 | ||||
| # limitations under the License. | ||||
| 
 | ||||
| # pylint: disable=too-many-lines | ||||
| 
 | ||||
| import abc | ||||
| import asyncio | ||||
| import typing | ||||
|  | @ -22,6 +24,10 @@ import respx | |||
| 
 | ||||
| import opentelemetry.instrumentation.httpx | ||||
| from opentelemetry import trace | ||||
| from opentelemetry.instrumentation._semconv import ( | ||||
|     OTEL_SEMCONV_STABILITY_OPT_IN, | ||||
|     _OpenTelemetrySemanticConventionStability, | ||||
| ) | ||||
| from opentelemetry.instrumentation.httpx import ( | ||||
|     AsyncOpenTelemetryTransport, | ||||
|     HTTPXClientInstrumentor, | ||||
|  | @ -30,6 +36,21 @@ from opentelemetry.instrumentation.httpx import ( | |||
| from opentelemetry.instrumentation.utils import suppress_http_instrumentation | ||||
| from opentelemetry.propagate import get_global_textmap, set_global_textmap | ||||
| 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.test.mock_textmap import MockTextMapPropagator | ||||
| from opentelemetry.test.test_base import TestBase | ||||
|  | @ -100,6 +121,9 @@ async def _async_no_update_request_hook(span: "Span", request: "RequestInfo"): | |||
|     return 123 | ||||
| 
 | ||||
| 
 | ||||
| # pylint: disable=too-many-public-methods | ||||
| 
 | ||||
| 
 | ||||
| # 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 | ||||
| # subclassing abc.ABC. | ||||
|  | @ -112,15 +136,39 @@ class BaseTestCases: | |||
|         request_hook = staticmethod(_request_hook) | ||||
|         no_update_request_hook = staticmethod(_no_update_request_hook) | ||||
| 
 | ||||
|         # TODO: make this more explicit to tests | ||||
|         # pylint: disable=invalid-name | ||||
|         def setUp(self): | ||||
|             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.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 | ||||
|         def tearDown(self): | ||||
|             super().tearDown() | ||||
|             self.env_patch.stop() | ||||
|             respx.stop() | ||||
| 
 | ||||
|         def assert_span( | ||||
|  | @ -169,6 +217,87 @@ class BaseTestCases: | |||
|                 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): | ||||
|             self.perform_request(self.URL) | ||||
|             self.perform_request(self.URL) | ||||
|  | @ -191,6 +320,48 @@ class BaseTestCases: | |||
|                 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): | ||||
|             with suppress_http_instrumentation(): | ||||
|                 result = self.perform_request(self.URL) | ||||
|  | @ -245,6 +416,83 @@ class BaseTestCases: | |||
| 
 | ||||
|             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.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): | ||||
|             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_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): | ||||
|         @abc.abstractmethod | ||||
|         def create_client( | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue