498 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			498 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
| # 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.instrumentation.utils import http_status_to_status_code
 | |
| from opentelemetry.semconv._incubating.attributes.http_attributes import (
 | |
|     HTTP_FLAVOR,
 | |
|     HTTP_HOST,
 | |
|     HTTP_METHOD,
 | |
|     HTTP_SCHEME,
 | |
|     HTTP_SERVER_NAME,
 | |
|     HTTP_STATUS_CODE,
 | |
|     HTTP_TARGET,
 | |
|     HTTP_URL,
 | |
|     HTTP_USER_AGENT,
 | |
| )
 | |
| from opentelemetry.semconv._incubating.attributes.net_attributes import (
 | |
|     NET_HOST_NAME,
 | |
|     NET_HOST_PORT,
 | |
|     NET_PEER_IP,
 | |
|     NET_PEER_NAME,
 | |
|     NET_PEER_PORT,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.client_attributes import (
 | |
|     CLIENT_ADDRESS,
 | |
|     CLIENT_PORT,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
 | |
| from opentelemetry.semconv.attributes.http_attributes import (
 | |
|     HTTP_REQUEST_METHOD,
 | |
|     HTTP_REQUEST_METHOD_ORIGINAL,
 | |
|     HTTP_RESPONSE_STATUS_CODE,
 | |
|     HTTP_ROUTE,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.network_attributes import (
 | |
|     NETWORK_PROTOCOL_VERSION,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.server_attributes import (
 | |
|     SERVER_ADDRESS,
 | |
|     SERVER_PORT,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.url_attributes import (
 | |
|     URL_FULL,
 | |
|     URL_PATH,
 | |
|     URL_QUERY,
 | |
|     URL_SCHEME,
 | |
| )
 | |
| from opentelemetry.semconv.attributes.user_agent_attributes import (
 | |
|     USER_AGENT_ORIGINAL,
 | |
| )
 | |
| from opentelemetry.semconv.schemas import Schemas
 | |
| from opentelemetry.trace.status import Status, StatusCode
 | |
| 
 | |
| # Values defined in milliseconds
 | |
| HTTP_DURATION_HISTOGRAM_BUCKETS_OLD = (
 | |
|     0.0,
 | |
|     5.0,
 | |
|     10.0,
 | |
|     25.0,
 | |
|     50.0,
 | |
|     75.0,
 | |
|     100.0,
 | |
|     250.0,
 | |
|     500.0,
 | |
|     750.0,
 | |
|     1000.0,
 | |
|     2500.0,
 | |
|     5000.0,
 | |
|     7500.0,
 | |
|     10000.0,
 | |
| )
 | |
| 
 | |
| # Values defined in seconds
 | |
| HTTP_DURATION_HISTOGRAM_BUCKETS_NEW = (
 | |
|     0.005,
 | |
|     0.01,
 | |
|     0.025,
 | |
|     0.05,
 | |
|     0.075,
 | |
|     0.1,
 | |
|     0.25,
 | |
|     0.5,
 | |
|     0.75,
 | |
|     1,
 | |
|     2.5,
 | |
|     5,
 | |
|     7.5,
 | |
|     10,
 | |
| )
 | |
| 
 | |
| # These lists represent attributes for metrics that are currently supported
 | |
| 
 | |
| _client_duration_attrs_old = [
 | |
|     HTTP_STATUS_CODE,
 | |
|     HTTP_HOST,
 | |
|     HTTP_METHOD,
 | |
|     HTTP_FLAVOR,
 | |
|     HTTP_SCHEME,
 | |
|     NET_PEER_PORT,
 | |
|     NET_PEER_NAME,
 | |
| ]
 | |
| 
 | |
| _client_duration_attrs_new = [
 | |
|     ERROR_TYPE,
 | |
|     HTTP_REQUEST_METHOD,
 | |
|     HTTP_RESPONSE_STATUS_CODE,
 | |
|     NETWORK_PROTOCOL_VERSION,
 | |
|     SERVER_ADDRESS,
 | |
|     SERVER_PORT,
 | |
|     # TODO: Support opt-in for scheme in new semconv
 | |
|     # URL_SCHEME,
 | |
| ]
 | |
| 
 | |
| _server_duration_attrs_old = [
 | |
|     HTTP_METHOD,
 | |
|     HTTP_HOST,
 | |
|     HTTP_SCHEME,
 | |
|     HTTP_STATUS_CODE,
 | |
|     HTTP_FLAVOR,
 | |
|     HTTP_SERVER_NAME,
 | |
|     NET_HOST_NAME,
 | |
|     NET_HOST_PORT,
 | |
| ]
 | |
| 
 | |
| _server_duration_attrs_new = [
 | |
|     ERROR_TYPE,
 | |
|     HTTP_REQUEST_METHOD,
 | |
|     HTTP_RESPONSE_STATUS_CODE,
 | |
|     HTTP_ROUTE,
 | |
|     NETWORK_PROTOCOL_VERSION,
 | |
|     URL_SCHEME,
 | |
| ]
 | |
| 
 | |
| _server_active_requests_count_attrs_old = [
 | |
|     HTTP_METHOD,
 | |
|     HTTP_HOST,
 | |
|     HTTP_SCHEME,
 | |
|     HTTP_FLAVOR,
 | |
|     HTTP_SERVER_NAME,
 | |
| ]
 | |
| 
 | |
| _server_active_requests_count_attrs_new = [
 | |
|     HTTP_REQUEST_METHOD,
 | |
|     URL_SCHEME,
 | |
|     # TODO: Support SERVER_ADDRESS AND SERVER_PORT
 | |
| ]
 | |
| 
 | |
| OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN"
 | |
| 
 | |
| 
 | |
| class _OpenTelemetryStabilitySignalType(Enum):
 | |
|     HTTP = "http"
 | |
|     DATABASE = "database"
 | |
|     GEN_AI = "gen_ai"
 | |
| 
 | |
| 
 | |
| class _StabilityMode(Enum):
 | |
|     DEFAULT = "default"
 | |
|     HTTP = "http"
 | |
|     HTTP_DUP = "http/dup"
 | |
|     DATABASE = "database"
 | |
|     DATABASE_DUP = "database/dup"
 | |
|     GEN_AI_LATEST_EXPERIMENTAL = "gen_ai_latest_experimental"
 | |
| 
 | |
| 
 | |
| def _report_new(mode: _StabilityMode):
 | |
|     return mode != _StabilityMode.DEFAULT
 | |
| 
 | |
| 
 | |
| def _report_old(mode: _StabilityMode):
 | |
|     return mode not in (_StabilityMode.HTTP, _StabilityMode.DATABASE)
 | |
| 
 | |
| 
 | |
| class _OpenTelemetrySemanticConventionStability:
 | |
|     _initialized = False
 | |
|     _lock = threading.Lock()
 | |
|     _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {}
 | |
| 
 | |
|     @classmethod
 | |
|     def _initialize(cls):
 | |
|         with cls._lock:
 | |
|             if cls._initialized:
 | |
|                 return
 | |
| 
 | |
|             # Users can pass in comma delimited string for opt-in options
 | |
|             # Only values for http, gen ai, and database stability are supported for now
 | |
|             opt_in = os.environ.get(OTEL_SEMCONV_STABILITY_OPT_IN)
 | |
| 
 | |
|             if not opt_in:
 | |
|                 # early return in case of default
 | |
|                 cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {
 | |
|                     _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT,
 | |
|                     _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT,
 | |
|                     _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.DEFAULT,
 | |
|                 }
 | |
|                 cls._initialized = True
 | |
|                 return
 | |
| 
 | |
|             opt_in_list = [s.strip() for s in opt_in.split(",")]
 | |
| 
 | |
|             cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
 | |
|                 _OpenTelemetryStabilitySignalType.HTTP
 | |
|             ] = cls._filter_mode(
 | |
|                 opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP
 | |
|             )
 | |
| 
 | |
|             cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
 | |
|                 _OpenTelemetryStabilitySignalType.GEN_AI
 | |
|             ] = cls._filter_mode(
 | |
|                 opt_in_list,
 | |
|                 _StabilityMode.DEFAULT,
 | |
|                 _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL,
 | |
|             )
 | |
| 
 | |
|             cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
 | |
|                 _OpenTelemetryStabilitySignalType.DATABASE
 | |
|             ] = cls._filter_mode(
 | |
|                 opt_in_list,
 | |
|                 _StabilityMode.DATABASE,
 | |
|                 _StabilityMode.DATABASE_DUP,
 | |
|             )
 | |
|             cls._initialized = True
 | |
| 
 | |
|     @staticmethod
 | |
|     def _filter_mode(opt_in_list, stable_mode, dup_mode):
 | |
|         # Process semconv stability opt-in
 | |
|         # http/dup,database/dup has higher precedence over http,database
 | |
|         if dup_mode.value in opt_in_list:
 | |
|             return dup_mode
 | |
| 
 | |
|         return (
 | |
|             stable_mode
 | |
|             if stable_mode.value in opt_in_list
 | |
|             else _StabilityMode.DEFAULT
 | |
|         )
 | |
| 
 | |
|     @classmethod
 | |
|     def _get_opentelemetry_stability_opt_in_mode(
 | |
|         cls, signal_type: _OpenTelemetryStabilitySignalType
 | |
|     ) -> _StabilityMode:
 | |
|         # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.)
 | |
|         return cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get(
 | |
|             signal_type, _StabilityMode.DEFAULT
 | |
|         )
 | |
| 
 | |
| 
 | |
| def _filter_semconv_duration_attrs(
 | |
|     attrs,
 | |
|     old_attrs,
 | |
|     new_attrs,
 | |
|     sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
 | |
| ):
 | |
|     filtered_attrs = {}
 | |
|     # duration is two different metrics depending on sem_conv_opt_in_mode, so no DUP attributes
 | |
|     allowed_attributes = (
 | |
|         new_attrs if sem_conv_opt_in_mode == _StabilityMode.HTTP else old_attrs
 | |
|     )
 | |
|     for key, val in attrs.items():
 | |
|         if key in allowed_attributes:
 | |
|             filtered_attrs[key] = val
 | |
|     return filtered_attrs
 | |
| 
 | |
| 
 | |
| def _filter_semconv_active_request_count_attr(
 | |
|     attrs,
 | |
|     old_attrs,
 | |
|     new_attrs,
 | |
|     sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
 | |
| ):
 | |
|     filtered_attrs = {}
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         for key, val in attrs.items():
 | |
|             if key in old_attrs:
 | |
|                 filtered_attrs[key] = val
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         for key, val in attrs.items():
 | |
|             if key in new_attrs:
 | |
|                 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, HTTP_REQUEST_METHOD_ORIGINAL, original)
 | |
| 
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_METHOD, normalized)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_REQUEST_METHOD, normalized)
 | |
