From 8d7b3526ef22acd7fb7d2e55529411cb08d72302 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 8 Aug 2024 20:30:07 +0000 Subject: [PATCH] Implement GAE resource detection --- .../gcp_resource_detector/_constants.py | 2 + .../gcp_resource_detector/_detector.py | 29 ++++++ .../gcp_resource_detector/_gae.py | 88 ++++++++++++++++++ .../tests/__snapshots__/test_detector.ambr | 32 +++++++ .../tests/test_detector.py | 42 +++++++++ .../tests/test_gae.py | 89 +++++++++++++++++++ 6 files changed, 282 insertions(+) create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gae.py diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py index 91b419ea..2828b6d6 100644 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + # TODO: use opentelemetry-semantic-conventions package for these constants once it has # stabilized. Right now, pinning an unstable version would cause dependency conflicts for # users so these are copied in. @@ -25,6 +26,7 @@ class ResourceAttributes: FAAS_INSTANCE = "faas.instance" FAAS_NAME = "faas.name" FAAS_VERSION = "faas.version" + GCP_APP_ENGINE = "gcp_app_engine" GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" GCP_CLOUD_RUN = "gcp_cloud_run" GCP_COMPUTE_ENGINE = "gcp_compute_engine" diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py index 28656dd1..9bbcaf6d 100644 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py @@ -16,6 +16,7 @@ from opentelemetry.resourcedetector.gcp_resource_detector import ( _faas, + _gae, _gce, _gke, _metadata, @@ -29,6 +30,7 @@ class GoogleCloudResourceDetector(ResourceDetector): def detect(self) -> Resource: + # pylint: disable=too-many-return-statements if not _metadata.is_available(): return Resource.get_empty() @@ -38,6 +40,8 @@ def detect(self) -> Resource: return _cloud_functions_resource() if _faas.on_cloud_run(): return _cloud_run_resource() + if _gae.on_app_engine(): + return _gae_resource() if _gce.on_gce(): return _gce_resource() @@ -99,6 +103,31 @@ def _cloud_functions_resource() -> Resource: ) +def _gae_resource() -> Resource: + if _gae.on_app_engine_standard(): + zone = _gae.standard_availability_zone() + region = _gae.standard_cloud_region() + else: + zone_and_region = _gae.flex_availability_zone_and_region() + zone = zone_and_region.zone + region = zone_and_region.region + + faas_name = _gae.service_name() + faas_version = _gae.service_version() + faas_instance = _gae.service_instance() + + return _make_resource( + { + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_APP_ENGINE, + ResourceAttributes.FAAS_NAME: faas_name, + ResourceAttributes.FAAS_VERSION: faas_version, + ResourceAttributes.FAAS_INSTANCE: faas_instance, + ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone, + ResourceAttributes.CLOUD_REGION: region, + } + ) + + def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource: return Resource.create( { diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py new file mode 100644 index 00000000..b67dc994 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py @@ -0,0 +1,88 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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. + +# Implementation in this file copied from +# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/app_engine.go + +import os + +from opentelemetry.resourcedetector.gcp_resource_detector import ( + _faas, + _gce, + _metadata, +) + +_GAE_SERVICE_ENV = "GAE_SERVICE" +_GAE_VERSION_ENV = "GAE_VERSION" +_GAE_INSTANCE_ENV = "GAE_INSTANCE" +_GAE_ENV = "GAE_ENV" +_GAE_STANDARD = "standard" + + +def on_app_engine_standard() -> bool: + return os.environ.get(_GAE_ENV) == _GAE_STANDARD + + +def on_app_engine() -> bool: + return _GAE_SERVICE_ENV in os.environ + + +def service_name() -> str: + """The service name of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_SERVICE_ENV] + + +def service_version() -> str: + """The service version of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_VERSION_ENV] + + +def service_instance() -> str: + """The service instance of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_INSTANCE_ENV] + + +def flex_availability_zone_and_region() -> _gce.ZoneAndRegion: + """The zone and region in which this program is running. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return _gce.availability_zone_and_region() + + +def standard_availability_zone() -> str: + """The zone the app engine service is running in. + + Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. + """ + zone = _metadata.get_metadata()["instance"]["zone"] + # zone is of the form "projects/233510669999/zones/us15" + return zone[zone.rfind("/") + 1 :] + + +def standard_cloud_region() -> str: + """The region the app engine service is running in. + + Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. + """ + return _faas.faas_cloud_region() diff --git a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr index 3310e5b0..e27ec90a 100644 --- a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr +++ b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr @@ -36,6 +36,38 @@ dict({ }) # --- +# name: test_detects_gae_flex + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.availability_zone': 'us-east4-b', + 'cloud.platform': 'gcp_app_engine', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'faas.instance': 'fake-instance', + 'faas.name': 'fake-service', + 'faas.version': 'fake-version', + 'service.name': 'unknown_service', + 'telemetry.sdk.language': 'python', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.20.0', + }) +# --- +# name: test_detects_gae_standard + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.availability_zone': 'us-east4-b', + 'cloud.platform': 'gcp_app_engine', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'faas.instance': 'fake-instance', + 'faas.name': 'fake-service', + 'faas.version': 'fake-version', + 'service.name': 'unknown_service', + 'telemetry.sdk.language': 'python', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.20.0', + }) +# --- # name: test_detects_gce dict({ 'cloud.account.id': 'fakeProject', diff --git a/opentelemetry-resourcedetector-gcp/tests/test_detector.py b/opentelemetry-resourcedetector-gcp/tests/test_detector.py index acc868e8..dfa3ca89 100644 --- a/opentelemetry-resourcedetector-gcp/tests/test_detector.py +++ b/opentelemetry-resourcedetector-gcp/tests/test_detector.py @@ -155,3 +155,45 @@ def test_detects_cloud_functions( ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_gae_standard( + snapshot, + fake_metadata: _metadata.Metadata, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("GAE_ENV", "standard") + monkeypatch.setenv("GAE_SERVICE", "fake-service") + monkeypatch.setenv("GAE_VERSION", "fake-version") + monkeypatch.setenv("GAE_INSTANCE", "fake-instance") + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + "region": "projects/233510669999/regions/us-east4", + "zone": "us-east4-b", + }, + } + ) + + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_gae_flex( + snapshot, + fake_metadata: _metadata.Metadata, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("GAE_SERVICE", "fake-service") + monkeypatch.setenv("GAE_VERSION", "fake-version") + monkeypatch.setenv("GAE_INSTANCE", "fake-instance") + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + "zone": "projects/233510669999/zones/us-east4-b", + }, + } + ) + + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gae.py b/opentelemetry-resourcedetector-gcp/tests/test_gae.py new file mode 100644 index 00000000..e418881f --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/test_gae.py @@ -0,0 +1,89 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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 unittest.mock import MagicMock + +import pytest +from opentelemetry.resourcedetector.gcp_resource_detector import _gae + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(fake_get_metadata): + pass + + +def test_detects_on_gae(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.on_app_engine() + + +def test_detects_not_on_gae() -> None: + assert not _gae.on_app_engine() + + +def test_detects_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_ENV", "standard") + assert _gae.on_app_engine_standard() + + +def test_detects_not_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.on_app_engine() + assert not _gae.on_app_engine_standard() + + +def test_detects_gae_service_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.service_name() == "fake-service" + + +def test_detects_gae_service_version(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_VERSION", "fake-version") + assert _gae.service_version() == "fake-version" + + +def test_detects_gae_service_instance(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_INSTANCE", "fake-instance") + assert _gae.service_instance() == "fake-instance" + + +def test_detects_gae_flex_zone_and_region( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"zone": "projects/233510669999/zones/us-east4-b"} + } + zone_and_region = _gae.flex_availability_zone_and_region() + assert zone_and_region.zone == "us-east4-b" + assert zone_and_region.region == "us-east4" + + +def test_gae_standard_zone( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"zone": "projects/233510669999/zones/us15"} + } + assert _gae.standard_availability_zone() == "us15" + + +def test_gae_standard_region( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"region": "projects/233510669999/regions/us-east4"} + } + assert _gae.standard_cloud_region() == "us-east4"