From f8758c6902ed725864ff677f739829aa6bce2078 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 16 May 2024 14:05:21 -0700 Subject: [PATCH] Implement functions resource detector (#2523) * Update .pylintrc * fn * Update CHANGELOG.md * commments * Add deployment.environment to functions detector * Revert "Add deployment.environment to functions detector" This reverts commit 5411759711b8bc9976705deb416d5ffd8f65590f. * Remove deployment.environment from readme * Release 0.1.5 --------- Co-authored-by: jeremydvoss --- .../CHANGELOG.md | 4 +- .../README.rst | 12 +- .../pyproject.toml | 1 + .../resource/detector/azure/__init__.py | 2 + .../resource/detector/azure/_constants.py | 6 + .../resource/detector/azure/_utils.py | 27 ++++- .../resource/detector/azure/app_service.py | 32 ++--- .../resource/detector/azure/functions.py | 68 +++++++++++ .../resource/detector/azure/version.py | 2 +- .../tests/test_app_service.py | 39 +++++++ .../tests/test_functions.py | 110 ++++++++++++++++++ 11 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py create mode 100644 resource/opentelemetry-resource-detector-azure/tests/test_functions.py diff --git a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md index 8954fc5359..f77fce18f1 100644 --- a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md +++ b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md @@ -5,10 +5,12 @@ All notable changes to this project will be documented in this file. 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 +## Version 0.1.5 (2024-05-16) - Ignore vm detector if already in other rps ([#2456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2456)) +- Implement functions resource detector + ([#2523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2523)) ## Version 0.1.4 (2024-04-05) diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index 6a376534ad..baf2dddbbe 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -60,7 +60,17 @@ The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.instance.id`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``azure.app.service.stamp`` set to the value of the ``WEBSITE_HOME_STAMPNAME`` environment variable. -The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: + The Azure Functions Resource Detector sets the following Resource Attributes: + * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. + * ``process.id`` set to the process ID collected from the running process. + * ``cloud.platform`` set to ``azure_functions``. + * ``cloud.provider`` set to ``azure``. + * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. + * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. + * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. + * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. + +The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: * ``azure.vm.scaleset.name`` set to the value of the ``vmScaleSetName`` field. * ``azure.vm.sku`` set to the value of the ``sku`` field. * ``cloud.platform`` set to the value of the ``azure_vm``. diff --git a/resource/opentelemetry-resource-detector-azure/pyproject.toml b/resource/opentelemetry-resource-detector-azure/pyproject.toml index 72260709f9..efa1b24ee7 100644 --- a/resource/opentelemetry-resource-detector-azure/pyproject.toml +++ b/resource/opentelemetry-resource-detector-azure/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.entry-points.opentelemetry_resource_detector] azure_app_service = "opentelemetry.resource.detector.azure.app_service:AzureAppServiceResourceDetector" +azure_functions = "opentelemetry.resource.detector.azure.functions:AzureFunctionsResourceDetector" azure_vm = "opentelemetry.resource.detector.azure.vm:AzureVMResourceDetector" [project.urls] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py index 913b677c3e..628a8ab781 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py @@ -15,11 +15,13 @@ # pylint: disable=import-error from .app_service import AzureAppServiceResourceDetector +from .functions import AzureFunctionsResourceDetector from .version import __version__ from .vm import AzureVMResourceDetector __all__ = [ "AzureAppServiceResourceDetector", + "AzureFunctionsResourceDetector", "AzureVMResourceDetector", "__version__", ] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py index dddc6632ac..3a6415e0d5 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py @@ -43,6 +43,12 @@ # Functions _FUNCTIONS_WORKER_RUNTIME = "FUNCTIONS_WORKER_RUNTIME" +_WEBSITE_MEMORY_LIMIT_MB = "WEBSITE_MEMORY_LIMIT_MB" + +_FUNCTIONS_ATTRIBUTE_ENV_VARS = { + ResourceAttributes.FAAS_INSTANCE: _WEBSITE_INSTANCE_ID, + ResourceAttributes.FAAS_MAX_MEMORY: _WEBSITE_MEMORY_LIMIT_MB, +} # Vm diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py index 3f73613945..62d00c5a6c 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py @@ -11,27 +11,44 @@ # 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 +from os import environ +from typing import Optional from ._constants import ( _AKS_ARM_NAMESPACE_ID, _FUNCTIONS_WORKER_RUNTIME, + _WEBSITE_OWNER_NAME, + _WEBSITE_RESOURCE_GROUP, _WEBSITE_SITE_NAME, ) def _is_on_aks() -> bool: - return os.environ.get(_AKS_ARM_NAMESPACE_ID) is not None + return environ.get(_AKS_ARM_NAMESPACE_ID) is not None def _is_on_app_service() -> bool: - return os.environ.get(_WEBSITE_SITE_NAME) is not None + return environ.get(_WEBSITE_SITE_NAME) is not None def _is_on_functions() -> bool: - return os.environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None + return environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None def _can_ignore_vm_detect() -> bool: return _is_on_aks() or _is_on_app_service() or _is_on_functions() + + +def _get_azure_resource_uri() -> Optional[str]: + website_site_name = environ.get(_WEBSITE_SITE_NAME) + website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) + website_owner_name = environ.get(_WEBSITE_OWNER_NAME) + + subscription_id = website_owner_name + if website_owner_name and "+" in website_owner_name: + subscription_id = website_owner_name[0 : website_owner_name.index("+")] + + if not (website_site_name and website_resource_group and subscription_id): + return None + + return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py index 613d8f9410..41371b8eec 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from os import environ from opentelemetry.sdk.resources import Resource, ResourceDetector @@ -20,29 +21,32 @@ CloudProviderValues, ResourceAttributes, ) +from opentelemetry.resource.detector.azure._utils import _get_azure_resource_uri from ._constants import ( _APP_SERVICE_ATTRIBUTE_ENV_VARS, - _WEBSITE_OWNER_NAME, - _WEBSITE_RESOURCE_GROUP, _WEBSITE_SITE_NAME, ) +from opentelemetry.resource.detector.azure._utils import _is_on_functions + class AzureAppServiceResourceDetector(ResourceDetector): def detect(self) -> Resource: attributes = {} website_site_name = environ.get(_WEBSITE_SITE_NAME) if website_site_name: - attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + # Functions resource detector takes priority with `service.name` and `cloud.platform` + if not _is_on_functions(): + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_APP_SERVICE.value + ) attributes[ResourceAttributes.CLOUD_PROVIDER] = ( CloudProviderValues.AZURE.value ) - attributes[ResourceAttributes.CLOUD_PLATFORM] = ( - CloudPlatformValues.AZURE_APP_SERVICE.value - ) - azure_resource_uri = _get_azure_resource_uri(website_site_name) + azure_resource_uri = _get_azure_resource_uri() if azure_resource_uri: attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( azure_resource_uri @@ -53,17 +57,3 @@ def detect(self) -> Resource: attributes[key] = value return Resource(attributes) - - -def _get_azure_resource_uri(website_site_name): - website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) - website_owner_name = environ.get(_WEBSITE_OWNER_NAME) - - subscription_id = website_owner_name - if website_owner_name and "+" in website_owner_name: - subscription_id = website_owner_name[0 : website_owner_name.index("+")] - - if not (website_resource_group and subscription_id): - return None - - return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py new file mode 100644 index 0000000000..0bf9a10f86 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py @@ -0,0 +1,68 @@ +# 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. + +from os import environ, getpid + +from opentelemetry.sdk.resources import Resource, ResourceDetector +from opentelemetry.semconv.resource import ( + CloudPlatformValues, + CloudProviderValues, + ResourceAttributes, +) + +from ._constants import ( + _FUNCTIONS_ATTRIBUTE_ENV_VARS, + _REGION_NAME, + _WEBSITE_SITE_NAME, +) +from opentelemetry.resource.detector.azure._utils import ( + _get_azure_resource_uri, + _is_on_functions, +) + + +class AzureFunctionsResourceDetector(ResourceDetector): + def detect(self) -> Resource: + attributes = {} + if _is_on_functions(): + website_site_name = environ.get(_WEBSITE_SITE_NAME) + if website_site_name: + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.PROCESS_PID] = getpid() + attributes[ResourceAttributes.CLOUD_PROVIDER] = ( + CloudProviderValues.AZURE.value + ) + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_FUNCTIONS.value + ) + cloud_region = environ.get(_REGION_NAME) + if cloud_region: + attributes[ResourceAttributes.CLOUD_REGION] = cloud_region + azure_resource_uri = _get_azure_resource_uri() + if azure_resource_uri: + attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( + azure_resource_uri + ) + for key, env_var in _FUNCTIONS_ATTRIBUTE_ENV_VARS.items(): + value = environ.get(env_var) + if value: + if key == ResourceAttributes.FAAS_MAX_MEMORY: + try: + value = int(value) + except ValueError: + continue + attributes[key] = value + + return Resource(attributes) + diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py index f961659f70..fac29d773f 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.4" +__version__ = "0.1.5" diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py index c5d2396dab..6c3d395994 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py @@ -68,6 +68,45 @@ def test_on_app_service(self): self.assertEqual( attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME ) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, + "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + }, + clear=True, + ) + def test_on_app_service_with_functions(self): + resource = AzureAppServiceResourceDetector().detect() + attributes = resource.attributes + self.assertIsNone(attributes.get("service.name")) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertIsNone(attributes.get("cloud.platform")) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual( + attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME + ) + self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) + self.assertEqual( + attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID + ) + self.assertEqual( + attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME + ) @patch.dict( "os.environ", diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_functions.py b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py new file mode 100644 index 0000000000..1f5354c500 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py @@ -0,0 +1,110 @@ +# 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 unittest +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.resource.detector.azure.functions import ( + AzureFunctionsResourceDetector, +) + +TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME" +TEST_REGION_NAME = "TEST_REGION_NAME" +TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID" + +TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP" +TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME" +TEST_WEBSITE_MEMORY_LIMIT_MB = "1024" + + +class TestAzureAppServiceResourceDetector(unittest.TestCase): + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertEqual(attributes["faas.max_memory"], 1024) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": "error", + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions_error_memory(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertIsNone(attributes.get("faas.max_memory")) + + @patch.dict( + "os.environ", + { + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + def test_off_app_service(self): + resource = AzureFunctionsResourceDetector().detect() + self.assertEqual(resource.attributes, {})