| 
 | |
| 
 | |
| def _set_http_status_code(result, code, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, HTTP_STATUS_CODE, code)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, HTTP_RESPONSE_STATUS_CODE, code)
 | |
| 
 | |
| 
 | |
| def _set_http_url(result, url, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_URL, url)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, 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, HTTP_SCHEME, scheme)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, URL_SCHEME, scheme)
 | |
| 
 | |
| 
 | |
| def _set_http_flavor_version(result, version, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_FLAVOR, version)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version)
 | |
| 
 | |
| 
 | |
| def _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_USER_AGENT, user_agent)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, USER_AGENT_ORIGINAL, user_agent)
 | |
| 
 | |
| 
 | |
| # Client
 | |
| 
 | |
| 
 | |
| def _set_http_host_client(result, host, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_HOST, host)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, SERVER_ADDRESS, host)
 | |
| 
 | |
| 
 | |
| def _set_http_net_peer_name_client(result, peer_name, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NET_PEER_NAME, peer_name)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, SERVER_ADDRESS, peer_name)
 | |
| 
 | |
| 
 | |
| def _set_http_peer_port_client(result, port, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, NET_PEER_PORT, port)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, SERVER_PORT, port)
 | |
| 
 | |
| 
 | |
| 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, HTTP_FLAVOR, version)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version)
 | |
