From 0c66d2695a4ea0eafe3b9af51deb18e0f76cb7d1 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 26 Jun 2026 20:43:05 +0000 Subject: [PATCH 1/5] Migrate GCP resource detector to contrib --- eachdist.ini | 1 + .../.changelog/.gitignore | 1 + .../CHANGELOG.md | 16 ++ .../LICENSE | 201 ++++++++++++++ .../MANIFEST.rst | 9 + .../README.rst | 46 ++++ .../pyproject.toml | 84 ++++++ .../resource/detector/gcp/__init__.py | 143 ++++++++++ .../resource/detector/gcp/_constants.py | 69 +++++ .../resource/detector/gcp/_faas.py | 60 ++++ .../resource/detector/gcp/_gae.py | 88 ++++++ .../resource/detector/gcp/_gce.py | 72 +++++ .../resource/detector/gcp/_gke.py | 56 ++++ .../resource/detector/gcp/_mapping.py | 222 +++++++++++++++ .../resource/detector/gcp/_metadata.py | 93 +++++++ .../resource/detector/gcp/version.py | 4 + .../test-requirements-0.txt | 16 ++ .../test-requirements-1.txt | 15 + .../test_gcp_resource_detector.ambr | 87 ++++++ .../tests/__snapshots__/test_mapping.ambr | 229 +++++++++++++++ .../tests/conftest.py | 25 ++ .../tests/test_faas.py | 65 +++++ .../tests/test_gae.py | 89 ++++++ .../tests/test_gce.py | 74 +++++ .../tests/test_gcp_resource_detector.py | 199 ++++++++++++++ .../tests/test_gke.py | 70 +++++ .../tests/test_mapping.py | 260 ++++++++++++++++++ tox.ini | 16 ++ 28 files changed, 2310 insertions(+) create mode 100644 resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore create mode 100644 resource/opentelemetry-resource-detector-gcp/CHANGELOG.md create mode 100644 resource/opentelemetry-resource-detector-gcp/LICENSE create mode 100644 resource/opentelemetry-resource-detector-gcp/MANIFEST.rst create mode 100644 resource/opentelemetry-resource-detector-gcp/README.rst create mode 100644 resource/opentelemetry-resource-detector-gcp/pyproject.toml create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py create mode 100644 resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py create mode 100644 resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt create mode 100644 resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/conftest.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_faas.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_gae.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_gce.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_gke.py create mode 100644 resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py diff --git a/eachdist.ini b/eachdist.ini index c6679d2079..71b68ef75c 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -51,6 +51,7 @@ packages= opentelemetry-opamp-client opentelemetry-propagator-aws-xray opentelemetry-resource-detector-azure + opentelemetry-resource-detector-gcp opentelemetry-sdk-extension-aws opentelemetry-propagator-aws-xray opentelemetry-instrumentation-anthropic diff --git a/resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore b/resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore new file mode 100644 index 0000000000..f935021a8f --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/resource/opentelemetry-resource-detector-gcp/CHANGELOG.md b/resource/opentelemetry-resource-detector-gcp/CHANGELOG.md new file mode 100644 index 0000000000..d682bb498c --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +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). + + \ No newline at end of file diff --git a/resource/opentelemetry-resource-detector-gcp/LICENSE b/resource/opentelemetry-resource-detector-gcp/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/resource/opentelemetry-resource-detector-gcp/MANIFEST.rst b/resource/opentelemetry-resource-detector-gcp/MANIFEST.rst new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/MANIFEST.rst @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/resource/opentelemetry-resource-detector-gcp/README.rst b/resource/opentelemetry-resource-detector-gcp/README.rst new file mode 100644 index 0000000000..633a3452c4 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/README.rst @@ -0,0 +1,46 @@ +OpenTelemetry Google Cloud Resource Detector +============================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-resource-detector-gcp.svg + :target: https://pypi.org/project/opentelemetry-resource-detector-gcp/ + +This library provides support for detecting GCP resources like GCE, GKE, etc. + +Installation +------------ + +:: + + pip install opentelemetry-resource-detector-gcp + +Usage +----- + +.. code-block:: python + + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry import trace + from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, Resource + + # This will use the GoogleCloudResourceDetector under the covers. + resource = Resource.create( + attributes={ + # Use the PID as the service.instance.id to avoid duplicate timeseries + # from different Gunicorn worker processes. + SERVICE_INSTANCE_ID: f"worker-{os.getpid()}", + } + ) + traceProvider = TracerProvider(resource=resource) + processor = BatchSpanProcessor(OTLPSpanExporter()) + traceProvider.add_span_processor(processor) + trace.set_tracer_provider(traceProvider) + +References +---------- + +* `Cloud Monitoring `_ +* `OpenTelemetry Project `_ diff --git a/resource/opentelemetry-resource-detector-gcp/pyproject.toml b/resource/opentelemetry-resource-detector-gcp/pyproject.toml new file mode 100644 index 0000000000..b9ac566397 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/pyproject.toml @@ -0,0 +1,84 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-resource-detector-gcp" +dynamic = ["version"] +description = "Google Cloud Resource Detector for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.10" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [ + "opentelemetry-api ~= 1.30", + "opentelemetry-sdk ~= 1.30", + "requests ~= 2.24", +] + +[project.entry-points.opentelemetry_resource_detector] +gcp = "opentelemetry.resource.detector.gcp:GoogleCloudResourceDetector" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resource-detector-gcp" +Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" + +[tool.hatch.version] +path = "src/opentelemetry/resource/detector/gcp/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] + +[tool.towncrier] +directory = ".changelog" +filename = "CHANGELOG.md" +start_string = "\n" +template = "../../scripts/changelog_template.j2" +issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" +wrap = true +issue_pattern = "^(\\d+)" + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py new file mode 100644 index 0000000000..53d3b50d1c --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py @@ -0,0 +1,143 @@ +# Copyright 2025 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 +# +# 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 typing import Mapping + +from opentelemetry.resource.detector.gcp import ( + _faas, + _gae, + _gce, + _gke, + _metadata, +) +from opentelemetry.resource.detector.gcp._constants import ( + ResourceAttributes, +) +from opentelemetry.sdk.resources import Resource, ResourceDetector +from opentelemetry.util.types import AttributeValue + + +class GoogleCloudResourceDetector(ResourceDetector): + def detect(self) -> Resource: + # pylint: disable=too-many-return-statements + if not _metadata.is_available(): + return Resource.get_empty() + + 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 _gae.on_app_engine(): + return _gae_resource() + if _gce.on_gce(): + return _gce_resource() + + return Resource.get_empty() + + +def _gke_resource() -> Resource: + zone_or_region = _gke.availability_zone_or_region() + zone_or_region_key = ( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE + if zone_or_region.type == "zone" + else ResourceAttributes.CLOUD_REGION + ) + return _make_resource( + { + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_KUBERNETES_ENGINE, + zone_or_region_key: zone_or_region.value, + ResourceAttributes.K8S_CLUSTER_NAME: _gke.cluster_name(), + ResourceAttributes.HOST_ID: _gke.host_id(), + } + ) + + +def _gce_resource() -> Resource: + zone_and_region = _gce.availability_zone_and_region() + return _make_resource( + { + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_COMPUTE_ENGINE, + ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone_and_region.zone, + ResourceAttributes.CLOUD_REGION: zone_and_region.region, + ResourceAttributes.HOST_TYPE: _gce.host_type(), + ResourceAttributes.HOST_ID: _gce.host_id(), + ResourceAttributes.HOST_NAME: _gce.host_name(), + } + ) + + +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 _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( + { + ResourceAttributes.CLOUD_PROVIDER: "gcp", + ResourceAttributes.CLOUD_ACCOUNT_ID: _metadata.get_metadata()[ + "project" + ]["projectId"], + **attrs, + } + ) + + +__all__ = ["GoogleCloudResourceDetector"] diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py new file mode 100644 index 0000000000..2828b6d6be --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py @@ -0,0 +1,69 @@ +# Copyright 2023 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. + + +# 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. +class ResourceAttributes: + AWS_EC2 = "aws_ec2" + CLOUD_ACCOUNT_ID = "cloud.account.id" + CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" + 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_APP_ENGINE = "gcp_app_engine" + 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" + HOST_NAME = "host.name" + HOST_TYPE = "host.type" + K8S_CLUSTER_NAME = "k8s.cluster.name" + K8S_CONTAINER_NAME = "k8s.container.name" + K8S_NAMESPACE_NAME = "k8s.namespace.name" + K8S_NODE_NAME = "k8s.node.name" + K8S_POD_NAME = "k8s.pod.name" + SERVICE_INSTANCE_ID = "service.instance.id" + SERVICE_NAME = "service.name" + SERVICE_NAMESPACE = "service.namespace" + + +AWS_ACCOUNT = "aws_account" +AWS_EC2_INSTANCE = "aws_ec2_instance" +CLUSTER_NAME = "cluster_name" +CONTAINER_NAME = "container_name" +GCE_INSTANCE = "gce_instance" +GENERIC_NODE = "generic_node" +GENERIC_TASK = "generic_task" +INSTANCE_ID = "instance_id" +JOB = "job" +K8S_CLUSTER = "k8s_cluster" +K8S_CONTAINER = "k8s_container" +K8S_NODE = "k8s_node" +K8S_POD = "k8s_pod" +LOCATION = "location" +NAMESPACE = "namespace" +NAMESPACE_NAME = "namespace_name" +NODE_ID = "node_id" +NODE_NAME = "node_name" +POD_NAME = "pod_name" +REGION = "region" +TASK_ID = "task_id" +ZONE = "zone" +UNKNOWN_SERVICE_PREFIX = "unknown_service" diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py new file mode 100644 index 0000000000..43e970c4b2 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_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.resource.detector.gcp 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/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py new file mode 100644 index 0000000000..593776f39a --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_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.resource.detector.gcp 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/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py new file mode 100644 index 0000000000..509b614b7f --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py @@ -0,0 +1,72 @@ +# Copyright 2023 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. + +import logging +import re +from dataclasses import dataclass + +from opentelemetry.resource.detector.gcp import _metadata + +# Format described in +# https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata +_ZONE_REGION_RE = re.compile( + r"projects\/\d+\/zones\/(?P(?P\w+-\w+)-\w+)" +) + +_logger = logging.getLogger(__name__) + + +def on_gce() -> bool: + try: + _metadata.get_metadata()["instance"]["machineType"] + except (_metadata.MetadataAccessException, KeyError): + _logger.debug( + "Could not fetch metadata attribute instance/machineType, " + "assuming not on GCE.", + exc_info=True, + ) + return False + return True + + +def host_type() -> str: + return _metadata.get_metadata()["instance"]["machineType"] + + +def host_id() -> str: + return str(_metadata.get_metadata()["instance"]["id"]) + + +def host_name() -> str: + return _metadata.get_metadata()["instance"]["name"] + + +@dataclass +class ZoneAndRegion: + zone: str + region: str + + +def availability_zone_and_region() -> ZoneAndRegion: + full_zone = _metadata.get_metadata()["instance"]["zone"] + match = _ZONE_REGION_RE.search(full_zone) + if not match: + raise ValueError( + "zone was not in the expected format: " + f"projects/PROJECT_NUM/zones/COUNTRY-REGION-ZONE. Got {full_zone}" + ) + + return ZoneAndRegion( + zone=match.group("zone"), region=match.group("region") + ) diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py new file mode 100644 index 0000000000..b8b4656211 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py @@ -0,0 +1,56 @@ +# Copyright 2023 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. + +import os +from dataclasses import dataclass +from typing import Literal + +from opentelemetry.resource.detector.gcp import ( + _gce, + _metadata, +) + +KUBERNETES_SERVICE_HOST_ENV = "KUBERNETES_SERVICE_HOST" + + +def on_gke() -> bool: + return os.environ.get(KUBERNETES_SERVICE_HOST_ENV) is not None + + +def host_id() -> str: + return _gce.host_id() + + +def cluster_name() -> str: + return _metadata.get_metadata()["instance"]["attributes"]["cluster-name"] + + +@dataclass +class ZoneOrRegion: + type: Literal["zone", "region"] + value: str + + +def availability_zone_or_region() -> ZoneOrRegion: + cluster_location = _metadata.get_metadata()["instance"]["attributes"][ + "cluster-location" + ] + hyphen_count = cluster_location.count("-") + if hyphen_count == 1: + return ZoneOrRegion(type="region", value=cluster_location) + if hyphen_count == 2: + return ZoneOrRegion(type="zone", value=cluster_location) + raise ValueError( + f"unrecognized format for cluster location: {cluster_location}" + ) diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py new file mode 100644 index 0000000000..3b3d8ddeb3 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py @@ -0,0 +1,222 @@ +# Copyright 2023 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. + +import json +from dataclasses import dataclass +from typing import Dict, Mapping, Optional, Tuple + +from opentelemetry.resource.detector.gcp import _constants +from opentelemetry.resource.detector.gcp._constants import ( + ResourceAttributes, +) +from opentelemetry.sdk.resources import Attributes, Resource + + +class MapConfig: + otel_keys: Tuple[str, ...] + """ + OTel resource keys to try and populate the resource label from. For entries with multiple + OTel resource keys, the keys' values will be coalesced in order until there is a non-empty + value. + """ + + fallback: str + """If none of the otelKeys are present in the Resource, fallback to this literal value""" + + def __init__(self, *otel_keys: str, fallback: str = ""): + self.otel_keys = otel_keys + self.fallback = fallback + + +# Mappings of GCM resource label keys onto mapping config from OTel resource for a given +# monitored resource type. Copied from Go impl: +# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/internal/resourcemapping/resourcemapping.go#L51 +MAPPINGS = { + _constants.GCE_INSTANCE: { + _constants.ZONE: MapConfig(ResourceAttributes.CLOUD_AVAILABILITY_ZONE), + _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), + }, + _constants.K8S_CONTAINER: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + ), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NAMESPACE_NAME: MapConfig( + ResourceAttributes.K8S_NAMESPACE_NAME + ), + _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), + _constants.CONTAINER_NAME: MapConfig( + ResourceAttributes.K8S_CONTAINER_NAME + ), + }, + _constants.K8S_POD: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + ), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NAMESPACE_NAME: MapConfig( + ResourceAttributes.K8S_NAMESPACE_NAME + ), + _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), + }, + _constants.K8S_NODE: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + ), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NODE_NAME: MapConfig(ResourceAttributes.K8S_NODE_NAME), + }, + _constants.K8S_CLUSTER: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + ), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + }, + _constants.AWS_EC2_INSTANCE: { + _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), + _constants.REGION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + ), + _constants.AWS_ACCOUNT: MapConfig(ResourceAttributes.CLOUD_ACCOUNT_ID), + }, + _constants.GENERIC_TASK: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + fallback="global", + ), + _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), + _constants.JOB: MapConfig( + ResourceAttributes.SERVICE_NAME, + ResourceAttributes.FAAS_NAME, + ), + _constants.TASK_ID: MapConfig( + ResourceAttributes.SERVICE_INSTANCE_ID, + ResourceAttributes.FAAS_INSTANCE, + ), + }, + _constants.GENERIC_NODE: { + _constants.LOCATION: MapConfig( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + ResourceAttributes.CLOUD_REGION, + fallback="global", + ), + _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), + _constants.NODE_ID: MapConfig( + ResourceAttributes.HOST_ID, ResourceAttributes.HOST_NAME + ), + }, +} + + +@dataclass +class MonitoredResourceData: + """Dataclass representing a protobuf monitored resource. Make sure to convert to a protobuf + if needed.""" + + type: str + labels: Mapping[str, str] + + +def get_monitored_resource( + resource: Resource, +) -> Optional[MonitoredResourceData]: + """Add Google resource specific information (e.g. instance id, region). + + See + https://cloud.google.com/monitoring/custom-metrics/creating-metrics#custom-metric-resources + for supported types + Args: + resource: OTel resource + """ + + attrs = resource.attributes + + platform = attrs.get(ResourceAttributes.CLOUD_PLATFORM_KEY) + if platform == ResourceAttributes.GCP_COMPUTE_ENGINE: + mr = _create_monitored_resource(_constants.GCE_INSTANCE, attrs) + elif platform == ResourceAttributes.GCP_KUBERNETES_ENGINE: + if ResourceAttributes.K8S_CONTAINER_NAME in attrs: + mr = _create_monitored_resource(_constants.K8S_CONTAINER, attrs) + elif ResourceAttributes.K8S_POD_NAME in attrs: + mr = _create_monitored_resource(_constants.K8S_POD, attrs) + elif ResourceAttributes.K8S_NODE_NAME in attrs: + mr = _create_monitored_resource(_constants.K8S_NODE, attrs) + else: + mr = _create_monitored_resource(_constants.K8S_CLUSTER, attrs) + elif platform == ResourceAttributes.AWS_EC2: + mr = _create_monitored_resource(_constants.AWS_EC2_INSTANCE, attrs) + else: + # fallback to generic_task + if ( + ResourceAttributes.SERVICE_NAME in attrs + or ResourceAttributes.FAAS_NAME in attrs + ) and ( + ResourceAttributes.SERVICE_INSTANCE_ID in attrs + or ResourceAttributes.FAAS_INSTANCE in attrs + ): + mr = _create_monitored_resource(_constants.GENERIC_TASK, attrs) + else: + mr = _create_monitored_resource(_constants.GENERIC_NODE, attrs) + + return mr + + +def _create_monitored_resource( + monitored_resource_type: str, resource_attrs: Attributes +) -> MonitoredResourceData: + mapping = MAPPINGS[monitored_resource_type] + labels: Dict[str, str] = {} + + for mr_key, map_config in mapping.items(): + mr_value = None + for otel_key in map_config.otel_keys: + if otel_key in resource_attrs and not str( + resource_attrs[otel_key] + ).startswith(_constants.UNKNOWN_SERVICE_PREFIX): + mr_value = resource_attrs[otel_key] + break + + if ( + mr_value is None + and ResourceAttributes.SERVICE_NAME in map_config.otel_keys + ): + # The service name started with unknown_service, and was ignored above. + mr_value = resource_attrs.get(ResourceAttributes.SERVICE_NAME) + + if mr_value is None: + mr_value = map_config.fallback + + # OTel attribute values can be any of str, bool, int, float, or Sequence of any of + # them. Encode any non-strings as json string + if not isinstance(mr_value, str): + mr_value = json.dumps( + mr_value, sort_keys=True, indent=None, separators=(",", ":") + ) + labels[mr_key] = mr_value + + return MonitoredResourceData(type=monitored_resource_type, labels=labels) diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py new file mode 100644 index 0000000000..51db56130b --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py @@ -0,0 +1,93 @@ +# Copyright 2023 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. + +import logging +from functools import lru_cache +from typing import TypedDict, Union + +import requests + +_GCP_METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/" +_INSTANCE = "instance" +_RECURSIVE_PARAMS = {"recursive": "true"} +_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"} +# Use a shorter timeout for connection so we won't block much if it's unreachable +_TIMEOUT = (2, 5) + +_logger = logging.getLogger(__name__) + + +class Project(TypedDict): + projectId: str + + +Attributes = TypedDict( + "Attributes", {"cluster-location": str, "cluster-name": str}, total=False +) + + +class Instance(TypedDict): + attributes: Attributes + # id can be an integer on GCE VMs or a string on other environments + id: Union[int, str] + machineType: str + name: str + region: str + zone: str + + +class Metadata(TypedDict): + instance: Instance + project: Project + + +class MetadataAccessException(Exception): + pass + + +@lru_cache(maxsize=None) +def get_metadata() -> Metadata: + """Get all instance and project metadata from the metadata server + + Cached for the lifetime of the process. + """ + try: + res = requests.get( + f"{_GCP_METADATA_URL}", + params=_RECURSIVE_PARAMS, + headers=_GCP_METADATA_URL_HEADER, + timeout=_TIMEOUT, + ) + res.raise_for_status() + all_metadata = res.json() + except requests.RequestException as err: + raise MetadataAccessException() from err + return all_metadata + + +@lru_cache(maxsize=None) +def is_available() -> bool: + try: + requests.get( + f"{_GCP_METADATA_URL}{_INSTANCE}/", + headers=_GCP_METADATA_URL_HEADER, + timeout=_TIMEOUT, + ).raise_for_status() + except requests.RequestException: + _logger.debug( + "Failed to make request to metadata server, assuming it's not available", + exc_info=True, + ) + return False + return True diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py new file mode 100644 index 0000000000..98ec6d01b8 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "0.1.0.dev" diff --git a/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt b/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt new file mode 100644 index 0000000000..84ac1449bd --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt @@ -0,0 +1,16 @@ +asgiref==3.8.1 +Deprecated==1.2.14 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.6.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +typing_extensions==4.12.2 +wrapt==1.16.0 +zipp==3.19.2 +requests==2.33.1 +syrupy==4.6.1 +opentelemetry-api==1.30 # when updating, also update in pyproject.toml +opentelemetry-sdk==1.30 # when updating, also update in pyproject.toml + +-e resource/opentelemetry-resource-detector-gcp diff --git a/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt b/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt new file mode 100644 index 0000000000..4bf62f6335 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt @@ -0,0 +1,15 @@ +asgiref==3.8.1 +Deprecated==1.2.14 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.6.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +typing_extensions==4.12.2 +wrapt==1.16.0 +zipp==3.19.2 +requests==2.33.1 +syrupy==4.6.1 +# test with the latest version of opentelemetry-api, sdk, and semantic conventions + +-e resource/opentelemetry-resource-detector-gcp diff --git a/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr b/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr new file mode 100644 index 0000000000..cc40be8cbd --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr @@ -0,0 +1,87 @@ +# serializer version: 1 +# 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', + }) +# --- +# 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', + }) +# --- +# name: test_detects_empty_as_fallback + dict({ + }) +# --- +# name: test_detects_empty_when_not_available + 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', + }) +# --- +# 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', + }) +# --- +# name: test_detects_gce + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.availability_zone': 'us-east4-b', + 'cloud.platform': 'gcp_compute_engine', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'host.id': '0087244a', + 'host.name': 'fakeName', + 'host.type': 'fakeMachineType', + }) +# --- +# name: test_detects_gke[regional] + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.platform': 'gcp_kubernetes_engine', + 'cloud.provider': 'gcp', + 'cloud.region': 'us-east4', + 'host.id': '12345', + 'k8s.cluster.name': 'fakeClusterName', + }) +# --- +# name: test_detects_gke[zonal] + dict({ + 'cloud.account.id': 'fakeProject', + 'cloud.availability_zone': 'us-east4-b', + 'cloud.platform': 'gcp_kubernetes_engine', + 'cloud.provider': 'gcp', + 'host.id': '12345', + 'k8s.cluster.name': 'fakeClusterName', + }) +# --- diff --git a/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr b/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr new file mode 100644 index 0000000000..34c4313ba9 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr @@ -0,0 +1,229 @@ +# serializer version: 1 +# name: test_get_monitored_resource[aws ec2 region fallback] + dict({ + 'labels': dict({ + 'aws_account': 'myawsaccount', + 'instance_id': 'myhostid', + 'region': 'myregion', + }), + 'type': 'aws_ec2_instance', + }) +# --- +# name: test_get_monitored_resource[aws ec2] + dict({ + 'labels': dict({ + 'aws_account': 'myawsaccount', + 'instance_id': 'myhostid', + 'region': 'myavailzone', + }), + 'type': 'aws_ec2_instance', + }) +# --- +# name: test_get_monitored_resource[empty] + dict({ + 'labels': dict({ + 'location': 'global', + 'namespace': '', + 'node_id': '', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[fallback generic node] + dict({ + 'labels': dict({ + 'location': 'global', + 'namespace': '', + 'node_id': '', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[gce instance] + dict({ + 'labels': dict({ + 'instance_id': 'myhost', + 'zone': 'foo', + }), + 'type': 'gce_instance', + }) +# --- +# name: test_get_monitored_resource[generic node fallback global] + dict({ + 'labels': dict({ + 'location': 'global', + 'namespace': 'servicens', + 'node_id': 'hostid', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[generic node fallback host name] + dict({ + 'labels': dict({ + 'location': 'global', + 'namespace': 'servicens', + 'node_id': 'hostname', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[generic node fallback region] + dict({ + 'labels': dict({ + 'location': 'myregion', + 'namespace': 'servicens', + 'node_id': 'hostid', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[generic node] + dict({ + 'labels': dict({ + 'location': 'myavailzone', + 'namespace': 'servicens', + 'node_id': 'hostid', + }), + 'type': 'generic_node', + }) +# --- +# name: test_get_monitored_resource[generic task faas fallback] + dict({ + 'labels': dict({ + 'job': 'unknown_service', + 'location': 'myregion', + 'namespace': 'servicens', + 'task_id': 'faasinstance', + }), + 'type': 'generic_task', + }) +# --- +# name: test_get_monitored_resource[generic task faas] + dict({ + 'labels': dict({ + 'job': 'faasname', + 'location': 'myregion', + 'namespace': 'servicens', + 'task_id': 'faasinstance', + }), + 'type': 'generic_task', + }) +# --- +# name: test_get_monitored_resource[generic task fallback global] + dict({ + 'labels': dict({ + 'job': 'servicename', + 'location': 'global', + 'namespace': 'servicens', + 'task_id': 'serviceinstanceid', + }), + 'type': 'generic_task', + }) +# --- +# name: test_get_monitored_resource[generic task fallback region] + dict({ + 'labels': dict({ + 'job': 'servicename', + 'location': 'myregion', + 'namespace': 'servicens', + 'task_id': 'serviceinstanceid', + }), + 'type': 'generic_task', + }) +# --- +# name: test_get_monitored_resource[generic task] + dict({ + 'labels': dict({ + 'job': 'servicename', + 'location': 'myavailzone', + 'namespace': 'servicens', + 'task_id': 'serviceinstanceid', + }), + 'type': 'generic_task', + }) +# --- +# name: test_get_monitored_resource[k8s cluster region fallback] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myregion', + }), + 'type': 'k8s_cluster', + }) +# --- +# name: test_get_monitored_resource[k8s cluster] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myavailzone', + }), + 'type': 'k8s_cluster', + }) +# --- +# name: test_get_monitored_resource[k8s container region fallback] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'container_name': 'mycontainer', + 'location': 'myregion', + 'namespace_name': 'myns', + 'pod_name': 'mypod', + }), + 'type': 'k8s_container', + }) +# --- +# name: test_get_monitored_resource[k8s container] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'container_name': 'mycontainer', + 'location': 'myavailzone', + 'namespace_name': 'myns', + 'pod_name': 'mypod', + }), + 'type': 'k8s_container', + }) +# --- +# name: test_get_monitored_resource[k8s node region fallback] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myregion', + 'node_name': 'mynode', + }), + 'type': 'k8s_node', + }) +# --- +# name: test_get_monitored_resource[k8s node] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myavailzone', + 'node_name': 'mynode', + }), + 'type': 'k8s_node', + }) +# --- +# name: test_get_monitored_resource[k8s pod region fallback] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myregion', + 'namespace_name': 'myns', + 'pod_name': 'mypod', + }), + 'type': 'k8s_pod', + }) +# --- +# name: test_get_monitored_resource[k8s pod] + dict({ + 'labels': dict({ + 'cluster_name': 'mycluster', + 'location': 'myavailzone', + 'namespace_name': 'myns', + 'pod_name': 'mypod', + }), + 'type': 'k8s_pod', + }) +# --- diff --git a/resource/opentelemetry-resource-detector-gcp/tests/conftest.py b/resource/opentelemetry-resource-detector-gcp/tests/conftest.py new file mode 100644 index 0000000000..af44f1d7c3 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/conftest.py @@ -0,0 +1,25 @@ +# Copyright 2023 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.resource.detector.gcp import _metadata + + +@pytest.fixture(name="fake_get_metadata") +def fixture_fake_get_metadata(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + mock = MagicMock() + monkeypatch.setattr(_metadata, "get_metadata", mock) + return mock diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_faas.py b/resource/opentelemetry-resource-detector-gcp/tests/test_faas.py new file mode 100644 index 0000000000..672097d2e8 --- /dev/null +++ b/resource/opentelemetry-resource-detector-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.resource.detector.gcp 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" diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gae.py b/resource/opentelemetry-resource-detector-gcp/tests/test_gae.py new file mode 100644 index 0000000000..8451f7c3d8 --- /dev/null +++ b/resource/opentelemetry-resource-detector-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.resource.detector.gcp 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" diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gce.py b/resource/opentelemetry-resource-detector-gcp/tests/test_gce.py new file mode 100644 index 0000000000..71fdb03892 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/test_gce.py @@ -0,0 +1,74 @@ +# Copyright 2023 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.resource.detector.gcp import ( + _gce, + _metadata, +) + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(fake_get_metadata): + pass + + +def test_detects_on_gce() -> None: + assert _gce.on_gce() + + +def test_detects_not_on_gce(fake_get_metadata: MagicMock) -> None: + # when the metadata server is not accessible + fake_get_metadata.side_effect = _metadata.MetadataAccessException() + assert not _gce.on_gce() + + # when the metadata server doesn't have the expected structure + fake_get_metadata.return_value = {} + assert not _gce.on_gce() + + +def test_detects_host_type(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"machineType": "fake"}} + assert _gce.host_type() == "fake" + + +def test_detects_host_id(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"id": 12345}} + assert _gce.host_id() == "12345" + + +def test_detects_host_name(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"name": "fake"}} + assert _gce.host_name() == "fake" + + +def test_detects_zone_and_region(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"zone": "projects/233510669999/zones/us-east4-b"} + } + zone_and_region = _gce.availability_zone_and_region() + + assert zone_and_region.zone == "us-east4-b" + assert zone_and_region.region == "us-east4" + + +def test_throws_for_invalid_zone(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"zone": ""}} + + with pytest.raises(ValueError, match="zone was not in the expected format"): + _gce.availability_zone_and_region() diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py b/resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py new file mode 100644 index 0000000000..1208c007c3 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py @@ -0,0 +1,199 @@ +# Copyright 2025 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 Mock + +import pytest +import requests +from opentelemetry.resource.detector.gcp import ( + GoogleCloudResourceDetector, + _metadata, +) + + +@pytest.fixture(name="reset_cache") +def fixture_reset_cache(): + yield + _metadata.get_metadata.cache_clear() + _metadata.is_available.cache_clear() + + +@pytest.fixture(name="fake_get") +def fixture_fake_get(monkeypatch: pytest.MonkeyPatch): + mock = Mock() + monkeypatch.setattr(requests, "get", mock) + return mock + + +@pytest.fixture(name="fake_metadata") +def fixture_fake_metadata(fake_get: Mock): + json = {"instance": {}, "project": {}} + fake_get().json.return_value = json + return json + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(reset_cache, fake_get, fake_metadata): + pass + + +def test_detects_empty_when_not_available(snapshot, fake_get: Mock): + fake_get.side_effect = requests.HTTPError() + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_empty_as_fallback(snapshot): + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +def test_detects_gce(snapshot, fake_metadata: _metadata.Metadata): + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + "name": "fakeName", + "id": "0087244a", + "machineType": "fakeMachineType", + "zone": "projects/233510669999/zones/us-east4-b", + "attributes": {}, + }, + } + ) + + assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot + + +@pytest.mark.parametrize( + "cluster_location", + ( + pytest.param("us-east4", id="regional"), + pytest.param("us-east4-b", id="zonal"), + ), +) +def test_detects_gke( + cluster_location: str, + snapshot, + fake_metadata: _metadata.Metadata, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "fakehost") + fake_metadata.update( + { + "project": {"projectId": "fakeProject"}, + "instance": { + "name": "fakeName", + "id": 12345, + "machineType": "fakeMachineType", + "zone": "projects/233510669999/zones/us-east4-b", + # Plus some attributes + "attributes": { + "cluster-name": "fakeClusterName", + "cluster-location": cluster_location, + }, + }, + } + ) + + 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 + + +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/resource/opentelemetry-resource-detector-gcp/tests/test_gke.py b/resource/opentelemetry-resource-detector-gcp/tests/test_gke.py new file mode 100644 index 0000000000..b5e0ebaed8 --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/test_gke.py @@ -0,0 +1,70 @@ +# Copyright 2023 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.resource.detector.gcp import _gke + + +def test_detects_on_gke(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "fakehost") + assert _gke.on_gke() + + +def test_detects_not_on_gke() -> None: + assert not _gke.on_gke() + + +def test_detects_host_id(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"id": 12345}} + assert _gke.host_id() == "12345" + + +def test_detects_cluster_name(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"attributes": {"cluster-name": "fake"}} + } + assert _gke.cluster_name() == "fake" + + +def test_detects_zone(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"attributes": {"cluster-location": "us-east4-b"}} + } + zone_or_region = _gke.availability_zone_or_region() + assert zone_or_region.type == "zone" + assert zone_or_region.value == "us-east4-b" + + +def test_detects_region(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"attributes": {"cluster-location": "us-east4"}} + } + zone_or_region = _gke.availability_zone_or_region() + assert zone_or_region.type == "region" + assert zone_or_region.value == "us-east4" + + +def test_throws_for_invalid_cluster_location( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"attributes": {"cluster-location": "invalid"}} + } + + with pytest.raises( + ValueError, match="unrecognized format for cluster location" + ): + _gke.availability_zone_or_region() diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py b/resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py new file mode 100644 index 0000000000..ba706705ce --- /dev/null +++ b/resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py @@ -0,0 +1,260 @@ +# Copyright 2022 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. + +import dataclasses + +import pytest +from opentelemetry.resource.detector.gcp._mapping import ( + get_monitored_resource, +) +from opentelemetry.sdk.resources import Attributes, LabelValue, Resource +from syrupy.assertion import SnapshotAssertion + + +@pytest.mark.parametrize( + "otel_attributes", + [ + # GCE + pytest.param( + { + "cloud.platform": "gcp_compute_engine", + "cloud.availability_zone": "foo", + "host.id": "myhost", + }, + id="gce instance", + ), + # k8s container + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.availability_zone": "myavailzone", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.pod.name": "mypod", + "k8s.container.name": "mycontainer", + }, + id="k8s container", + ), + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.region": "myregion", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.pod.name": "mypod", + "k8s.container.name": "mycontainer", + }, + id="k8s container region fallback", + ), + # k8s pod + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.availability_zone": "myavailzone", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.pod.name": "mypod", + }, + id="k8s pod", + ), + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.region": "myregion", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.pod.name": "mypod", + }, + id="k8s pod region fallback", + ), + # k8s node + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.availability_zone": "myavailzone", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.node.name": "mynode", + }, + id="k8s node", + ), + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.region": "myregion", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + "k8s.node.name": "mynode", + }, + id="k8s node region fallback", + ), + # k8s cluster + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.availability_zone": "myavailzone", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + }, + id="k8s cluster", + ), + pytest.param( + { + "cloud.platform": "gcp_kubernetes_engine", + "cloud.region": "myregion", + "k8s.cluster.name": "mycluster", + "k8s.namespace.name": "myns", + }, + id="k8s cluster region fallback", + ), + # aws ec2 + pytest.param( + { + "cloud.platform": "aws_ec2", + "cloud.availability_zone": "myavailzone", + "host.id": "myhostid", + "cloud.account.id": "myawsaccount", + }, + id="aws ec2", + ), + pytest.param( + { + "cloud.platform": "aws_ec2", + "cloud.region": "myregion", + "host.id": "myhostid", + "cloud.account.id": "myawsaccount", + }, + id="aws ec2 region fallback", + ), + # generic task + pytest.param( + { + "cloud.availability_zone": "myavailzone", + "service.namespace": "servicens", + "service.name": "servicename", + "service.instance.id": "serviceinstanceid", + }, + id="generic task", + ), + pytest.param( + { + "cloud.region": "myregion", + "service.namespace": "servicens", + "service.name": "servicename", + "service.instance.id": "serviceinstanceid", + }, + id="generic task fallback region", + ), + pytest.param( + { + "service.namespace": "servicens", + "service.name": "servicename", + "service.instance.id": "serviceinstanceid", + }, + id="generic task fallback global", + ), + pytest.param( + { + "service.name": "unknown_service", + "cloud.region": "myregion", + "service.namespace": "servicens", + "faas.name": "faasname", + "faas.instance": "faasinstance", + }, + id="generic task faas", + ), + pytest.param( + { + "service.name": "unknown_service", + "cloud.region": "myregion", + "service.namespace": "servicens", + "faas.instance": "faasinstance", + }, + id="generic task faas fallback", + ), + # generic node + pytest.param( + { + "cloud.availability_zone": "myavailzone", + "service.namespace": "servicens", + "service.name": "servicename", + "host.id": "hostid", + }, + id="generic node", + ), + pytest.param( + { + "cloud.region": "myregion", + "service.namespace": "servicens", + "service.name": "servicename", + "host.id": "hostid", + }, + id="generic node fallback region", + ), + pytest.param( + { + "service.namespace": "servicens", + "service.name": "servicename", + "host.id": "hostid", + }, + id="generic node fallback global", + ), + pytest.param( + { + "service.namespace": "servicens", + "service.name": "servicename", + "host.name": "hostname", + }, + id="generic node fallback host name", + ), + # fallback empty + pytest.param( + {"foo": "bar", "no.useful": "resourceattribs"}, + id="fallback generic node", + ), + pytest.param( + {}, + id="empty", + ), + ], +) +def test_get_monitored_resource( + otel_attributes: Attributes, snapshot: SnapshotAssertion +) -> None: + resource = Resource(otel_attributes) + monitored_resource_data = get_monitored_resource(resource) + as_dict = dataclasses.asdict(monitored_resource_data) + assert as_dict == snapshot + + +@pytest.mark.parametrize( + ("value", "expect"), + [ + (None, ""), + (123, "123"), + (123.4, "123.4"), + ([1, 2, 3, 4], "[1,2,3,4]"), + ([1.1, 2.2, 3.3, 4.4], "[1.1,2.2,3.3,4.4]"), + (["a", "b", "c", "d"], '["a","b","c","d"]'), + ], +) +def test_non_string_values(value: LabelValue, expect: str): + # host.id will end up in generic_node's node_id label + monitored_resource_data = get_monitored_resource( + Resource({"host.id": value}) + ) + assert monitored_resource_data is not None + + value_as_gcm_label = monitored_resource_data.labels["node_id"] + assert value_as_gcm_label == expect diff --git a/tox.ini b/tox.ini index 8e119a3fdd..05f2c6c523 100644 --- a/tox.ini +++ b/tox.ini @@ -51,6 +51,11 @@ envlist = pypy3-test-resource-detector-azure-{0,1} lint-resource-detector-azure + ; opentelemetry-resource-detector-gcp + py3{10,11,12,13,14}-test-resource-detector-gcp-{0,1} + pypy3-test-resource-detector-gcp-{0,1} + lint-resource-detector-gcp + ; opentelemetry-sdk-extension-aws py3{10,11,12,13,14}-test-sdk-extension-aws-{0,1} pypy3-test-sdk-extension-aws-{0,1} @@ -763,6 +768,14 @@ deps = resource-detector-azure-1: -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements-1.txt lint-resource-detector-azure: -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements-0.txt + # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API + # and SDK supported to test we are honoring it + resource-detector-gcp-0: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt + # and the latest version of OTel API and SDK + resource-detector-gcp-1: {[testenv]test_deps} + resource-detector-gcp-1: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt + lint-resource-detector-gcp: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt + propagator-ot-trace: {[testenv]test_deps} propagator-ot-trace: -r {toxinidir}/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt @@ -1021,6 +1034,9 @@ commands = test-resource-detector-azure: pytest {toxinidir}/resource/opentelemetry-resource-detector-azure/tests {posargs} lint-resource-detector-azure: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-azure" + test-resource-detector-gcp: pytest {toxinidir}/resource/opentelemetry-resource-detector-gcp/tests {posargs} + lint-resource-detector-gcp: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-gcp" + test-processor-baggage: pytest {toxinidir}/processor/opentelemetry-processor-baggage/tests {posargs} lint-processor-baggage: sh -c "cd processor && pylint --rcfile ../.pylintrc opentelemetry-processor-baggage" From 061ae133ea96b7baacb2d956423aa73c77681769 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 26 Jun 2026 20:48:29 +0000 Subject: [PATCH 2/5] Rename package to original name.. --- eachdist.ini | 2 +- .../.changelog/.gitignore | 0 .../CHANGELOG.md | 0 .../LICENSE | 0 .../MANIFEST.rst | 0 .../README.rst | 6 +++--- .../pyproject.toml | 4 ++-- .../resource/detector/gcp/__init__.py | 0 .../resource/detector/gcp/_constants.py | 0 .../resource/detector/gcp/_faas.py | 0 .../resource/detector/gcp/_gae.py | 0 .../resource/detector/gcp/_gce.py | 0 .../resource/detector/gcp/_gke.py | 0 .../resource/detector/gcp/_mapping.py | 0 .../resource/detector/gcp/_metadata.py | 0 .../resource/detector/gcp/version.py | 0 .../test-requirements-0.txt | 2 +- .../test-requirements-1.txt | 2 +- .../test_gcp_resource_detector.ambr | 0 .../tests/__snapshots__/test_mapping.ambr | 0 .../tests/conftest.py | 0 .../tests/test_faas.py | 0 .../tests/test_gae.py | 0 .../tests/test_gce.py | 0 .../tests/test_gcp_resource_detector.py | 0 .../tests/test_gke.py | 0 .../tests/test_mapping.py | 0 tox.ini | 20 +++++++++---------- 28 files changed, 18 insertions(+), 18 deletions(-) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/.changelog/.gitignore (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/CHANGELOG.md (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/LICENSE (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/MANIFEST.rst (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/README.rst (90%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/pyproject.toml (94%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/__init__.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_constants.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_faas.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_gae.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_gce.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_gke.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_mapping.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/_metadata.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/src/opentelemetry/resource/detector/gcp/version.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/test-requirements-0.txt (87%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/test-requirements-1.txt (85%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/__snapshots__/test_gcp_resource_detector.ambr (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/__snapshots__/test_mapping.ambr (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/conftest.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_faas.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_gae.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_gce.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_gcp_resource_detector.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_gke.py (100%) rename resource/{opentelemetry-resource-detector-gcp => opentelemetry-resourcedetector-gcp}/tests/test_mapping.py (100%) diff --git a/eachdist.ini b/eachdist.ini index 71b68ef75c..03fad476e1 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -51,7 +51,7 @@ packages= opentelemetry-opamp-client opentelemetry-propagator-aws-xray opentelemetry-resource-detector-azure - opentelemetry-resource-detector-gcp + opentelemetry-resourcedetector-gcp opentelemetry-sdk-extension-aws opentelemetry-propagator-aws-xray opentelemetry-instrumentation-anthropic diff --git a/resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore b/resource/opentelemetry-resourcedetector-gcp/.changelog/.gitignore similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/.changelog/.gitignore rename to resource/opentelemetry-resourcedetector-gcp/.changelog/.gitignore diff --git a/resource/opentelemetry-resource-detector-gcp/CHANGELOG.md b/resource/opentelemetry-resourcedetector-gcp/CHANGELOG.md similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/CHANGELOG.md rename to resource/opentelemetry-resourcedetector-gcp/CHANGELOG.md diff --git a/resource/opentelemetry-resource-detector-gcp/LICENSE b/resource/opentelemetry-resourcedetector-gcp/LICENSE similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/LICENSE rename to resource/opentelemetry-resourcedetector-gcp/LICENSE diff --git a/resource/opentelemetry-resource-detector-gcp/MANIFEST.rst b/resource/opentelemetry-resourcedetector-gcp/MANIFEST.rst similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/MANIFEST.rst rename to resource/opentelemetry-resourcedetector-gcp/MANIFEST.rst diff --git a/resource/opentelemetry-resource-detector-gcp/README.rst b/resource/opentelemetry-resourcedetector-gcp/README.rst similarity index 90% rename from resource/opentelemetry-resource-detector-gcp/README.rst rename to resource/opentelemetry-resourcedetector-gcp/README.rst index 633a3452c4..5defc043c8 100644 --- a/resource/opentelemetry-resource-detector-gcp/README.rst +++ b/resource/opentelemetry-resourcedetector-gcp/README.rst @@ -3,8 +3,8 @@ OpenTelemetry Google Cloud Resource Detector |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-resource-detector-gcp.svg - :target: https://pypi.org/project/opentelemetry-resource-detector-gcp/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp.svg + :target: https://pypi.org/project/opentelemetry-resourcedetector-gcp/ This library provides support for detecting GCP resources like GCE, GKE, etc. @@ -13,7 +13,7 @@ Installation :: - pip install opentelemetry-resource-detector-gcp + pip install opentelemetry-resourcedetector-gcp Usage ----- diff --git a/resource/opentelemetry-resource-detector-gcp/pyproject.toml b/resource/opentelemetry-resourcedetector-gcp/pyproject.toml similarity index 94% rename from resource/opentelemetry-resource-detector-gcp/pyproject.toml rename to resource/opentelemetry-resourcedetector-gcp/pyproject.toml index b9ac566397..517b88a69f 100644 --- a/resource/opentelemetry-resource-detector-gcp/pyproject.toml +++ b/resource/opentelemetry-resourcedetector-gcp/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "opentelemetry-resource-detector-gcp" +name = "opentelemetry-resourcedetector-gcp" dynamic = ["version"] description = "Google Cloud Resource Detector for OpenTelemetry" readme = "README.rst" @@ -34,7 +34,7 @@ dependencies = [ gcp = "opentelemetry.resource.detector.gcp:GoogleCloudResourceDetector" [project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resource-detector-gcp" +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/__init__.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_constants.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_faas.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gae.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gce.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_gke.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_mapping.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/_metadata.py diff --git a/resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/src/opentelemetry/resource/detector/gcp/version.py rename to resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py diff --git a/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt b/resource/opentelemetry-resourcedetector-gcp/test-requirements-0.txt similarity index 87% rename from resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt rename to resource/opentelemetry-resourcedetector-gcp/test-requirements-0.txt index 84ac1449bd..6c276610b2 100644 --- a/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt +++ b/resource/opentelemetry-resourcedetector-gcp/test-requirements-0.txt @@ -13,4 +13,4 @@ syrupy==4.6.1 opentelemetry-api==1.30 # when updating, also update in pyproject.toml opentelemetry-sdk==1.30 # when updating, also update in pyproject.toml --e resource/opentelemetry-resource-detector-gcp +-e resource/opentelemetry-resourcedetector-gcp diff --git a/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt b/resource/opentelemetry-resourcedetector-gcp/test-requirements-1.txt similarity index 85% rename from resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt rename to resource/opentelemetry-resourcedetector-gcp/test-requirements-1.txt index 4bf62f6335..ab80c9ba19 100644 --- a/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt +++ b/resource/opentelemetry-resourcedetector-gcp/test-requirements-1.txt @@ -12,4 +12,4 @@ requests==2.33.1 syrupy==4.6.1 # test with the latest version of opentelemetry-api, sdk, and semantic conventions --e resource/opentelemetry-resource-detector-gcp +-e resource/opentelemetry-resourcedetector-gcp diff --git a/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr b/resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr rename to resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr diff --git a/resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr b/resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/__snapshots__/test_mapping.ambr rename to resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr diff --git a/resource/opentelemetry-resource-detector-gcp/tests/conftest.py b/resource/opentelemetry-resourcedetector-gcp/tests/conftest.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/conftest.py rename to resource/opentelemetry-resourcedetector-gcp/tests/conftest.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_faas.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_faas.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_faas.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_faas.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gae.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_gae.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_gae.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_gae.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gce.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_gce.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_gce.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_gce.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_gcp_resource_detector.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_gke.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_gke.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_gke.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_gke.py diff --git a/resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py b/resource/opentelemetry-resourcedetector-gcp/tests/test_mapping.py similarity index 100% rename from resource/opentelemetry-resource-detector-gcp/tests/test_mapping.py rename to resource/opentelemetry-resourcedetector-gcp/tests/test_mapping.py diff --git a/tox.ini b/tox.ini index 05f2c6c523..52dcb40b37 100644 --- a/tox.ini +++ b/tox.ini @@ -51,10 +51,10 @@ envlist = pypy3-test-resource-detector-azure-{0,1} lint-resource-detector-azure - ; opentelemetry-resource-detector-gcp - py3{10,11,12,13,14}-test-resource-detector-gcp-{0,1} - pypy3-test-resource-detector-gcp-{0,1} - lint-resource-detector-gcp + ; opentelemetry-resourcedetector-gcp + py3{10,11,12,13,14}-test-resourcedetector-gcp-{0,1} + pypy3-test-resourcedetector-gcp-{0,1} + lint-resourcedetector-gcp ; opentelemetry-sdk-extension-aws py3{10,11,12,13,14}-test-sdk-extension-aws-{0,1} @@ -770,11 +770,11 @@ deps = # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it - resource-detector-gcp-0: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt + resourcedetector-gcp-0: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/test-requirements-0.txt # and the latest version of OTel API and SDK - resource-detector-gcp-1: {[testenv]test_deps} - resource-detector-gcp-1: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-1.txt - lint-resource-detector-gcp: -r {toxinidir}/resource/opentelemetry-resource-detector-gcp/test-requirements-0.txt + resourcedetector-gcp-1: {[testenv]test_deps} + resourcedetector-gcp-1: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/test-requirements-1.txt + lint-resourcedetector-gcp: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/test-requirements-0.txt propagator-ot-trace: {[testenv]test_deps} propagator-ot-trace: -r {toxinidir}/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt @@ -1034,8 +1034,8 @@ commands = test-resource-detector-azure: pytest {toxinidir}/resource/opentelemetry-resource-detector-azure/tests {posargs} lint-resource-detector-azure: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-azure" - test-resource-detector-gcp: pytest {toxinidir}/resource/opentelemetry-resource-detector-gcp/tests {posargs} - lint-resource-detector-gcp: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-gcp" + test-resourcedetector-gcp: pytest {toxinidir}/resource/opentelemetry-resourcedetector-gcp/tests {posargs} + lint-resourcedetector-gcp: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resourcedetector-gcp" test-processor-baggage: pytest {toxinidir}/processor/opentelemetry-processor-baggage/tests {posargs} lint-processor-baggage: sh -c "cd processor && pylint --rcfile ../.pylintrc opentelemetry-processor-baggage" From 0bdf3cd707e37b5acc9cef1499f5893a8785ccd2 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 26 Jun 2026 20:51:42 +0000 Subject: [PATCH 3/5] Tweak readme --- resource/opentelemetry-resourcedetector-gcp/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource/opentelemetry-resourcedetector-gcp/README.rst b/resource/opentelemetry-resourcedetector-gcp/README.rst index 5defc043c8..f39a87ae09 100644 --- a/resource/opentelemetry-resourcedetector-gcp/README.rst +++ b/resource/opentelemetry-resourcedetector-gcp/README.rst @@ -26,7 +26,7 @@ Usage from opentelemetry import trace from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, Resource - # This will use the GoogleCloudResourceDetector under the covers. + # This will use the GoogleCloudResourceDetector under the covers if it's installed in the python environment. resource = Resource.create( attributes={ # Use the PID as the service.instance.id to avoid duplicate timeseries From aee49e0dc7bb36522789be8eec324011d5834f2c Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 26 Jun 2026 20:55:06 +0000 Subject: [PATCH 4/5] Change verison.. --- .../src/opentelemetry/resource/detector/gcp/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py index 98ec6d01b8..5d1e81efd8 100644 --- a/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py +++ b/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resource/detector/gcp/version.py @@ -1,4 +1,4 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "0.1.0.dev" +__version__ = "1.13.0.dev" From 3f366cde8523afcf66ca5f8ebcd612a16a6cd0dc Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 26 Jun 2026 21:00:10 +0000 Subject: [PATCH 5/5] Add changelog --- .../opentelemetry-resourcedetector-gcp/.changelog/4749.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 resource/opentelemetry-resourcedetector-gcp/.changelog/4749.added diff --git a/resource/opentelemetry-resourcedetector-gcp/.changelog/4749.added b/resource/opentelemetry-resourcedetector-gcp/.changelog/4749.added new file mode 100644 index 0000000000..510f34ea0a --- /dev/null +++ b/resource/opentelemetry-resourcedetector-gcp/.changelog/4749.added @@ -0,0 +1 @@ +Moved the Google Cloud resource detector package from the opentelemetry-operations-python repository to the opentelemetry-python-contrib repo.