Requests instrumentation http semantic convention migration (#2002)

This commit is contained in:
Leighton Chen
2023-11-29 14:22:16 -08:00
committed by GitHub
parent 4336dc7330
commit 4bf3577fb7
7 changed files with 651 additions and 115 deletions

View File

@ -6,7 +6,7 @@ on:
- 'release/*'
pull_request:
env:
CORE_REPO_SHA: 9831afaff5b4d371fd9a14266ab47884546bd971
CORE_REPO_SHA: 35a021194787359324c46f5ca99d31802e4c92bd
jobs:
build:

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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__)

View File

@ -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
)