# 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. """ This package implements `OpenTelemetry Resources `_: *A Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry that is running in a container on Kubernetes has a Pod name, it is in a namespace and possibly is part of a Deployment which also has a name. All three of these attributes can be included in the Resource.* Resource objects are created with `Resource.create`, which accepts attributes (key-values). Resources should NOT be created via constructor, and working with `Resource` objects should only be done via the Resource API methods. Resource attributes can also be passed at process invocation in the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should register your resource with the `mysql.opentelemetry.sdk.trace.TracerProvider` by passing them into their constructors. The `Resource` passed to a provider is available to the exporter, which can send on this information as it sees fit. .. code-block:: python trace.set_tracer_provider( TracerProvider( resource=Resource.create({ "service.name": "shoppingcart", "service.instance.id": "instance-12", }), ), ) print(trace.get_tracer_provider().resource.attributes) {'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '0.13.dev0', 'service.name': 'shoppingcart', 'service.instance.id': 'instance-12'} Note that the OpenTelemetry project documents certain `"standard attributes" `_ that have prescribed semantic meanings, for example ``service.name`` in the above example. """ import abc import concurrent.futures import logging import sys import typing from json import dumps from os import environ from urllib import parse from mysql.opentelemetry.attributes import BoundedAttributes from mysql.opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) from mysql.opentelemetry.semconv.resource import ResourceAttributes from mysql.opentelemetry.util._importlib_metadata import entry_points, version from mysql.opentelemetry.util.types import AttributeValue LabelValue = AttributeValue Attributes = typing.Dict[str, LabelValue] logger = logging.getLogger(__name__) CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID CLOUD_REGION = ResourceAttributes.CLOUD_REGION CLOUD_AVAILABILITY_ZONE = ResourceAttributes.CLOUD_AVAILABILITY_ZONE CONTAINER_NAME = ResourceAttributes.CONTAINER_NAME CONTAINER_ID = ResourceAttributes.CONTAINER_ID CONTAINER_IMAGE_NAME = ResourceAttributes.CONTAINER_IMAGE_NAME CONTAINER_IMAGE_TAG = ResourceAttributes.CONTAINER_IMAGE_TAG DEPLOYMENT_ENVIRONMENT = ResourceAttributes.DEPLOYMENT_ENVIRONMENT FAAS_NAME = ResourceAttributes.FAAS_NAME FAAS_ID = ResourceAttributes.FAAS_ID FAAS_VERSION = ResourceAttributes.FAAS_VERSION FAAS_INSTANCE = ResourceAttributes.FAAS_INSTANCE HOST_NAME = ResourceAttributes.HOST_NAME HOST_TYPE = ResourceAttributes.HOST_TYPE HOST_IMAGE_NAME = ResourceAttributes.HOST_IMAGE_NAME HOST_IMAGE_ID = ResourceAttributes.HOST_IMAGE_ID HOST_IMAGE_VERSION = ResourceAttributes.HOST_IMAGE_VERSION KUBERNETES_CLUSTER_NAME = ResourceAttributes.K8S_CLUSTER_NAME KUBERNETES_NAMESPACE_NAME = ResourceAttributes.K8S_NAMESPACE_NAME KUBERNETES_POD_UID = ResourceAttributes.K8S_POD_UID KUBERNETES_POD_NAME = ResourceAttributes.K8S_POD_NAME KUBERNETES_CONTAINER_NAME = ResourceAttributes.K8S_CONTAINER_NAME KUBERNETES_REPLICA_SET_UID = ResourceAttributes.K8S_REPLICASET_UID KUBERNETES_REPLICA_SET_NAME = ResourceAttributes.K8S_REPLICASET_NAME KUBERNETES_DEPLOYMENT_UID = ResourceAttributes.K8S_DEPLOYMENT_UID KUBERNETES_DEPLOYMENT_NAME = ResourceAttributes.K8S_DEPLOYMENT_NAME KUBERNETES_STATEFUL_SET_UID = ResourceAttributes.K8S_STATEFULSET_UID KUBERNETES_STATEFUL_SET_NAME = ResourceAttributes.K8S_STATEFULSET_NAME KUBERNETES_DAEMON_SET_UID = ResourceAttributes.K8S_DAEMONSET_UID KUBERNETES_DAEMON_SET_NAME = ResourceAttributes.K8S_DAEMONSET_NAME KUBERNETES_JOB_UID = ResourceAttributes.K8S_JOB_UID KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME OS_TYPE = ResourceAttributes.OS_TYPE OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION PROCESS_PID = ResourceAttributes.PROCESS_PID PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND PROCESS_COMMAND_LINE = ResourceAttributes.PROCESS_COMMAND_LINE PROCESS_COMMAND_ARGS = ResourceAttributes.PROCESS_COMMAND_ARGS PROCESS_OWNER = ResourceAttributes.PROCESS_OWNER PROCESS_RUNTIME_NAME = ResourceAttributes.PROCESS_RUNTIME_NAME PROCESS_RUNTIME_VERSION = ResourceAttributes.PROCESS_RUNTIME_VERSION PROCESS_RUNTIME_DESCRIPTION = ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION SERVICE_NAME = ResourceAttributes.SERVICE_NAME SERVICE_NAMESPACE = ResourceAttributes.SERVICE_NAMESPACE SERVICE_INSTANCE_ID = ResourceAttributes.SERVICE_INSTANCE_ID SERVICE_VERSION = ResourceAttributes.SERVICE_VERSION TELEMETRY_SDK_NAME = ResourceAttributes.TELEMETRY_SDK_NAME TELEMETRY_SDK_VERSION = ResourceAttributes.TELEMETRY_SDK_VERSION TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE _OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk") class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" def __init__(self, attributes: Attributes, schema_url: typing.Optional[str] = None): self._attributes = BoundedAttributes(attributes=attributes) if schema_url is None: schema_url = "" self._schema_url = schema_url @staticmethod def create( attributes: typing.Optional[Attributes] = None, schema_url: typing.Optional[str] = None, ) -> "Resource": """Creates a new `Resource` from attributes. Args: attributes: Optional zero or more key-value pairs. schema_url: Optional URL pointing to the schema Returns: The newly-created Resource. """ if not attributes: attributes = {} resource_detectors = [] resource = _DEFAULT_RESOURCE otel_experimental_resource_detectors = environ.get( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "otel" ).split(",") if "otel" not in otel_experimental_resource_detectors: otel_experimental_resource_detectors.append("otel") for resource_detector in otel_experimental_resource_detectors: resource_detectors.append( next( iter( entry_points( group="opentelemetry_resource_detector", name=resource_detector.strip(), ) ) ).load()() ) resource = get_aggregated_resources( resource_detectors, _DEFAULT_RESOURCE ).merge(Resource(attributes, schema_url)) if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" process_executable_name = resource.attributes.get( PROCESS_EXECUTABLE_NAME, None ) if process_executable_name: default_service_name += ":" + process_executable_name resource = resource.merge( Resource({SERVICE_NAME: default_service_name}, schema_url) ) return resource @staticmethod def get_empty() -> "Resource": return _EMPTY_RESOURCE @property def attributes(self) -> Attributes: return self._attributes @property def schema_url(self) -> str: return self._schema_url def merge(self, other: "Resource") -> "Resource": """Merges this resource and an updating resource into a new `Resource`. If a key exists on both the old and updating resource, the value of the updating resource will override the old resource value. The updating resource's `schema_url` will be used only if the old `schema_url` is empty. Attempting to merge two resources with different, non-empty values for `schema_url` will result in an error and return the old resource. Args: other: The other resource to be merged. Returns: The newly-created Resource. """ merged_attributes = self.attributes.copy() merged_attributes.update(other.attributes) if self.schema_url == "": schema_url = other.schema_url elif other.schema_url == "": schema_url = self.schema_url elif self.schema_url == other.schema_url: schema_url = other.schema_url else: logger.error( "Failed to merge resources: The two schemas %s and %s are incompatible", self.schema_url, other.schema_url, ) return self return Resource(merged_attributes, schema_url) def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False return ( self._attributes == other._attributes and self._schema_url == other._schema_url ) def __hash__(self): return hash( f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}" ) def to_json(self, indent=4) -> str: return dumps( { "attributes": dict(self._attributes), "schema_url": self._schema_url, }, indent=indent, ) _EMPTY_RESOURCE = Resource({}) _DEFAULT_RESOURCE = Resource( { TELEMETRY_SDK_LANGUAGE: "python", TELEMETRY_SDK_NAME: "opentelemetry", TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, } ) class ResourceDetector(abc.ABC): def __init__(self, raise_on_error=False): self.raise_on_error = raise_on_error @abc.abstractmethod def detect(self) -> "Resource": raise NotImplementedError() class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map = {} if env_resources_items: for item in env_resources_items.split(","): try: key, value = item.split("=", maxsplit=1) except ValueError as exc: logger.warning( "Invalid key value resource attribute pair %s: %s", item, exc, ) continue value_url_decoded = parse.unquote(value.strip()) env_resource_map[key.strip()] = value_url_decoded service_name = environ.get(OTEL_SERVICE_NAME) if service_name: env_resource_map[SERVICE_NAME] = service_name return Resource(env_resource_map) class ProcessResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": _runtime_version = ".".join( map( str, sys.version_info[:3] if sys.version_info.releaselevel == "final" and not sys.version_info.serial else sys.version_info, ) ) return Resource( { PROCESS_RUNTIME_DESCRIPTION: sys.version, PROCESS_RUNTIME_NAME: sys.implementation.name, PROCESS_RUNTIME_VERSION: _runtime_version, } ) def get_aggregated_resources( detectors: typing.List["ResourceDetector"], initial_resource: typing.Optional[Resource] = None, timeout=5, ) -> "Resource": """Retrieves resources from detectors in the order that they were passed :param detectors: List of resources in order of priority :param initial_resource: Static resource. This has highest priority :param timeout: Number of seconds to wait for each detector to return :return: """ detectors_merged_resource = initial_resource or Resource.create() with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(detector.detect) for detector in detectors] for detector_ind, future in enumerate(futures): detector = detectors[detector_ind] try: detected_resource = future.result(timeout=timeout) # pylint: disable=broad-except except Exception as ex: detected_resource = _EMPTY_RESOURCE if detector.raise_on_error: raise ex logger.warning("Exception %s in detector %s, ignoring", ex, detector) finally: detectors_merged_resource = detectors_merged_resource.merge( detected_resource ) return detectors_merged_resource