diff --git a/CHANGELOG.md b/CHANGELOG.md index a72276d75dd..612bcdd6a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +- Incorporate resource detectors to auto instrumentation (experimental feature) + ([#3172](https://github.com/open-telemetry/opentelemetry-python/pull/3172)) - PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) - - Add exponential histogram ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index bffc677fd00..25a8b25bead 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -59,6 +59,10 @@ sdk_tracer_provider = "opentelemetry.sdk.trace:TracerProvider" [project.entry-points.opentelemetry_traces_exporter] console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" +[project.entry-points.opentelemetry_resource_detector] +otel = "opentelemetry.sdk.resources:OTELResourceDetector" +process = "opentelemetry.sdk.resources:ProcessResourceDetector" + [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 376fb187dc1..fc1b0a0a1c6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -633,3 +633,12 @@ The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` is the client certificate/chain trust for clients private key to use in mTLS communication in PEM format. """ + +_OTEL_RESOURCE_DETECTORS = "_OTEL_RESOURCE_DETECTORS" +""" +.. envvar:: _OTEL_RESOURCE_DETECTORS + +The :envvar:`_OTEL_RESOURCE_DETECTORS` is a comma-separated string of names of +resource detectors. These names must be the same as the names of entry points +for the `opentelemetry-resource-detectors` entry point. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c46b87f89cc..9762b38d38e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -58,19 +58,20 @@ import abc import concurrent.futures import logging -import os import sys import typing from json import dumps +from os import environ from urllib import parse from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.environment_variables import ( + _OTEL_RESOURCE_DETECTORS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.util._importlib_metadata import version +from opentelemetry.util._importlib_metadata import entry_points, version from opentelemetry.util.types import AttributeValue LabelValue = AttributeValue @@ -165,9 +166,23 @@ def create( """ if not attributes: attributes = {} - resource = _DEFAULT_RESOURCE.merge( - OTELResourceDetector().detect() - ).merge(Resource(attributes, schema_url)) + + resource = _DEFAULT_RESOURCE + + for resource_detector in environ.get( + _OTEL_RESOURCE_DETECTORS, "otel" + ).split(","): + + resource = resource.merge( + entry_points( + group="opentelemetry_resource_detector", + name=resource_detector, + )[0] + .load()() + .detect() + ) + + resource = 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( @@ -273,7 +288,7 @@ def detect(self) -> "Resource": class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": - env_resources_items = os.environ.get(OTEL_RESOURCE_ATTRIBUTES) + env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map = {} if env_resources_items: @@ -290,7 +305,7 @@ def detect(self) -> "Resource": value_url_decoded = parse.unquote(value.strip()) env_resource_map[key.strip()] = value_url_decoded - service_name = os.environ.get(OTEL_SERVICE_NAME) + service_name = environ.get(OTEL_SERVICE_NAME) if service_name: env_resource_map[SERVICE_NAME] = service_name return Resource(env_resource_map) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index c9649f685ad..2bfddb9c650 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -14,22 +14,42 @@ # pylint: disable=protected-access -import os import unittest import uuid from logging import ERROR -from unittest import mock +from os import environ +from unittest.mock import Mock, patch from urllib import parse -from opentelemetry.sdk import resources +from opentelemetry.sdk.environment_variables import _OTEL_RESOURCE_DETECTORS +from opentelemetry.sdk.resources import ( + _DEFAULT_RESOURCE, + _EMPTY_RESOURCE, + _OPENTELEMETRY_SDK_VERSION, + OTEL_RESOURCE_ATTRIBUTES, + OTEL_SERVICE_NAME, + PROCESS_EXECUTABLE_NAME, + PROCESS_RUNTIME_DESCRIPTION, + PROCESS_RUNTIME_NAME, + PROCESS_RUNTIME_VERSION, + SERVICE_NAME, + TELEMETRY_SDK_LANGUAGE, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_VERSION, + OTELResourceDetector, + ProcessResourceDetector, + Resource, + ResourceDetector, + get_aggregated_resources, +) class TestResources(unittest.TestCase): def setUp(self) -> None: - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: - os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) + environ.pop(OTEL_RESOURCE_ATTRIBUTES) def test_create(self): attributes = { @@ -44,109 +64,101 @@ def test_create(self): "version": 1, "has_bugs": True, "cost": 112.12, - resources.TELEMETRY_SDK_NAME: "opentelemetry", - resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, - resources.SERVICE_NAME: "unknown_service", + TELEMETRY_SDK_NAME: "opentelemetry", + TELEMETRY_SDK_LANGUAGE: "python", + TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, + SERVICE_NAME: "unknown_service", } - resource = resources.Resource.create(attributes) - self.assertIsInstance(resource, resources.Resource) + resource = Resource.create(attributes) + self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, "") schema_url = "https://opentelemetry.io/schemas/1.3.0" - resource = resources.Resource.create(attributes, schema_url) - self.assertIsInstance(resource, resources.Resource) + resource = Resource.create(attributes, schema_url) + self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, schema_url) - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value" - resource = resources.Resource.create(attributes) - self.assertIsInstance(resource, resources.Resource) + environ[OTEL_RESOURCE_ATTRIBUTES] = "key=value" + resource = Resource.create(attributes) + self.assertIsInstance(resource, Resource) expected_with_envar = expected_attributes.copy() expected_with_envar["key"] = "value" self.assertEqual(resource.attributes, expected_with_envar) - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" - resource = resources.Resource.get_empty() - self.assertEqual(resource, resources._EMPTY_RESOURCE) + resource = Resource.get_empty() + self.assertEqual(resource, _EMPTY_RESOURCE) - resource = resources.Resource.create(None) + resource = Resource.create(None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create(None, None) + resource = Resource.create(None, None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create({}) + resource = Resource.create({}) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") - resource = resources.Resource.create({}, None) + resource = Resource.create({}, None) self.assertEqual( resource, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) self.assertEqual(resource.schema_url, "") def test_resource_merge(self): - left = resources.Resource({"service": "ui"}) - right = resources.Resource({"host": "service-host"}) + left = Resource({"service": "ui"}) + right = Resource({"host": "service-host"}) self.assertEqual( left.merge(right), - resources.Resource({"service": "ui", "host": "service-host"}), + Resource({"service": "ui", "host": "service-host"}), ) schema_urls = ( "https://opentelemetry.io/schemas/1.2.0", "https://opentelemetry.io/schemas/1.3.0", ) - left = resources.Resource.create({}, None) - right = resources.Resource.create({}, None) + left = Resource.create({}, None) + right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, "") - left = resources.Resource.create({}, None) - right = resources.Resource.create({}, schema_urls[0]) + left = Resource.create({}, None) + right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, None) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, schema_urls[0]) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) - left = resources.Resource.create({}, schema_urls[0]) - right = resources.Resource.create({}, schema_urls[1]) + left = Resource.create({}, schema_urls[0]) + right = Resource.create({}, schema_urls[1]) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual(left.merge(right), left) self.assertIn(schema_urls[0], log_entry.output[0]) @@ -159,13 +171,11 @@ def test_resource_merge_empty_string(self): the exception of the empty string. """ - left = resources.Resource({"service": "ui", "host": ""}) - right = resources.Resource( - {"host": "service-host", "service": "not-ui"} - ) + left = Resource({"service": "ui", "host": ""}) + right = Resource({"host": "service-host", "service": "not-ui"}) self.assertEqual( left.merge(right), - resources.Resource({"service": "not-ui", "host": "service-host"}), + Resource({"service": "not-ui", "host": "service-host"}), ) def test_immutability(self): @@ -177,16 +187,16 @@ def test_immutability(self): } default_attributes = { - resources.TELEMETRY_SDK_NAME: "opentelemetry", - resources.TELEMETRY_SDK_LANGUAGE: "python", - resources.TELEMETRY_SDK_VERSION: resources._OPENTELEMETRY_SDK_VERSION, - resources.SERVICE_NAME: "unknown_service", + TELEMETRY_SDK_NAME: "opentelemetry", + TELEMETRY_SDK_LANGUAGE: "python", + TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, + SERVICE_NAME: "unknown_service", } attributes_copy = attributes.copy() attributes_copy.update(default_attributes) - resource = resources.Resource.create(attributes) + resource = Resource.create(attributes) self.assertEqual(resource.attributes, attributes_copy) with self.assertRaises(TypeError): @@ -202,18 +212,16 @@ def test_immutability(self): self.assertEqual(resource.schema_url, "") def test_service_name_using_process_name(self): - resource = resources.Resource.create( - {resources.PROCESS_EXECUTABLE_NAME: "test"} - ) + resource = Resource.create({PROCESS_EXECUTABLE_NAME: "test"}) self.assertEqual( - resource.attributes.get(resources.SERVICE_NAME), + resource.attributes.get(SERVICE_NAME), "unknown_service:test", ) def test_invalid_resource_attribute_values(self): - resource = resources.Resource( + resource = Resource( { - resources.SERVICE_NAME: "test", + SERVICE_NAME: "test", "non-primitive-data-type": {}, "invalid-byte-type-attribute": b"\xd8\xe1\xb7\xeb\xa8\xe5 \xd2\xb7\xe1", "": "empty-key-value", @@ -224,43 +232,39 @@ def test_invalid_resource_attribute_values(self): self.assertEqual( resource.attributes, { - resources.SERVICE_NAME: "test", + SERVICE_NAME: "test", }, ) self.assertEqual(len(resource.attributes), 1) def test_aggregated_resources_no_detectors(self): - aggregated_resources = resources.get_aggregated_resources([]) + aggregated_resources = get_aggregated_resources([]) self.assertEqual( aggregated_resources, - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) def test_aggregated_resources_with_default_destroying_static_resource( self, ): - static_resource = resources.Resource({"static_key": "static_value"}) + static_resource = Resource({"static_key": "static_value"}) self.assertEqual( - resources.get_aggregated_resources( - [], initial_resource=static_resource - ), + get_aggregated_resources([], initial_resource=static_resource), static_resource, ) - resource_detector = mock.Mock(spec=resources.ResourceDetector) - resource_detector.detect.return_value = resources.Resource( + resource_detector = Mock(spec=ResourceDetector) + resource_detector.detect.return_value = Resource( {"static_key": "try_to_overwrite_existing_value", "key": "value"} ) self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector], initial_resource=static_resource ), - resources.Resource( + Resource( { "static_key": "try_to_overwrite_existing_value", "key": "value", @@ -269,16 +273,14 @@ def test_aggregated_resources_with_default_destroying_static_resource( ) def test_aggregated_resources_multiple_detectors(self): - resource_detector1 = mock.Mock(spec=resources.ResourceDetector) - resource_detector1.detect.return_value = resources.Resource( - {"key1": "value1"} - ) - resource_detector2 = mock.Mock(spec=resources.ResourceDetector) - resource_detector2.detect.return_value = resources.Resource( + resource_detector1 = Mock(spec=ResourceDetector) + resource_detector1.detect.return_value = Resource({"key1": "value1"}) + resource_detector2 = Mock(spec=ResourceDetector) + resource_detector2.detect.return_value = Resource( {"key2": "value2", "key3": "value3"} ) - resource_detector3 = mock.Mock(spec=resources.ResourceDetector) - resource_detector3.detect.return_value = resources.Resource( + resource_detector3 = Mock(spec=ResourceDetector) + resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -287,15 +289,13 @@ def test_aggregated_resources_multiple_detectors(self): ) self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector1, resource_detector2, resource_detector3] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", @@ -307,16 +307,16 @@ def test_aggregated_resources_multiple_detectors(self): ) def test_aggregated_resources_different_schema_urls(self): - resource_detector1 = mock.Mock(spec=resources.ResourceDetector) - resource_detector1.detect.return_value = resources.Resource( + resource_detector1 = Mock(spec=ResourceDetector) + resource_detector1.detect.return_value = Resource( {"key1": "value1"}, "" ) - resource_detector2 = mock.Mock(spec=resources.ResourceDetector) - resource_detector2.detect.return_value = resources.Resource( + resource_detector2 = Mock(spec=ResourceDetector) + resource_detector2.detect.return_value = Resource( {"key2": "value2", "key3": "value3"}, "url1" ) - resource_detector3 = mock.Mock(spec=resources.ResourceDetector) - resource_detector3.detect.return_value = resources.Resource( + resource_detector3 = Mock(spec=ResourceDetector) + resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -324,8 +324,8 @@ def test_aggregated_resources_different_schema_urls(self): }, "url2", ) - resource_detector4 = mock.Mock(spec=resources.ResourceDetector) - resource_detector4.detect.return_value = resources.Resource( + resource_detector4 = Mock(spec=ResourceDetector) + resource_detector4.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", @@ -334,15 +334,11 @@ def test_aggregated_resources_different_schema_urls(self): "url1", ) self.assertEqual( - resources.get_aggregated_resources( - [resource_detector1, resource_detector2] - ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + get_aggregated_resources([resource_detector1, resource_detector2]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( {"key1": "value1", "key2": "value2", "key3": "value3"}, "url1", ) @@ -350,24 +346,20 @@ def test_aggregated_resources_different_schema_urls(self): ) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [resource_detector2, resource_detector3] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( - {"key2": "value2", "key3": "value3"}, "url1" - ) + Resource({"key2": "value2", "key3": "value3"}, "url1") ), ) self.assertIn("url1", log_entry.output[0]) self.assertIn("url2", log_entry.output[0]) with self.assertLogs(level=ERROR): self.assertEqual( - resources.get_aggregated_resources( + get_aggregated_resources( [ resource_detector2, resource_detector3, @@ -375,12 +367,10 @@ def test_aggregated_resources_different_schema_urls(self): resource_detector1, ] ), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ).merge( - resources.Resource( + Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", @@ -395,116 +385,106 @@ def test_aggregated_resources_different_schema_urls(self): self.assertIn("url2", log_entry.output[0]) def test_resource_detector_ignore_error(self): - resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = False self.assertEqual( - resources.get_aggregated_resources([resource_detector]), - resources._DEFAULT_RESOURCE.merge( - resources.Resource( - {resources.SERVICE_NAME: "unknown_service"}, "" - ) + get_aggregated_resources([resource_detector]), + _DEFAULT_RESOURCE.merge( + Resource({SERVICE_NAME: "unknown_service"}, "") ), ) def test_resource_detector_raise_error(self): - resource_detector = mock.Mock(spec=resources.ResourceDetector) + resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = True self.assertRaises( - Exception, resources.get_aggregated_resources, [resource_detector] + Exception, get_aggregated_resources, [resource_detector] ) - @mock.patch.dict( - os.environ, + @patch.dict( + environ, {"OTEL_RESOURCE_ATTRIBUTES": "key1=env_value1,key2=env_value2"}, ) def test_env_priority(self): - resource_env = resources.Resource.create() + resource_env = Resource.create() self.assertEqual(resource_env.attributes["key1"], "env_value1") self.assertEqual(resource_env.attributes["key2"], "env_value2") - resource_env_override = resources.Resource.create( + resource_env_override = Resource.create( {"key1": "value1", "key2": "value2"} ) self.assertEqual(resource_env_override.attributes["key1"], "value1") self.assertEqual(resource_env_override.attributes["key2"], "value2") - @mock.patch.dict( - os.environ, + @patch.dict( + environ, { - resources.OTEL_SERVICE_NAME: "test-srv-name", - resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", + OTEL_SERVICE_NAME: "test-srv-name", + OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", }, ) def test_service_name_env(self): - resource = resources.Resource.create() + resource = Resource.create() self.assertEqual(resource.attributes["service.name"], "test-srv-name") - resource = resources.Resource.create({"service.name": "from-code"}) + resource = Resource.create({"service.name": "from-code"}) self.assertEqual(resource.attributes["service.name"], "from-code") class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" + environ[OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: - os.environ.pop(resources.OTEL_RESOURCE_ATTRIBUTES) + environ.pop(OTEL_RESOURCE_ATTRIBUTES) def test_empty(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "" - self.assertEqual(detector.detect(), resources.Resource.get_empty()) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "" + self.assertEqual(detector.detect(), Resource.get_empty()) def test_one(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v" - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v" + self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_one_with_whitespace(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = " k = v " - self.assertEqual(detector.detect(), resources.Resource({"k": "v"})) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v " + self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_multiple(self): - detector = resources.OTELResourceDetector() - os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" + self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_multiple_with_whitespace(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES - ] = " k = v , k2 = v2 " - self.assertEqual( - detector.detect(), resources.Resource({"k": "v", "k2": "v2"}) - ) + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v , k2 = v2 " + self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_invalid_key_value_pairs(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES - ] = "k=v,k2=v2,invalid,,foo=bar=baz," + detector = OTELResourceDetector() + environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2,invalid,,foo=bar=baz," self.assertEqual( detector.detect(), - resources.Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), + Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), ) def test_multiple_with_url_decode(self): - detector = resources.OTELResourceDetector() - os.environ[ - resources.OTEL_RESOURCE_ATTRIBUTES + detector = OTELResourceDetector() + environ[ + OTEL_RESOURCE_ATTRIBUTES ] = "key=value%20test%0A, key2=value+%202" self.assertEqual( detector.detect(), - resources.Resource({"key": "value test\n", "key2": "value+ 2"}), + Resource({"key": "value test\n", "key2": "value+ 2"}), ) self.assertEqual( detector.detect(), - resources.Resource( + Resource( { "key": parse.unquote("value%20test%0A"), "key2": parse.unquote("value+%202"), @@ -512,46 +492,107 @@ def test_multiple_with_url_decode(self): ), ) - @mock.patch.dict( - os.environ, - {resources.OTEL_SERVICE_NAME: "test-srv-name"}, + @patch.dict( + environ, + {OTEL_SERVICE_NAME: "test-srv-name"}, ) def test_service_name_env(self): - detector = resources.OTELResourceDetector() + detector = OTELResourceDetector() self.assertEqual( detector.detect(), - resources.Resource({"service.name": "test-srv-name"}), + Resource({"service.name": "test-srv-name"}), ) - @mock.patch.dict( - os.environ, + @patch.dict( + environ, { - resources.OTEL_SERVICE_NAME: "from-service-name", - resources.OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", + OTEL_SERVICE_NAME: "from-service-name", + OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", }, ) def test_service_name_env_precedence(self): - detector = resources.OTELResourceDetector() + detector = OTELResourceDetector() self.assertEqual( detector.detect(), - resources.Resource({"service.name": "from-service-name"}), + Resource({"service.name": "from-service-name"}), ) def test_process_detector(self): - initial_resource = resources.Resource({"foo": "bar"}) - aggregated_resource = resources.get_aggregated_resources( - [resources.ProcessResourceDetector()], initial_resource + initial_resource = Resource({"foo": "bar"}) + aggregated_resource = get_aggregated_resources( + [ProcessResourceDetector()], initial_resource ) self.assertIn( - resources.PROCESS_RUNTIME_NAME, + PROCESS_RUNTIME_NAME, aggregated_resource.attributes.keys(), ) self.assertIn( - resources.PROCESS_RUNTIME_DESCRIPTION, + PROCESS_RUNTIME_DESCRIPTION, aggregated_resource.attributes.keys(), ) self.assertIn( - resources.PROCESS_RUNTIME_VERSION, + PROCESS_RUNTIME_VERSION, aggregated_resource.attributes.keys(), ) + + def test_resource_detector_entry_points_default(self): + + resource = Resource({}).create() + + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.schema_url, "") + + resource = Resource({}).create({"a": "b", "c": "d"}) + + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.attributes["c"], "d") + self.assertEqual(resource.schema_url, "") + + @patch.dict(environ, {_OTEL_RESOURCE_DETECTORS: "mock"}, clear=True) + @patch( + "opentelemetry.sdk.resources.entry_points", + Mock( + return_value=[ + Mock( + **{ + "load.return_value": Mock( + return_value=Mock( + **{"detect.return_value": Resource({"a": "b"})} + ) + ) + } + ) + ] + ), + ) + def test_resource_detector_entry_points_non_default(self): + resource = Resource({}).create() + self.assertEqual( + resource.attributes["telemetry.sdk.language"], "python" + ) + self.assertEqual( + resource.attributes["telemetry.sdk.name"], "opentelemetry" + ) + self.assertEqual( + resource.attributes["service.name"], "unknown_service" + ) + self.assertEqual(resource.attributes["a"], "b") + self.assertEqual(resource.schema_url, "")