From 2af2ab60a0bd6135701e89d4f18fcbccd2270192 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Tue, 23 Jul 2024 03:28:21 +0000 Subject: [PATCH] Implement faas resource detection --- .../gcp_resource_detector/_constants.py | 8 ++- .../gcp_resource_detector/_detector.py | 29 +++++++++ .../gcp_resource_detector/_faas.py | 60 +++++++++++++++++ .../gcp_resource_detector/_metadata.py | 1 + .../tests/__snapshots__/test_detector.ambr | 30 +++++++++ .../tests/test_detector.py | 45 +++++++++++++ .../tests/test_faas.py | 65 +++++++++++++++++++ 7 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_faas.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 27f74707..67404102 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. @@ -22,6 +23,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 +41,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 b947c263..12e65c50 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 @@ -43,6 +43,7 @@ class Instance(TypedDict): id: int 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..ae95a42e 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': '12345', + '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': '12345', + '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({ }) diff --git a/opentelemetry-resourcedetector-gcp/tests/test_detector.py b/opentelemetry-resourcedetector-gcp/tests/test_detector.py index b6a51e1e..df71352a 100644 --- a/opentelemetry-resourcedetector-gcp/tests/test_detector.py +++ b/opentelemetry-resourcedetector-gcp/tests/test_detector.py @@ -108,3 +108,48 @@ 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": { + "id": 12345, + "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": { + "id": 12345, + "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..baee5841 --- /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": 12345}} + assert _faas.faas_instance() == "12345" + + +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"