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 27f74707..91b419ea 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 @@ -22,6 +22,11 @@ class ResourceAttributes: CLOUD_PLATFORM_KEY = "cloud.platform" CLOUD_PROVIDER = "cloud.provider" CLOUD_REGION = "cloud.region" + FAAS_INSTANCE = "faas.instance" + FAAS_NAME = "faas.name" + FAAS_VERSION = "faas.version" + GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" + GCP_CLOUD_RUN = "gcp_cloud_run" GCP_COMPUTE_ENGINE = "gcp_compute_engine" GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" HOST_ID = "host.id" @@ -35,8 +40,6 @@ class ResourceAttributes: SERVICE_INSTANCE_ID = "service.instance.id" SERVICE_NAME = "service.name" SERVICE_NAMESPACE = "service.namespace" - FAAS_INSTANCE = "faas.instance" - FAAS_NAME = "faas.name" AWS_ACCOUNT = "aws_account" 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 94256bbd..28656dd1 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 @@ -15,6 +15,7 @@ from typing import Mapping from opentelemetry.resourcedetector.gcp_resource_detector import ( + _faas, _gce, _gke, _metadata, @@ -33,6 +34,10 @@ def detect(self) -> Resource: if _gke.on_gke(): return _gke_resource() + if _faas.on_cloud_functions(): + return _cloud_functions_resource() + if _faas.on_cloud_run(): + return _cloud_run_resource() if _gce.on_gce(): return _gce_resource() @@ -70,6 +75,30 @@ def _gce_resource() -> Resource: ) +def _cloud_run_resource() -> Resource: + return _make_resource( + { + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_RUN, + ResourceAttributes.FAAS_NAME: _faas.faas_name(), + ResourceAttributes.FAAS_VERSION: _faas.faas_version(), + ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), + ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), + } + ) + + +def _cloud_functions_resource() -> Resource: + return _make_resource( + { + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_FUNCTIONS, + ResourceAttributes.FAAS_NAME: _faas.faas_name(), + ResourceAttributes.FAAS_VERSION: _faas.faas_version(), + ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), + ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), + } + ) + + def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource: return Resource.create( { diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py new file mode 100644 index 00000000..93af57fb --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py @@ -0,0 +1,60 @@ +# 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/faas.go + +import os + +from opentelemetry.resourcedetector.gcp_resource_detector import _metadata + +_CLOUD_RUN_CONFIG_ENV = "K_CONFIGURATION" +_CLOUD_FUNCTION_TARGET_ENV = "FUNCTION_TARGET" +_FAAS_SERVICE_ENV = "K_SERVICE" +_FAAS_REVISION_ENV = "K_REVISION" + + +def on_cloud_run() -> bool: + return _CLOUD_RUN_CONFIG_ENV in os.environ + + +def on_cloud_functions() -> bool: + return _CLOUD_FUNCTION_TARGET_ENV in os.environ + + +def faas_name() -> str: + """The name of the Cloud Run or Cloud Function. + + Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may + throw exceptions. + """ + return os.environ[_FAAS_SERVICE_ENV] + + +def faas_version() -> str: + """The version/revision of the Cloud Run or Cloud Function. + + Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may + throw exceptions. + """ + return os.environ[_FAAS_REVISION_ENV] + + +def faas_instance() -> str: + return str(_metadata.get_metadata()["instance"]["id"]) + + +def faas_cloud_region() -> str: + region = _metadata.get_metadata()["instance"]["region"] + return region[region.rfind("/") + 1 :] diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py index bfbf1803..c1ee2be5 100644 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py @@ -45,6 +45,7 @@ class Instance(TypedDict): id: Union[int, str] machineType: str name: str + region: str zone: str diff --git a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr index 7805dd82..3310e5b0 100644 --- a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr +++ b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr @@ -1,3 +1,33 @@ +# name: test_detects_cloud_functions + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.platform': 'gcp_cloud_functions', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'faas.instance': '0087244a', + 'faas.name': 'fake-service', + 'faas.version': 'fake-revision', + 'service.name': 'unknown_service', + 'telemetry.sdk.language': 'python', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.20.0', + }) +# --- +# name: test_detects_cloud_run + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.platform': 'gcp_cloud_run', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'faas.instance': '0087244a', + 'faas.name': 'fake-service', + 'faas.version': 'fake-revision', + 'service.name': 'unknown_service', + 'telemetry.sdk.language': 'python', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.20.0', + }) +# --- # name: test_detects_empty_as_fallback dict({ }) @@ -13,7 +43,7 @@ 'cloud.platform': 'gcp_compute_engine', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', - 'host.id': '12345', + 'host.id': '0087244a', 'host.name': 'fakeName', 'host.type': 'fakeMachineType', 'service.name': 'unknown_service', diff --git a/opentelemetry-resourcedetector-gcp/tests/test_detector.py b/opentelemetry-resourcedetector-gcp/tests/test_detector.py index b6a51e1e..acc868e8 100644 --- a/opentelemetry-resourcedetector-gcp/tests/test_detector.py +++ b/opentelemetry-resourcedetector-gcp/tests/test_detector.py @@ -65,7 +65,7 @@ def test_detects_gce(snapshot, fake_metadata: _metadata.Metadata): "project": {"projectId": "fakeProject"}, "instance": { "name": "fakeName", - "id": 12345, + "id": "0087244a", "machineType": "fakeMachineType", "zone": "projects/233510669999/zones/us-east4-b", "attributes": {}, @@ -108,3 +108,50 @@ def test_detects_gke( ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_cloud_run( + snapshot, + fake_metadata: _metadata.Metadata, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") + monkeypatch.setenv("K_SERVICE", "fake-service") + monkeypatch.setenv("K_REVISION", "fake-revision") + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + # this will not be numeric on FaaS + "id": "0087244a", + "region": "projects/233510669999/regions/us-east4", + }, + } + ) + + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_cloud_functions( + snapshot, + fake_metadata: _metadata.Metadata, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") + # Note all K_* environment variables are set since Cloud Functions executes within Cloud + # Run. This tests that the detector can differentiate between them + monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") + monkeypatch.setenv("K_SERVICE", "fake-service") + monkeypatch.setenv("K_REVISION", "fake-revision") + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + # this will not be numeric on FaaS + "id": "0087244a", + "region": "projects/233510669999/regions/us-east4", + }, + } + ) + + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot diff --git a/opentelemetry-resourcedetector-gcp/tests/test_faas.py b/opentelemetry-resourcedetector-gcp/tests/test_faas.py new file mode 100644 index 00000000..d1a47a70 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/test_faas.py @@ -0,0 +1,65 @@ +# 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 _faas + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(fake_get_metadata): + pass + + +def test_detects_on_cloud_run(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") + assert _faas.on_cloud_run() + + +def test_detects_not_on_cloud_run() -> None: + assert not _faas.on_cloud_run() + + +def test_detects_on_cloud_functions(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") + assert _faas.on_cloud_functions() + + +def test_detects_not_on_cloud_functions() -> None: + assert not _faas.on_cloud_functions() + + +def test_detects_faas_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_SERVICE", "fake-service") + assert _faas.faas_name() == "fake-service" + + +def test_detects_faas_version(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_REVISION", "fake-revision") + assert _faas.faas_version() == "fake-revision" + + +def test_detects_faas_instance(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"id": "0087244a"}} + assert _faas.faas_instance() == "0087244a" + + +def test_detects_faas_region(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"region": "projects/233510669999/regions/us-east4"} + } + assert _faas.faas_cloud_region() == "us-east4"