From a08b59dda08f17c0085bf4444b14b1a4ee30e2d5 Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:22:50 -0700 Subject: [PATCH 1/7] Add Azure Kubernetes Service resource detector Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../README.rst | 45 +++- .../pyproject.toml | 1 + .../resource/detector/azure/__init__.py | 2 + .../resource/detector/azure/_constants.py | 3 + .../resource/detector/azure/_utils.py | 9 +- .../resource/detector/azure/aks.py | 92 ++++++++ .../tests/test_aks.py | 201 ++++++++++++++++++ 7 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py create mode 100644 resource/opentelemetry-resource-detector-azure/tests/test_aks.py diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index 19a0f97f32..05064a2b13 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -7,6 +7,7 @@ OpenTelemetry Resource detectors for Azure :target: https://pypi.org/project/opentelemetry-resource-detector-azure/ This library contains OpenTelemetry `Resource Detectors `_ for the following Azure resources: + * `Azure Kubernetes Service `_ * `Azure App Service `_ * `Azure Virtual Machines `_ * `Azure Functions (Experimental) `_ @@ -26,13 +27,11 @@ Usage example for ``opentelemetry-resource-detector-azure`` from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.resource.detector.azure.app_service import ( + from opentelemetry.resource.detector.azure import ( + AzureAKSResourceDetector, AzureAppServiceResourceDetector, AzureVMResourceDetector, ) - from opentelemetry.resource.detector.azure.vm import ( - AzureVMResourceDetector, - ) from opentelemetry.sdk.resources import get_aggregated_resources @@ -40,6 +39,7 @@ Usage example for ``opentelemetry-resource-detector-azure`` TracerProvider( resource=get_aggregated_resources( [ + AzureAKSResourceDetector(), AzureAppServiceResourceDetector(), AzureVMResourceDetector(), ] @@ -50,6 +50,43 @@ Usage example for ``opentelemetry-resource-detector-azure`` Mappings -------- +The Azure Kubernetes Service Resource Detector reads the cluster resource ID from the +``CLUSTER_RESOURCE_ID`` environment variable or from a mounted ``aks-cluster-metadata`` +ConfigMap at ``/etc/kubernetes/aks-cluster-metadata``. It sets the following Resource +Attributes: + * ``cloud.platform`` set to ``azure_aks``. + * ``cloud.provider`` set to ``azure``. + * ``cloud.resource_id`` set to the full Azure Resource Manager cluster resource ID. + * ``k8s.cluster.name`` set to the cluster name extracted from the resource ID. + +The native AKS ConfigMap is named ``aks-cluster-metadata`` and contains a +``clusterResourceId`` key. It can be exposed to a pod as an environment variable: + +.. code-block:: yaml + + env: + - name: CLUSTER_RESOURCE_ID + valueFrom: + configMapKeyRef: + name: aks-cluster-metadata + key: clusterResourceId + +Alternatively, mount the ConfigMap as a volume: + +.. code-block:: yaml + + volumes: + - name: aks-cluster-metadata + configMap: + name: aks-cluster-metadata + volumeMounts: + - name: aks-cluster-metadata + mountPath: /etc/kubernetes/aks-cluster-metadata + +Kubernetes resolves ConfigMap references within the pod's namespace. Because the native +ConfigMap is in ``kube-public``, copy it into the workload namespace or use tooling such +as an init container to expose its value through one of the supported locations. + The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. * ``cloud.platform`` set to ``azure_app_service``. diff --git a/resource/opentelemetry-resource-detector-azure/pyproject.toml b/resource/opentelemetry-resource-detector-azure/pyproject.toml index 214b2f7c1c..226e124945 100644 --- a/resource/opentelemetry-resource-detector-azure/pyproject.toml +++ b/resource/opentelemetry-resource-detector-azure/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ ] [project.entry-points.opentelemetry_resource_detector] +azure_aks = "opentelemetry.resource.detector.azure.aks:AzureAKSResourceDetector" 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" 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 56968843e5..08534b0439 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 @@ -3,12 +3,14 @@ # pylint: disable=import-error +from .aks import AzureAKSResourceDetector from .app_service import AzureAppServiceResourceDetector from .functions import AzureFunctionsResourceDetector from .version import __version__ from .vm import AzureVMResourceDetector __all__ = [ + "AzureAKSResourceDetector", "AzureAppServiceResourceDetector", "AzureFunctionsResourceDetector", "AzureVMResourceDetector", 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 478ca97a24..35239735a7 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 @@ -8,6 +8,9 @@ # Azure Kubernetes _AKS_ARM_NAMESPACE_ID = "AKS_ARM_NAMESPACE_ID" +_AKS_CLUSTER_RESOURCE_ID = "CLUSTER_RESOURCE_ID" +_AKS_CLUSTER_RESOURCE_ID_KEY = "clusterResourceId" +_AKS_METADATA_FILE_PATH = "/etc/kubernetes/aks-cluster-metadata" # AppService 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 0043d03a78..eb3bbff633 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 @@ -1,10 +1,13 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import environ +from pathlib import Path from typing import Optional from ._constants import ( _AKS_ARM_NAMESPACE_ID, + _AKS_CLUSTER_RESOURCE_ID, + _AKS_METADATA_FILE_PATH, _FUNCTIONS_WORKER_RUNTIME, _WEBSITE_OWNER_NAME, _WEBSITE_RESOURCE_GROUP, @@ -13,7 +16,11 @@ def _is_on_aks() -> bool: - return environ.get(_AKS_ARM_NAMESPACE_ID) is not None + return ( + environ.get(_AKS_ARM_NAMESPACE_ID) is not None + or environ.get(_AKS_CLUSTER_RESOURCE_ID) is not None + or Path(_AKS_METADATA_FILE_PATH).exists() + ) def _is_on_app_service() -> bool: diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py new file mode 100644 index 0000000000..2c56f9b865 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py @@ -0,0 +1,92 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from logging import getLogger +from os import environ +from pathlib import Path +from typing import Optional + +from opentelemetry.sdk.resources import Resource, ResourceDetector +from opentelemetry.semconv.resource import ( + CloudPlatformValues, + CloudProviderValues, + ResourceAttributes, +) + +from ._constants import ( + _AKS_CLUSTER_RESOURCE_ID, + _AKS_CLUSTER_RESOURCE_ID_KEY, + _AKS_METADATA_FILE_PATH, +) + +_logger = getLogger(__name__) + + +def _extract_cluster_name(resource_id: str) -> Optional[str]: + segments = resource_id.split("/") + for index, segment in enumerate(segments): + if segment.lower() == "managedclusters" and index < len(segments) - 1: + return segments[index + 1] + return segments[-1] or None + + +def _parse_aks_metadata(content: str) -> Optional[str]: + keyed_resource_id: Optional[str] = None + bare_values: list[str] = [] + + for line in content.splitlines(): + stripped_line = line.strip().lstrip("\ufeff") + if not stripped_line or stripped_line.startswith("#"): + continue + + key, separator, value = stripped_line.partition("=") + if not separator: + bare_values.append(stripped_line) + elif key.strip() == _AKS_CLUSTER_RESOURCE_ID_KEY and value.strip(): + keyed_resource_id = value.strip() + + if keyed_resource_id: + return keyed_resource_id + if len(bare_values) == 1: + return bare_values[0] + return None + + +def _get_aks_metadata_from_file() -> Optional[str]: + metadata_path = Path(_AKS_METADATA_FILE_PATH) + try: + if metadata_path.is_dir(): + metadata_path = metadata_path / _AKS_CLUSTER_RESOURCE_ID_KEY + content = metadata_path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + _logger.debug( + "Failed to read AKS metadata from %s", + metadata_path, + exc_info=True, + ) + return None + + return _parse_aks_metadata(content) + + +class AzureAKSResourceDetector(ResourceDetector): + def detect(self) -> Resource: + resource_id = ( + environ.get(_AKS_CLUSTER_RESOURCE_ID) + or _get_aks_metadata_from_file() + ) + if not resource_id: + return Resource({}) + + attributes = { + ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AZURE.value, + ResourceAttributes.CLOUD_PLATFORM: ( + CloudPlatformValues.AZURE_AKS.value + ), + ResourceAttributes.CLOUD_RESOURCE_ID: resource_id, + } + cluster_name = _extract_cluster_name(resource_id) + if cluster_name: + attributes[ResourceAttributes.K8S_CLUSTER_NAME] = cluster_name + + return Resource(attributes) diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py new file mode 100644 index 0000000000..7355a63575 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py @@ -0,0 +1,201 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Mapping +from unittest.mock import patch + +from opentelemetry.resource.detector.azure._utils import _is_on_aks +from opentelemetry.resource.detector.azure.aks import ( + AzureAKSResourceDetector, +) +from opentelemetry.resource.detector.azure.vm import AzureVMResourceDetector + +TEST_RESOURCE_ID = ( + "/subscriptions/test-sub/resourceGroups/test-rg/providers/" + "Microsoft.ContainerService/managedClusters/test-aks-cluster" +) + + +class TestAzureAKSResourceDetector(unittest.TestCase): + @patch.dict( + "os.environ", {"CLUSTER_RESOURCE_ID": TEST_RESOURCE_ID}, clear=True + ) + def test_detects_aks_from_environment(self) -> None: + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_aks") + self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") + self.assertIsInstance(attributes["cloud.provider"], str) + self.assertIsInstance(attributes["cloud.platform"], str) + self.assertIsInstance(attributes["cloud.resource_id"], str) + self.assertIsInstance(attributes["k8s.cluster.name"], str) + + @patch.dict( + "os.environ", + { + "CLUSTER_RESOURCE_ID": ( + "/subscriptions/test-sub/resourceGroups/test-rg/providers/" + "Microsoft.ContainerService/ManagedClusters/my-cluster" + ) + }, + clear=True, + ) + def test_cluster_name_resource_type_is_case_insensitive(self) -> None: + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes["k8s.cluster.name"], "my-cluster") + + @patch.dict( + "os.environ", {"CLUSTER_RESOURCE_ID": "standalone-name"}, clear=True + ) + def test_cluster_name_falls_back_to_last_segment(self) -> None: + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes["k8s.cluster.name"], "standalone-name") + + @patch.dict("os.environ", {}, clear=True) + @patch( + "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", + "/missing/aks-cluster-metadata", + ) + def test_returns_empty_resource_outside_aks(self) -> None: + resource = AzureAKSResourceDetector().detect() + + self.assertEqual(resource.attributes, {}) + + @patch.dict("os.environ", {}, clear=True) + def test_detects_aks_from_configmap_volume(self) -> None: + with TemporaryDirectory() as directory: + metadata_path = Path(directory) / "aks-cluster-metadata" + metadata_path.mkdir() + (metadata_path / "clusterResourceId").write_text( + f"{TEST_RESOURCE_ID}\n", encoding="utf-8" + ) + + with patch( + "opentelemetry.resource.detector.azure.aks." + "_AKS_METADATA_FILE_PATH", + str(metadata_path), + ): + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") + + @patch.dict("os.environ", {}, clear=True) + def test_detects_aks_from_subpath_mount(self) -> None: + with TemporaryDirectory() as directory: + metadata_path = Path(directory) / "aks-cluster-metadata" + metadata_path.write_text(f"{TEST_RESOURCE_ID}\n", encoding="utf-8") + + with patch( + "opentelemetry.resource.detector.azure.aks." + "_AKS_METADATA_FILE_PATH", + str(metadata_path), + ): + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + + @patch.dict("os.environ", {}, clear=True) + def test_detects_aks_from_key_value_file(self) -> None: + content = ( + f"\ufeff# AKS metadata\r\nclusterResourceId={TEST_RESOURCE_ID}\r\n" + ) + + attributes = self._detect_from_file(content) + + self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + + @patch.dict("os.environ", {}, clear=True) + def test_explicit_key_wins_over_bare_lines(self) -> None: + content = ( + f"clusterResourceId={TEST_RESOURCE_ID}\n" + "stray-token\n" + "// not a supported comment\n" + ) + + attributes = self._detect_from_file(content) + + self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + + @patch.dict("os.environ", {}, clear=True) + def test_ignores_ambiguous_bare_values(self) -> None: + attributes = self._detect_from_file( + f"{TEST_RESOURCE_ID}\nstray-token\n" + ) + + self.assertEqual(attributes, {}) + + @patch.dict("os.environ", {}, clear=True) + def test_ignores_configmap_volume_without_resource_id(self) -> None: + with TemporaryDirectory() as directory: + metadata_path = Path(directory) / "aks-cluster-metadata" + metadata_path.mkdir() + (metadata_path / "somethingElse").write_text( + "value\n", encoding="utf-8" + ) + + with patch( + "opentelemetry.resource.detector.azure.aks." + "_AKS_METADATA_FILE_PATH", + str(metadata_path), + ): + attributes = AzureAKSResourceDetector().detect().attributes + + self.assertEqual(attributes, {}) + + @patch.dict( + "os.environ", + { + "CLUSTER_RESOURCE_ID": ( + "/subscriptions/test-sub/resourceGroups/test-rg/providers/" + "Microsoft.ContainerService/managedClusters/from-env" + ) + }, + clear=True, + ) + def test_environment_takes_precedence_over_file(self) -> None: + attributes = self._detect_from_file(TEST_RESOURCE_ID) + + self.assertEqual(attributes["k8s.cluster.name"], "from-env") + + @patch.dict( + "os.environ", {"CLUSTER_RESOURCE_ID": TEST_RESOURCE_ID}, clear=True + ) + @patch("opentelemetry.resource.detector.azure.vm.urlopen") + def test_vm_detection_is_skipped_on_aks(self, mock_urlopen) -> None: + resource = AzureVMResourceDetector().detect() + + self.assertEqual(resource.attributes, {}) + mock_urlopen.assert_not_called() + + @patch.dict("os.environ", {}, clear=True) + def test_mounted_metadata_marks_environment_as_aks(self) -> None: + with TemporaryDirectory() as directory: + metadata_path = Path(directory) / "aks-cluster-metadata" + metadata_path.write_text(TEST_RESOURCE_ID, encoding="utf-8") + + with patch( + "opentelemetry.resource.detector.azure._utils." + "_AKS_METADATA_FILE_PATH", + str(metadata_path), + ): + self.assertTrue(_is_on_aks()) + + @staticmethod + def _detect_from_file(content: str) -> Mapping[str, object]: + with TemporaryDirectory() as directory: + metadata_path = Path(directory) / "aks-cluster-metadata" + metadata_path.write_text(content, encoding="utf-8") + with patch( + "opentelemetry.resource.detector.azure.aks." + "_AKS_METADATA_FILE_PATH", + str(metadata_path), + ): + return AzureAKSResourceDetector().detect().attributes From e0bd961af2d591c6f9b02014cc1b139a48955007 Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:23:28 -0700 Subject: [PATCH 2/7] Add changelog fragment for AKS detector Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../opentelemetry-resource-detector-azure/.changelog/5012.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 resource/opentelemetry-resource-detector-azure/.changelog/5012.added diff --git a/resource/opentelemetry-resource-detector-azure/.changelog/5012.added b/resource/opentelemetry-resource-detector-azure/.changelog/5012.added new file mode 100644 index 0000000000..81646afd4d --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/.changelog/5012.added @@ -0,0 +1 @@ +`opentelemetry-resource-detector-azure`: add AKS resource detector From beca0daeb5cdf82ec8e67c5f49e7bdfcb49ad497 Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:35:37 -0700 Subject: [PATCH 3/7] Fix AKS detector CI checks Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .codespellrc | 2 +- resource/opentelemetry-resource-detector-azure/README.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index 9087e5816c..2a554b4841 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] # skipping auto generated folders skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,*/cassettes -ignore-words-list = ot +ignore-words-list = aks,ot diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index 05064a2b13..fc1efaba49 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -54,6 +54,7 @@ The Azure Kubernetes Service Resource Detector reads the cluster resource ID fro ``CLUSTER_RESOURCE_ID`` environment variable or from a mounted ``aks-cluster-metadata`` ConfigMap at ``/etc/kubernetes/aks-cluster-metadata``. It sets the following Resource Attributes: + * ``cloud.platform`` set to ``azure_aks``. * ``cloud.provider`` set to ``azure``. * ``cloud.resource_id`` set to the full Azure Resource Manager cluster resource ID. From ce3182a008fde533295e4872dad690207f08e83c Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:43:50 -0700 Subject: [PATCH 4/7] Use current AKS semantic conventions Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f134c07d-5aff-4dd1-bb7f-c6fd38ee8eed --- .../resource/detector/azure/aks.py | 18 +++++++++--------- .../tests/test_aks.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py index 5e2d43abad..7e57842ce3 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py @@ -6,11 +6,6 @@ from pathlib import Path from opentelemetry.sdk.resources import Resource, ResourceDetector -from opentelemetry.semconv.resource import ( - CloudPlatformValues, - CloudProviderValues, - ResourceAttributes, -) from ._constants import ( _AKS_CLUSTER_RESOURCE_ID, @@ -20,6 +15,11 @@ _logger = getLogger(__name__) +_CLOUD_PLATFORM = "cloud.platform" +_CLOUD_PROVIDER = "cloud.provider" +_CLOUD_RESOURCE_ID = "cloud.resource_id" +_K8S_CLUSTER_NAME = "k8s.cluster.name" + def _extract_cluster_name(resource_id: str) -> str | None: segments = resource_id.split("/") @@ -75,12 +75,12 @@ def detect(self) -> Resource: return Resource({}) attributes = { - ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AZURE.value, - ResourceAttributes.CLOUD_PLATFORM: (CloudPlatformValues.AZURE_AKS.value), - ResourceAttributes.CLOUD_RESOURCE_ID: resource_id, + _CLOUD_PROVIDER: "azure", + _CLOUD_PLATFORM: "azure.aks", + _CLOUD_RESOURCE_ID: resource_id, } cluster_name = _extract_cluster_name(resource_id) if cluster_name: - attributes[ResourceAttributes.K8S_CLUSTER_NAME] = cluster_name + attributes[_K8S_CLUSTER_NAME] = cluster_name return Resource(attributes) diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py index cc1110f30c..71c10569cb 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py @@ -25,7 +25,7 @@ def test_detects_aks_from_environment(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.provider"], "azure") - self.assertEqual(attributes["cloud.platform"], "azure_aks") + self.assertEqual(attributes["cloud.platform"], "azure.aks") self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") self.assertIsInstance(attributes["cloud.provider"], str) From 7a6870e99d14e5c0d634ea0fa13e505cadb3c74a Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:24:48 -0700 Subject: [PATCH 5/7] Add AKS cloud account ID Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f134c07d-5aff-4dd1-bb7f-c6fd38ee8eed --- .../opentelemetry/resource/detector/azure/aks.py | 13 +++++++++++++ .../tests/test_aks.py | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py index 7e57842ce3..ffa9e4bede 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py @@ -15,12 +15,21 @@ _logger = getLogger(__name__) +_CLOUD_ACCOUNT_ID = "cloud.account.id" _CLOUD_PLATFORM = "cloud.platform" _CLOUD_PROVIDER = "cloud.provider" _CLOUD_RESOURCE_ID = "cloud.resource_id" _K8S_CLUSTER_NAME = "k8s.cluster.name" +def _extract_subscription_id(resource_id: str) -> str | None: + segments = resource_id.split("/") + for index, segment in enumerate(segments): + if segment.lower() == "subscriptions" and index < len(segments) - 1: + return segments[index + 1] or None + return None + + def _extract_cluster_name(resource_id: str) -> str | None: segments = resource_id.split("/") for index, segment in enumerate(segments): @@ -79,6 +88,10 @@ def detect(self) -> Resource: _CLOUD_PLATFORM: "azure.aks", _CLOUD_RESOURCE_ID: resource_id, } + subscription_id = _extract_subscription_id(resource_id) + if subscription_id: + attributes[_CLOUD_ACCOUNT_ID] = subscription_id + cluster_name = _extract_cluster_name(resource_id) if cluster_name: attributes[_K8S_CLUSTER_NAME] = cluster_name diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py index 71c10569cb..dbdf35fb85 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py @@ -27,31 +27,35 @@ def test_detects_aks_from_environment(self) -> None: self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure.aks") self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) + self.assertEqual(attributes["cloud.account.id"], "test-sub") self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") self.assertIsInstance(attributes["cloud.provider"], str) self.assertIsInstance(attributes["cloud.platform"], str) self.assertIsInstance(attributes["cloud.resource_id"], str) + self.assertIsInstance(attributes["cloud.account.id"], str) self.assertIsInstance(attributes["k8s.cluster.name"], str) @patch.dict( "os.environ", { "CLUSTER_RESOURCE_ID": ( - "/subscriptions/test-sub/resourceGroups/test-rg/providers/" + "/Subscriptions/test-sub/resourceGroups/test-rg/providers/" "Microsoft.ContainerService/ManagedClusters/my-cluster" ) }, clear=True, ) - def test_cluster_name_resource_type_is_case_insensitive(self) -> None: + def test_resource_id_segments_are_case_insensitive(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes + self.assertEqual(attributes["cloud.account.id"], "test-sub") self.assertEqual(attributes["k8s.cluster.name"], "my-cluster") @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": "standalone-name"}, clear=True) def test_cluster_name_falls_back_to_last_segment(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes + self.assertNotIn("cloud.account.id", attributes) self.assertEqual(attributes["k8s.cluster.name"], "standalone-name") @patch.dict("os.environ", {}, clear=True) From b4d222279349e656eb1aff1c23698c5f093a3356 Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:03:31 -0700 Subject: [PATCH 6/7] Stop reporting AKS cluster name Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../README.rst | 1 - .../resource/detector/azure/aks.py | 25 +++++---------- .../tests/test_aks.py | 31 ++++--------------- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index fc1efaba49..0515c6d5f3 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -58,7 +58,6 @@ Attributes: * ``cloud.platform`` set to ``azure_aks``. * ``cloud.provider`` set to ``azure``. * ``cloud.resource_id`` set to the full Azure Resource Manager cluster resource ID. - * ``k8s.cluster.name`` set to the cluster name extracted from the resource ID. The native AKS ConfigMap is named ``aks-cluster-metadata`` and contains a ``clusterResourceId`` key. It can be exposed to a pod as an environment variable: diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py index 5e2d43abad..4b0c78f818 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/aks.py @@ -21,14 +21,6 @@ _logger = getLogger(__name__) -def _extract_cluster_name(resource_id: str) -> str | None: - segments = resource_id.split("/") - for index, segment in enumerate(segments): - if segment.lower() == "managedclusters" and index < len(segments) - 1: - return segments[index + 1] - return segments[-1] or None - - def _parse_aks_metadata(content: str) -> str | None: keyed_resource_id: str | None = None bare_values: list[str] = [] @@ -74,13 +66,10 @@ def detect(self) -> Resource: if not resource_id: return Resource({}) - attributes = { - ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AZURE.value, - ResourceAttributes.CLOUD_PLATFORM: (CloudPlatformValues.AZURE_AKS.value), - ResourceAttributes.CLOUD_RESOURCE_ID: resource_id, - } - cluster_name = _extract_cluster_name(resource_id) - if cluster_name: - attributes[ResourceAttributes.K8S_CLUSTER_NAME] = cluster_name - - return Resource(attributes) + return Resource( + { + ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AZURE.value, + ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AZURE_AKS.value, + ResourceAttributes.CLOUD_RESOURCE_ID: resource_id, + } + ) diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py index cc1110f30c..78dba556e0 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_aks.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_aks.py @@ -27,32 +27,10 @@ def test_detects_aks_from_environment(self) -> None: self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_aks") self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) - self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") + self.assertNotIn("k8s.cluster.name", attributes) self.assertIsInstance(attributes["cloud.provider"], str) self.assertIsInstance(attributes["cloud.platform"], str) self.assertIsInstance(attributes["cloud.resource_id"], str) - self.assertIsInstance(attributes["k8s.cluster.name"], str) - - @patch.dict( - "os.environ", - { - "CLUSTER_RESOURCE_ID": ( - "/subscriptions/test-sub/resourceGroups/test-rg/providers/" - "Microsoft.ContainerService/ManagedClusters/my-cluster" - ) - }, - clear=True, - ) - def test_cluster_name_resource_type_is_case_insensitive(self) -> None: - attributes = AzureAKSResourceDetector().detect().attributes - - self.assertEqual(attributes["k8s.cluster.name"], "my-cluster") - - @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": "standalone-name"}, clear=True) - def test_cluster_name_falls_back_to_last_segment(self) -> None: - attributes = AzureAKSResourceDetector().detect().attributes - - self.assertEqual(attributes["k8s.cluster.name"], "standalone-name") @patch.dict("os.environ", {}, clear=True) @patch( @@ -78,7 +56,6 @@ def test_detects_aks_from_configmap_volume(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) - self.assertEqual(attributes["k8s.cluster.name"], "test-aks-cluster") @patch.dict("os.environ", {}, clear=True) def test_detects_aks_from_subpath_mount(self) -> None: @@ -144,7 +121,11 @@ def test_ignores_configmap_volume_without_resource_id(self) -> None: def test_environment_takes_precedence_over_file(self) -> None: attributes = self._detect_from_file(TEST_RESOURCE_ID) - self.assertEqual(attributes["k8s.cluster.name"], "from-env") + self.assertEqual( + attributes["cloud.resource_id"], + "/subscriptions/test-sub/resourceGroups/test-rg/providers/" + "Microsoft.ContainerService/managedClusters/from-env", + ) @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": TEST_RESOURCE_ID}, clear=True) @patch("opentelemetry.resource.detector.azure.vm.urlopen") From 04c21919be954194c68aab5a9c6b834edefd5f04 Mon Sep 17 00:00:00 2001 From: Jackson Weber <47067795+JacksonWeber@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:08:45 -0700 Subject: [PATCH 7/7] Rerun CI after transient ASGI failure Assisted-by: GPT-5.6 Sol Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>