mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-29 21:23:55 +08:00
Requests instrumentation http semantic convention migration (#2002)
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- 'release/*'
|
||||
pull_request:
|
||||
env:
|
||||
CORE_REPO_SHA: 9831afaff5b4d371fd9a14266ab47884546bd971
|
||||
CORE_REPO_SHA: 35a021194787359324c46f5ca99d31802e4c92bd
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -13,13 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987))
|
||||
- `opentelemetry-instrumentation-httpx` Fix mixing async and non async hooks
|
||||
([#1920](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1920))
|
||||
- `opentelemetry-instrumentation-requests` Implement new semantic convention opt-in with stable http semantic conventions
|
||||
([#2002](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2002))
|
||||
- `opentelemetry-instrument-grpc` Fix arity of context.abort for AIO RPCs
|
||||
([#2066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2066))
|
||||
|
||||
### Fixed
|
||||
|
||||
- `opentelemetry-instrumentation-urllib`/`opentelemetry-instrumentation-urllib3` Fix metric descriptions to match semantic conventions
|
||||
([#1959]((https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959))
|
||||
([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959))
|
||||
|
||||
## Version 1.21.0/0.42b0 (2023-11-01)
|
||||
|
||||
|
@ -62,6 +62,27 @@ from opentelemetry import context
|
||||
|
||||
# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.
|
||||
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
|
||||
from opentelemetry.instrumentation._semconv import (
|
||||
_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME,
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT,
|
||||
_filter_duration_attrs,
|
||||
_get_schema_url,
|
||||
_OpenTelemetrySemanticConventionStability,
|
||||
_OpenTelemetryStabilityMode,
|
||||
_OpenTelemetryStabilitySignalType,
|
||||
_report_new,
|
||||
_report_old,
|
||||
_set_http_hostname,
|
||||
_set_http_method,
|
||||
_set_http_net_peer_name,
|
||||
_set_http_network_protocol_version,
|
||||
_set_http_port,
|
||||
_set_http_scheme,
|
||||
_set_http_status_code,
|
||||
_set_http_url,
|
||||
)
|
||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||
from opentelemetry.instrumentation.requests.package import _instruments
|
||||
from opentelemetry.instrumentation.requests.version import __version__
|
||||
@ -72,15 +93,15 @@ from opentelemetry.instrumentation.utils import (
|
||||
from opentelemetry.metrics import Histogram, get_meter
|
||||
from opentelemetry.propagate import inject
|
||||
from opentelemetry.semconv.metrics import MetricInstruments
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace import SpanKind, Tracer, get_tracer
|
||||
from opentelemetry.trace.span import Span
|
||||
from opentelemetry.trace.status import Status
|
||||
from opentelemetry.trace.status import StatusCode
|
||||
from opentelemetry.util.http import (
|
||||
ExcludeList,
|
||||
get_excluded_urls,
|
||||
parse_excluded_urls,
|
||||
remove_url_credentials,
|
||||
sanitize_method,
|
||||
)
|
||||
from opentelemetry.util.http.httplib import set_ip_on_next_http_connection
|
||||
|
||||
@ -94,10 +115,12 @@ _ResponseHookT = Optional[Callable[[Span, PreparedRequest], None]]
|
||||
# pylint: disable=R0915
|
||||
def _instrument(
|
||||
tracer: Tracer,
|
||||
duration_histogram: Histogram,
|
||||
duration_histogram_old: Histogram,
|
||||
duration_histogram_new: Histogram,
|
||||
request_hook: _RequestHookT = None,
|
||||
response_hook: _ResponseHookT = None,
|
||||
excluded_urls: ExcludeList = None,
|
||||
sem_conv_opt_in_mode: _OpenTelemetryStabilityMode = _OpenTelemetryStabilityMode.DEFAULT,
|
||||
):
|
||||
"""Enables tracing of all requests calls that go through
|
||||
:code:`requests.session.Session.request` (this includes
|
||||
@ -132,31 +155,58 @@ def _instrument(
|
||||
return wrapped_send(self, request, **kwargs)
|
||||
|
||||
# See
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client
|
||||
method = request.method.upper()
|
||||
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client
|
||||
method = request.method
|
||||
span_name = get_default_span_name(method)
|
||||
|
||||
url = remove_url_credentials(request.url)
|
||||
|
||||
span_attributes = {
|
||||
SpanAttributes.HTTP_METHOD: method,
|
||||
SpanAttributes.HTTP_URL: url,
|
||||
}
|
||||
span_attributes = {}
|
||||
_set_http_method(
|
||||
span_attributes, method, span_name, sem_conv_opt_in_mode
|
||||
)
|
||||
_set_http_url(span_attributes, url, sem_conv_opt_in_mode)
|
||||
|
||||
metric_labels = {
|
||||
SpanAttributes.HTTP_METHOD: method,
|
||||
}
|
||||
metric_labels = {}
|
||||
_set_http_method(
|
||||
metric_labels, method, span_name, sem_conv_opt_in_mode
|
||||
)
|
||||
|
||||
try:
|
||||
parsed_url = urlparse(url)
|
||||
metric_labels[SpanAttributes.HTTP_SCHEME] = parsed_url.scheme
|
||||
if parsed_url.scheme:
|
||||
_set_http_scheme(
|
||||
metric_labels, parsed_url.scheme, sem_conv_opt_in_mode
|
||||
)
|
||||
if parsed_url.hostname:
|
||||
metric_labels[SpanAttributes.HTTP_HOST] = parsed_url.hostname
|
||||
metric_labels[
|
||||
SpanAttributes.NET_PEER_NAME
|
||||
] = parsed_url.hostname
|
||||
_set_http_hostname(
|
||||
metric_labels, parsed_url.hostname, sem_conv_opt_in_mode
|
||||
)
|
||||
_set_http_net_peer_name(
|
||||
metric_labels, parsed_url.hostname, sem_conv_opt_in_mode
|
||||
)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
_set_http_hostname(
|
||||
span_attributes,
|
||||
parsed_url.hostname,
|
||||
sem_conv_opt_in_mode,
|
||||
)
|
||||
# Use semconv library when available
|
||||
span_attributes[
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS
|
||||
] = parsed_url.hostname
|
||||
if parsed_url.port:
|
||||
metric_labels[SpanAttributes.NET_PEER_PORT] = parsed_url.port
|
||||
_set_http_port(
|
||||
metric_labels, parsed_url.port, sem_conv_opt_in_mode
|
||||
)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
_set_http_port(
|
||||
span_attributes, parsed_url.port, sem_conv_opt_in_mode
|
||||
)
|
||||
# Use semconv library when available
|
||||
span_attributes[
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT
|
||||
] = parsed_url.port
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@ -182,35 +232,78 @@ def _instrument(
|
||||
exception = exc
|
||||
result = getattr(exc, "response", None)
|
||||
finally:
|
||||
elapsed_time = max(
|
||||
round((default_timer() - start_time) * 1000), 0
|
||||
)
|
||||
elapsed_time = max(default_timer() - start_time, 0)
|
||||
context.detach(token)
|
||||
|
||||
if isinstance(result, Response):
|
||||
span_attributes = {}
|
||||
if span.is_recording():
|
||||
span.set_attribute(
|
||||
SpanAttributes.HTTP_STATUS_CODE, result.status_code
|
||||
_set_http_status_code(
|
||||
span_attributes,
|
||||
result.status_code,
|
||||
sem_conv_opt_in_mode,
|
||||
)
|
||||
span.set_status(
|
||||
Status(http_status_to_status_code(result.status_code))
|
||||
_set_http_status_code(
|
||||
metric_labels, result.status_code, sem_conv_opt_in_mode
|
||||
)
|
||||
|
||||
metric_labels[
|
||||
SpanAttributes.HTTP_STATUS_CODE
|
||||
] = result.status_code
|
||||
status_code = http_status_to_status_code(
|
||||
result.status_code
|
||||
)
|
||||
span.set_status(status_code)
|
||||
if (
|
||||
_report_new(sem_conv_opt_in_mode)
|
||||
and status_code is StatusCode.ERROR
|
||||
):
|
||||
span_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = str(
|
||||
result.status_code
|
||||
)
|
||||
metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = str(
|
||||
result.status_code
|
||||
)
|
||||
|
||||
if result.raw is not None:
|
||||
version = getattr(result.raw, "version", None)
|
||||
if version:
|
||||
metric_labels[SpanAttributes.HTTP_FLAVOR] = (
|
||||
"1.1" if version == 11 else "1.0"
|
||||
# Only HTTP/1 is supported by requests
|
||||
version_text = "1.1" if version == 11 else "1.0"
|
||||
_set_http_network_protocol_version(
|
||||
metric_labels, version_text, sem_conv_opt_in_mode
|
||||
)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
_set_http_network_protocol_version(
|
||||
span_attributes,
|
||||
version_text,
|
||||
sem_conv_opt_in_mode,
|
||||
)
|
||||
for key, val in span_attributes.items():
|
||||
span.set_attribute(key, val)
|
||||
|
||||
if callable(response_hook):
|
||||
response_hook(span, request, result)
|
||||
|
||||
duration_histogram.record(elapsed_time, attributes=metric_labels)
|
||||
if exception is not None and _report_new(sem_conv_opt_in_mode):
|
||||
span.set_attribute(
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE, type(exception).__qualname__
|
||||
)
|
||||
metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = type(
|
||||
exception
|
||||
).__qualname__
|
||||
|
||||
if duration_histogram_old is not None:
|
||||
duration_attrs_old = _filter_duration_attrs(
|
||||
metric_labels, _OpenTelemetryStabilityMode.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_duration_attrs(
|
||||
metric_labels, _OpenTelemetryStabilityMode.HTTP
|
||||
)
|
||||
duration_histogram_new.record(
|
||||
elapsed_time, attributes=duration_attrs_new
|
||||
)
|
||||
|
||||
if exception is not None:
|
||||
raise exception.with_traceback(exception.__traceback__)
|
||||
@ -254,7 +347,7 @@ def get_default_span_name(method):
|
||||
Returns:
|
||||
span name
|
||||
"""
|
||||
return method.strip()
|
||||
return sanitize_method(method.upper().strip())
|
||||
|
||||
|
||||
class RequestsInstrumentor(BaseInstrumentor):
|
||||
@ -276,12 +369,16 @@ class RequestsInstrumentor(BaseInstrumentor):
|
||||
``excluded_urls``: A string containing a comma-delimited
|
||||
list of regexes used to exclude URLs from tracking
|
||||
"""
|
||||
semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
|
||||
_OpenTelemetryStabilitySignalType.HTTP,
|
||||
)
|
||||
schema_url = _get_schema_url(semconv_opt_in_mode)
|
||||
tracer_provider = kwargs.get("tracer_provider")
|
||||
tracer = get_tracer(
|
||||
__name__,
|
||||
__version__,
|
||||
tracer_provider,
|
||||
schema_url="https://opentelemetry.io/schemas/1.11.0",
|
||||
schema_url=schema_url,
|
||||
)
|
||||
excluded_urls = kwargs.get("excluded_urls")
|
||||
meter_provider = kwargs.get("meter_provider")
|
||||
@ -289,21 +386,32 @@ class RequestsInstrumentor(BaseInstrumentor):
|
||||
__name__,
|
||||
__version__,
|
||||
meter_provider,
|
||||
schema_url="https://opentelemetry.io/schemas/1.11.0",
|
||||
)
|
||||
duration_histogram = meter.create_histogram(
|
||||
name=MetricInstruments.HTTP_CLIENT_DURATION,
|
||||
unit="ms",
|
||||
description="measures the duration of the outbound HTTP request",
|
||||
schema_url=schema_url,
|
||||
)
|
||||
duration_histogram_old = None
|
||||
if _report_old(semconv_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",
|
||||
)
|
||||
duration_histogram_new = None
|
||||
if _report_new(semconv_opt_in_mode):
|
||||
duration_histogram_new = meter.create_histogram(
|
||||
name=_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME,
|
||||
unit="s",
|
||||
description="Duration of HTTP client requests.",
|
||||
)
|
||||
_instrument(
|
||||
tracer,
|
||||
duration_histogram,
|
||||
duration_histogram_old,
|
||||
duration_histogram_new,
|
||||
request_hook=kwargs.get("request_hook"),
|
||||
response_hook=kwargs.get("response_hook"),
|
||||
excluded_urls=_excluded_urls_from_env
|
||||
if excluded_urls is None
|
||||
else parse_excluded_urls(excluded_urls),
|
||||
sem_conv_opt_in_mode=semconv_opt_in_mode,
|
||||
)
|
||||
|
||||
def _uninstrument(self, **kwargs):
|
||||
|
@ -25,6 +25,13 @@ from opentelemetry import context, trace
|
||||
|
||||
# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.
|
||||
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
|
||||
from opentelemetry.instrumentation._semconv import (
|
||||
_OTEL_SEMCONV_STABILITY_OPT_IN_KEY,
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT,
|
||||
_OpenTelemetrySemanticConventionStability,
|
||||
)
|
||||
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
||||
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
||||
from opentelemetry.propagate import get_global_textmap, set_global_textmap
|
||||
@ -69,12 +76,24 @@ class RequestsIntegrationTestBase(abc.ABC):
|
||||
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_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg"
|
||||
"OTEL_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg",
|
||||
_OTEL_SEMCONV_STABILITY_OPT_IN_KEY: sem_conv_mode,
|
||||
},
|
||||
)
|
||||
|
||||
_OpenTelemetrySemanticConventionStability._initialized = False
|
||||
|
||||
self.env_patch.start()
|
||||
|
||||
self.exclude_patch = mock.patch(
|
||||
@ -118,6 +137,11 @@ class RequestsIntegrationTestBase(abc.ABC):
|
||||
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||
self.assertEqual(span.name, "GET")
|
||||
|
||||
self.assertEqual(
|
||||
span.instrumentation_scope.schema_url,
|
||||
"https://opentelemetry.io/schemas/1.11.0",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
{
|
||||
@ -133,6 +157,84 @@ class RequestsIntegrationTestBase(abc.ABC):
|
||||
span, opentelemetry.instrumentation.requests
|
||||
)
|
||||
|
||||
def test_basic_new_semconv(self):
|
||||
url_with_port = "http://mock:80/status/200"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET, url_with_port, status=200, body="Hello!"
|
||||
)
|
||||
result = self.perform_request(url_with_port)
|
||||
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_REQUEST_METHOD: "GET",
|
||||
SpanAttributes.URL_FULL: url_with_port,
|
||||
SpanAttributes.SERVER_ADDRESS: "mock",
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock",
|
||||
SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200,
|
||||
SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1",
|
||||
SpanAttributes.SERVER_PORT: 80,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
|
||||
|
||||
self.assertEqualSpanInstrumentationScope(
|
||||
span, opentelemetry.instrumentation.requests
|
||||
)
|
||||
|
||||
def test_basic_both_semconv(self):
|
||||
url_with_port = "http://mock:80/status/200"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET, url_with_port, status=200, body="Hello!"
|
||||
)
|
||||
result = self.perform_request(url_with_port)
|
||||
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",
|
||||
SpanAttributes.HTTP_REQUEST_METHOD: "GET",
|
||||
SpanAttributes.HTTP_URL: url_with_port,
|
||||
SpanAttributes.URL_FULL: url_with_port,
|
||||
SpanAttributes.HTTP_HOST: "mock",
|
||||
SpanAttributes.SERVER_ADDRESS: "mock",
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock",
|
||||
SpanAttributes.NET_PEER_PORT: 80,
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200,
|
||||
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||
SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1",
|
||||
SpanAttributes.SERVER_PORT: 80,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
|
||||
|
||||
self.assertEqualSpanInstrumentationScope(
|
||||
span, opentelemetry.instrumentation.requests
|
||||
)
|
||||
|
||||
def test_hooks(self):
|
||||
def request_hook(span, request_obj):
|
||||
span.update_name("name set from hook")
|
||||
@ -214,6 +316,57 @@ class RequestsIntegrationTestBase(abc.ABC):
|
||||
trace.StatusCode.ERROR,
|
||||
)
|
||||
|
||||
def test_not_foundbasic_new_semconv(self):
|
||||
url_404 = "http://mock/status/404"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_404,
|
||||
status=404,
|
||||
)
|
||||
result = self.perform_request(url_404)
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
span = self.assert_span()
|
||||
|
||||
self.assertEqual(
|
||||
span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404
|
||||
)
|
||||
self.assertEqual(
|
||||
span.attributes.get(_SPAN_ATTRIBUTES_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"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
url_404,
|
||||
status=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(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404
|
||||
)
|
||||
self.assertEqual(
|
||||
span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404"
|
||||
)
|
||||
|
||||
self.assertIs(
|
||||
span.status.status_code,
|
||||
trace.StatusCode.ERROR,
|
||||
)
|
||||
|
||||
def test_uninstrument(self):
|
||||
RequestsInstrumentor().uninstrument()
|
||||
result = self.perform_request(self.URL)
|
||||
@ -368,6 +521,34 @@ class RequestsIntegrationTestBase(abc.ABC):
|
||||
)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
|
||||
@mock.patch(
|
||||
"requests.adapters.HTTPAdapter.send",
|
||||
side_effect=requests.RequestException,
|
||||
)
|
||||
def test_requests_exception_new_semconv(self, *_, **__):
|
||||
url_with_port = "http://mock:80/status/200"
|
||||
httpretty.register_uri(
|
||||
httpretty.GET, url_with_port, status=200, body="Hello!"
|
||||
)
|
||||
with self.assertRaises(requests.RequestException):
|
||||
self.perform_request(url_with_port)
|
||||
|
||||
span = self.assert_span()
|
||||
print(span.attributes)
|
||||
self.assertEqual(
|
||||
span.attributes,
|
||||
{
|
||||
SpanAttributes.HTTP_REQUEST_METHOD: "GET",
|
||||
SpanAttributes.URL_FULL: url_with_port,
|
||||
SpanAttributes.SERVER_ADDRESS: "mock",
|
||||
SpanAttributes.SERVER_PORT: 80,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80,
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock",
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE: "RequestException",
|
||||
},
|
||||
)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
|
||||
mocked_response = requests.Response()
|
||||
mocked_response.status_code = 500
|
||||
mocked_response.reason = "Internal Server Error"
|
||||
@ -489,6 +670,22 @@ class TestRequestsIntergrationMetric(TestBase):
|
||||
|
||||
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_KEY: sem_conv_mode,
|
||||
},
|
||||
)
|
||||
self.env_patch.start()
|
||||
_OpenTelemetrySemanticConventionStability._initialized = False
|
||||
RequestsInstrumentor().instrument(meter_provider=self.meter_provider)
|
||||
|
||||
httpretty.enable()
|
||||
@ -496,6 +693,7 @@ class TestRequestsIntergrationMetric(TestBase):
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.env_patch.stop()
|
||||
RequestsInstrumentor().uninstrument()
|
||||
httpretty.disable()
|
||||
|
||||
@ -507,22 +705,94 @@ class TestRequestsIntergrationMetric(TestBase):
|
||||
self.perform_request(self.URL)
|
||||
|
||||
expected_attributes = {
|
||||
"http.status_code": 200,
|
||||
"http.host": "examplehost",
|
||||
"net.peer.port": 8000,
|
||||
"net.peer.name": "examplehost",
|
||||
"http.method": "GET",
|
||||
"http.flavor": "1.1",
|
||||
"http.scheme": "http",
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
SpanAttributes.HTTP_HOST: "examplehost",
|
||||
SpanAttributes.NET_PEER_PORT: 8000,
|
||||
SpanAttributes.NET_PEER_NAME: "examplehost",
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||
SpanAttributes.HTTP_SCHEME: "http",
|
||||
}
|
||||
|
||||
for (
|
||||
resource_metrics
|
||||
) in self.memory_metrics_reader.get_metrics_data().resource_metrics:
|
||||
for scope_metrics in resource_metrics.scope_metrics:
|
||||
self.assertEqual(len(scope_metrics.metrics), 1)
|
||||
for metric in scope_metrics.metrics:
|
||||
self.assertEqual(metric.unit, "ms")
|
||||
self.assertEqual(
|
||||
metric.description,
|
||||
"measures the duration of the outbound HTTP request",
|
||||
)
|
||||
for data_point in metric.data.data_points:
|
||||
self.assertDictEqual(
|
||||
expected_attributes, dict(data_point.attributes)
|
||||
)
|
||||
self.assertEqual(data_point.count, 1)
|
||||
|
||||
def test_basic_metric_new_semconv(self):
|
||||
self.perform_request(self.URL)
|
||||
|
||||
expected_attributes = {
|
||||
SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200,
|
||||
SpanAttributes.SERVER_ADDRESS: "examplehost",
|
||||
SpanAttributes.SERVER_PORT: 8000,
|
||||
SpanAttributes.HTTP_REQUEST_METHOD: "GET",
|
||||
SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1",
|
||||
}
|
||||
for (
|
||||
resource_metrics
|
||||
) in self.memory_metrics_reader.get_metrics_data().resource_metrics:
|
||||
for scope_metrics in resource_metrics.scope_metrics:
|
||||
self.assertEqual(len(scope_metrics.metrics), 1)
|
||||
for metric in scope_metrics.metrics:
|
||||
self.assertEqual(metric.unit, "s")
|
||||
self.assertEqual(
|
||||
metric.description, "Duration of HTTP client requests."
|
||||
)
|
||||
for data_point in metric.data.data_points:
|
||||
self.assertDictEqual(
|
||||
expected_attributes, dict(data_point.attributes)
|
||||
)
|
||||
self.assertEqual(data_point.count, 1)
|
||||
|
||||
def test_basic_metric_both_semconv(self):
|
||||
self.perform_request(self.URL)
|
||||
|
||||
expected_attributes_old = {
|
||||
SpanAttributes.HTTP_STATUS_CODE: 200,
|
||||
SpanAttributes.HTTP_HOST: "examplehost",
|
||||
SpanAttributes.NET_PEER_PORT: 8000,
|
||||
SpanAttributes.NET_PEER_NAME: "examplehost",
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
SpanAttributes.HTTP_FLAVOR: "1.1",
|
||||
SpanAttributes.HTTP_SCHEME: "http",
|
||||
}
|
||||
|
||||
expected_attributes_new = {
|
||||
SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200,
|
||||
SpanAttributes.SERVER_ADDRESS: "examplehost",
|
||||
SpanAttributes.SERVER_PORT: 8000,
|
||||
SpanAttributes.HTTP_REQUEST_METHOD: "GET",
|
||||
SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1",
|
||||
}
|
||||
|
||||
for (
|
||||
resource_metrics
|
||||
) in self.memory_metrics_reader.get_metrics_data().resource_metrics:
|
||||
for scope_metrics in resource_metrics.scope_metrics:
|
||||
self.assertEqual(len(scope_metrics.metrics), 2)
|
||||
for metric in scope_metrics.metrics:
|
||||
for data_point in metric.data.data_points:
|
||||
if metric.unit == "ms":
|
||||
self.assertDictEqual(
|
||||
expected_attributes_old,
|
||||
dict(data_point.attributes),
|
||||
)
|
||||
else:
|
||||
self.assertDictEqual(
|
||||
expected_attributes_new,
|
||||
dict(data_point.attributes),
|
||||
)
|
||||
self.assertEqual(data_point.count, 1)
|
||||
|
@ -0,0 +1,217 @@
|
||||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import threading
|
||||
from enum import Enum
|
||||
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
|
||||
# TODO: will come through semconv package once updated
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE = "error.type"
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS = "network.peer.address"
|
||||
_SPAN_ATTRIBUTES_NETWORK_PEER_PORT = "network.peer.port"
|
||||
_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME = "http.client.request.duration"
|
||||
|
||||
_client_duration_attrs_old = [
|
||||
SpanAttributes.HTTP_STATUS_CODE,
|
||||
SpanAttributes.HTTP_HOST,
|
||||
SpanAttributes.NET_PEER_PORT,
|
||||
SpanAttributes.NET_PEER_NAME,
|
||||
SpanAttributes.HTTP_METHOD,
|
||||
SpanAttributes.HTTP_FLAVOR,
|
||||
SpanAttributes.HTTP_SCHEME,
|
||||
]
|
||||
|
||||
_client_duration_attrs_new = [
|
||||
_SPAN_ATTRIBUTES_ERROR_TYPE,
|
||||
SpanAttributes.HTTP_REQUEST_METHOD,
|
||||
SpanAttributes.HTTP_RESPONSE_STATUS_CODE,
|
||||
SpanAttributes.NETWORK_PROTOCOL_VERSION,
|
||||
SpanAttributes.SERVER_ADDRESS,
|
||||
SpanAttributes.SERVER_PORT,
|
||||
# TODO: Support opt-in for scheme in new semconv
|
||||
# SpanAttributes.URL_SCHEME,
|
||||
]
|
||||
|
||||
|
||||
def _filter_duration_attrs(attrs, sem_conv_opt_in_mode):
|
||||
filtered_attrs = {}
|
||||
allowed_attributes = (
|
||||
_client_duration_attrs_new
|
||||
if sem_conv_opt_in_mode == _OpenTelemetryStabilityMode.HTTP
|
||||
else _client_duration_attrs_old
|
||||
)
|
||||
for key, val in attrs.items():
|
||||
if key in allowed_attributes:
|
||||
filtered_attrs[key] = val
|
||||
return filtered_attrs
|
||||
|
||||
|
||||
def set_string_attribute(result, key, value):
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
|
||||
def set_int_attribute(result, key, value):
|
||||
if value:
|
||||
try:
|
||||
result[key] = int(value)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
|
||||
def _set_http_method(result, original, normalized, sem_conv_opt_in_mode):
|
||||
original = original.strip()
|
||||
normalized = normalized.strip()
|
||||
# See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes
|
||||
# Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized.
|
||||
if original != normalized and _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(
|
||||
result, SpanAttributes.HTTP_REQUEST_METHOD_ORIGINAL, original
|
||||
)
|
||||
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.HTTP_METHOD, normalized)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(
|
||||
result, SpanAttributes.HTTP_REQUEST_METHOD, normalized
|
||||
)
|
||||
|
||||
|
||||
def _set_http_url(result, url, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.HTTP_URL, url)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.URL_FULL, url)
|
||||
|
||||
|
||||
def _set_http_scheme(result, scheme, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.HTTP_SCHEME, scheme)
|
||||
# TODO: Support opt-in for scheme in new semconv
|
||||
# if _report_new(sem_conv_opt_in_mode):
|
||||
# set_string_attribute(result, SpanAttributes.URL_SCHEME, scheme)
|
||||
|
||||
|
||||
def _set_http_hostname(result, hostname, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.HTTP_HOST, hostname)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, hostname)
|
||||
|
||||
|
||||
def _set_http_net_peer_name(result, peer_name, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.NET_PEER_NAME, peer_name)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, peer_name)
|
||||
|
||||
|
||||
def _set_http_port(result, port, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_int_attribute(result, SpanAttributes.SERVER_PORT, port)
|
||||
|
||||
|
||||
def _set_http_status_code(result, code, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_int_attribute(
|
||||
result, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, code
|
||||
)
|
||||
|
||||
|
||||
def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode):
|
||||
if _report_old(sem_conv_opt_in_mode):
|
||||
set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version)
|
||||
if _report_new(sem_conv_opt_in_mode):
|
||||
set_string_attribute(
|
||||
result, SpanAttributes.NETWORK_PROTOCOL_VERSION, version
|
||||
)
|
||||
|
||||
|
||||
_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN"
|
||||
|
||||
|
||||
class _OpenTelemetryStabilitySignalType:
|
||||
HTTP = "http"
|
||||
|
||||
|
||||
class _OpenTelemetryStabilityMode(Enum):
|
||||
# http - emit the new, stable HTTP and networking conventions ONLY
|
||||
HTTP = "http"
|
||||
# http/dup - emit both the old and the stable HTTP and networking conventions
|
||||
HTTP_DUP = "http/dup"
|
||||
# default - continue emitting old experimental HTTP and networking conventions
|
||||
DEFAULT = "default"
|
||||
|
||||
|
||||
def _report_new(mode):
|
||||
return mode.name != _OpenTelemetryStabilityMode.DEFAULT.name
|
||||
|
||||
|
||||
def _report_old(mode):
|
||||
return mode.name != _OpenTelemetryStabilityMode.HTTP.name
|
||||
|
||||
|
||||
class _OpenTelemetrySemanticConventionStability:
|
||||
_initialized = False
|
||||
_lock = threading.Lock()
|
||||
_OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {}
|
||||
|
||||
@classmethod
|
||||
def _initialize(cls):
|
||||
with _OpenTelemetrySemanticConventionStability._lock:
|
||||
if not _OpenTelemetrySemanticConventionStability._initialized:
|
||||
# Users can pass in comma delimited string for opt-in options
|
||||
# Only values for http stability are supported for now
|
||||
opt_in = os.environ.get(_OTEL_SEMCONV_STABILITY_OPT_IN_KEY, "")
|
||||
opt_in_list = []
|
||||
if opt_in:
|
||||
opt_in_list = [s.strip() for s in opt_in.split(",")]
|
||||
http_opt_in = _OpenTelemetryStabilityMode.DEFAULT
|
||||
if opt_in_list:
|
||||
# Process http opt-in
|
||||
# http/dup takes priority over http
|
||||
if (
|
||||
_OpenTelemetryStabilityMode.HTTP_DUP.value
|
||||
in opt_in_list
|
||||
):
|
||||
http_opt_in = _OpenTelemetryStabilityMode.HTTP_DUP
|
||||
elif _OpenTelemetryStabilityMode.HTTP.value in opt_in_list:
|
||||
http_opt_in = _OpenTelemetryStabilityMode.HTTP
|
||||
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
|
||||
_OpenTelemetryStabilitySignalType.HTTP
|
||||
] = http_opt_in
|
||||
_OpenTelemetrySemanticConventionStability._initialized = True
|
||||
|
||||
@classmethod
|
||||
# Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.)
|
||||
def _get_opentelemetry_stability_opt_in_mode(
|
||||
cls,
|
||||
signal_type: _OpenTelemetryStabilitySignalType,
|
||||
) -> _OpenTelemetryStabilityMode:
|
||||
return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get(
|
||||
signal_type, _OpenTelemetryStabilityMode.DEFAULT
|
||||
)
|
||||
|
||||
|
||||
# Get schema version based off of opt-in mode
|
||||
def _get_schema_url(mode: _OpenTelemetryStabilityMode) -> str:
|
||||
if mode is _OpenTelemetryStabilityMode.DEFAULT:
|
||||
return "https://opentelemetry.io/schemas/1.11.0"
|
||||
return SpanAttributes.SCHEMA_URL
|
@ -21,13 +21,13 @@ from abc import ABC, abstractmethod
|
||||
from logging import getLogger
|
||||
from typing import Collection, Optional
|
||||
|
||||
from opentelemetry.instrumentation._semconv import (
|
||||
_OpenTelemetrySemanticConventionStability,
|
||||
)
|
||||
from opentelemetry.instrumentation.dependencies import (
|
||||
DependencyConflict,
|
||||
get_dependency_conflicts,
|
||||
)
|
||||
from opentelemetry.instrumentation.utils import (
|
||||
_OpenTelemetrySemanticConventionStability,
|
||||
)
|
||||
|
||||
_LOG = getLogger(__name__)
|
||||
|
||||
|
@ -12,10 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import threading
|
||||
import urllib.parse
|
||||
from enum import Enum
|
||||
from re import escape, sub
|
||||
from typing import Dict, Sequence
|
||||
|
||||
@ -155,61 +152,3 @@ def _python_path_without_directory(python_path, directory, path_separator):
|
||||
"",
|
||||
python_path,
|
||||
)
|
||||
|
||||
|
||||
_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN"
|
||||
|
||||
|
||||
class _OpenTelemetryStabilitySignalType:
|
||||
HTTP = "http"
|
||||
|
||||
|
||||
class _OpenTelemetryStabilityMode(Enum):
|
||||
# http - emit the new, stable HTTP and networking conventions ONLY
|
||||
HTTP = "http"
|
||||
# http/dup - emit both the old and the stable HTTP and networking conventions
|
||||
HTTP_DUP = "http/dup"
|
||||
# default - continue emitting old experimental HTTP and networking conventions
|
||||
DEFAULT = "default"
|
||||
|
||||
|
||||
class _OpenTelemetrySemanticConventionStability:
|
||||
_initialized = False
|
||||
_lock = threading.Lock()
|
||||
_OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {}
|
||||
|
||||
@classmethod
|
||||
def _initialize(cls):
|
||||
with _OpenTelemetrySemanticConventionStability._lock:
|
||||
if not _OpenTelemetrySemanticConventionStability._initialized:
|
||||
# Users can pass in comma delimited string for opt-in options
|
||||
# Only values for http stability are supported for now
|
||||
opt_in = os.environ.get(_OTEL_SEMCONV_STABILITY_OPT_IN_KEY, "")
|
||||
opt_in_list = []
|
||||
if opt_in:
|
||||
opt_in_list = [s.strip() for s in opt_in.split(",")]
|
||||
http_opt_in = _OpenTelemetryStabilityMode.DEFAULT
|
||||
if opt_in_list:
|
||||
# Process http opt-in
|
||||
# http/dup takes priority over http
|
||||
if (
|
||||
_OpenTelemetryStabilityMode.HTTP_DUP.value
|
||||
in opt_in_list
|
||||
):
|
||||
http_opt_in = _OpenTelemetryStabilityMode.HTTP_DUP
|
||||
elif _OpenTelemetryStabilityMode.HTTP.value in opt_in_list:
|
||||
http_opt_in = _OpenTelemetryStabilityMode.HTTP
|
||||
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
|
||||
_OpenTelemetryStabilitySignalType.HTTP
|
||||
] = http_opt_in
|
||||
_OpenTelemetrySemanticConventionStability._initialized = True
|
||||
|
||||
@classmethod
|
||||
def _get_opentelemetry_stability_opt_in(
|
||||
cls,
|
||||
signal_type: _OpenTelemetryStabilitySignalType,
|
||||
) -> _OpenTelemetryStabilityMode:
|
||||
with _OpenTelemetrySemanticConventionStability._lock:
|
||||
return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get(
|
||||
signal_type, _OpenTelemetryStabilityMode.DEFAULT
|
||||
)
|
||||
|
Reference in New Issue
Block a user