| 
 | |
| 
 | |
| # Server
 | |
| 
 | |
| 
 | |
| def _set_http_net_host(result, host, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NET_HOST_NAME, host)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, SERVER_ADDRESS, host)
 | |
| 
 | |
| 
 | |
| def _set_http_net_host_port(result, port, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, NET_HOST_PORT, port)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, SERVER_PORT, port)
 | |
| 
 | |
| 
 | |
| def _set_http_target(result, target, path, query, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_TARGET, target)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         if path:
 | |
|             set_string_attribute(result, URL_PATH, path)
 | |
|         if query:
 | |
|             set_string_attribute(result, URL_QUERY, query)
 | |
| 
 | |
| 
 | |
| def _set_http_host_server(result, host, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, HTTP_HOST, host)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         if not result.get(SERVER_ADDRESS):
 | |
|             set_string_attribute(result, SERVER_ADDRESS, host)
 | |
| 
 | |
| 
 | |
| # net.peer.ip -> net.sock.peer.addr
 | |
| # https://github.com/open-telemetry/semantic-conventions/blob/40db676ca0e735aa84f242b5a0fb14e49438b69b/schemas/1.15.0#L18
 | |
| # net.sock.peer.addr -> client.socket.address for server spans (TODO) AND client.address if missing
 | |
| # https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/CHANGELOG.md#v1210-2023-07-13
 | |
| # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md#common-attributes-across-http-client-and-server-spans
 | |
| def _set_http_peer_ip_server(result, ip, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NET_PEER_IP, ip)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         # Only populate if not already populated
 | |
|         if not result.get(CLIENT_ADDRESS):
 | |
|             set_string_attribute(result, CLIENT_ADDRESS, ip)
 | |
| 
 | |
| 
 | |
| def _set_http_peer_port_server(result, port, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, NET_PEER_PORT, port)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_int_attribute(result, CLIENT_PORT, port)
 | |
| 
 | |
| 
 | |
| def _set_http_net_peer_name_server(result, name, sem_conv_opt_in_mode):
 | |
|     if _report_old(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, NET_PEER_NAME, name)
 | |
|     if _report_new(sem_conv_opt_in_mode):
 | |
|         set_string_attribute(result, CLIENT_ADDRESS, name)
 | |
| 
 | |
| 
 | |
| def _set_status(
 | |
|     span,
 | |
|     metrics_attributes: dict,
 | |
|     status_code: int,
 | |
|     status_code_str: str,
 | |
|     server_span: bool = True,
 | |
|     sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
 | |
| ):
 | |
|     if status_code < 0:
 | |
|         if _report_new(sem_conv_opt_in_mode):
 | |
|             metrics_attributes[ERROR_TYPE] = status_code_str
 | |
|         if span.is_recording():
 | |
|             if _report_new(sem_conv_opt_in_mode):
 | |
|                 span.set_attribute(ERROR_TYPE, status_code_str)
 | |
|             span.set_status(
 | |
|                 Status(
 | |
|                     StatusCode.ERROR,
 | |
|                     "Non-integer HTTP status: " + status_code_str,
 | |
|                 )
 | |
|             )
 | |
|     else:
 | |
|         status = http_status_to_status_code(
 | |
|             status_code, server_span=server_span
 | |
|         )
 | |
| 
 | |
|         if _report_old(sem_conv_opt_in_mode):
 | |
|             if span.is_recording():
 | |
|                 span.set_attribute(HTTP_STATUS_CODE, status_code)
 | |
|             metrics_attributes[HTTP_STATUS_CODE] = status_code
 | |
|         if _report_new(sem_conv_opt_in_mode):
 | |
|             if span.is_recording():
 | |
|                 span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code)
 | |
|             metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code
 | |
|             if status == StatusCode.ERROR:
 | |
|                 if span.is_recording():
 | |
|                     span.set_attribute(ERROR_TYPE, status_code_str)
 | |
|                 metrics_attributes[ERROR_TYPE] = status_code_str
 | |
|         if span.is_recording():
 | |
|             span.set_status(Status(status))
 | |
| 
 | |
| 
 | |
| # Get schema version based off of opt-in mode
 | |
| def _get_schema_url(mode: _StabilityMode) -> str:
 | |
|     if mode is _StabilityMode.DEFAULT:
 | |
|         return "https://opentelemetry.io/schemas/1.11.0"
 | |
|     return Schemas.V1_21_0.value
 |