From c67c7e7a5222f5ee4a6aa6ba6a5e3f7fd9a0bf02 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Tue, 21 Jul 2026 19:12:27 +0000 Subject: [PATCH 01/16] Add deprecation warning and migration guide --- MIGRATION.md | 229 ++++++++++++++++++ opentelemetry-exporter-gcp-logging/setup.cfg | 2 + .../exporter/cloud_logging/__init__.py | 5 + .../tests/test_cloud_logging.py | 9 + .../setup.cfg | 2 + .../exporter/cloud_monitoring/__init__.py | 5 + .../tests/test_cloud_monitoring.py | 6 + opentelemetry-exporter-gcp-trace/setup.cfg | 2 + .../exporter/cloud_trace/__init__.py | 5 + .../tests/test_cloud_trace_exporter.py | 5 + 10 files changed, 270 insertions(+) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..3d728e32 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,229 @@ +# Migration Guide + +This guide provides instructions on how to migrate from the custom exporters in this repository to the standard OpenTelemetry OTLP exporters. + +## Overview + +Google Cloud supports native OTLP (OpenTelemetry Protocol) ingestion for Cloud Trace and Cloud Monitoring via the [Telemetry API](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/overview). This allows you to use standard OpenTelemetry OTLP exporters for sending telemetry data to Google Cloud. + +## Deprecation Notice + +All exporters in this repository are deprecated. Please migrate to the standard OTLP exporters using standard OpenTelemetry libraries. This repository will be +archived soon. + +--- + +## Migrate from OpenTelemetry Google Cloud Trace Exporter (`CloudTraceSpanExporter`) to OTLP Exporter + +To migrate from `opentelemetry-exporter-gcp-trace` (`CloudTraceSpanExporter`) to the standard OpenTelemetry OTLP exporter, follow these steps: + +### 1. Add Dependencies + +Install the standard OpenTelemetry OTLP exporter and GCP authentication dependencies: + +```bash +pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio requests +``` + +### 2. Configure the SDK + +You can configure the SDK using environment variables: + +```bash +# Environment Variables +export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" +export OTEL_TRACES_EXPORTER="otlp" +export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +``` + +Or programmatically in Python using GCP authentication: + +```python +import google.auth +import google.auth.transport.grpc +import google.auth.transport.requests +import grpc +from google.auth.transport.grpc import AuthMetadataPlugin +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +# Configure GCP Authentication +credentials, project_id = google.auth.default() +request = google.auth.transport.requests.Request() +auth_metadata_plugin = AuthMetadataPlugin( + credentials=credentials, request=request +) +channel_creds = grpc.composite_channel_credentials( + grpc.ssl_channel_credentials(), + grpc.metadata_call_credentials(auth_metadata_plugin), +) + +trace.set_tracer_provider(TracerProvider()) + +otlp_exporter = OTLPSpanExporter( + endpoint="telemetry.googleapis.com", + credentials=channel_creds, +) +trace.get_tracer_provider().add_span_processor( + BatchSpanProcessor(otlp_exporter) +) +``` + +### 3. Follow the Migration Guide + +For more details, follow the official Google Cloud guide: [Migrate from the Trace exporter to the OTLP endpoint](https://docs.cloud.google.com/trace/docs/migrate-to-otlp-endpoints). + +### Mapping and Limitations + +#### Configuration Mapping + +| `CloudTraceSpanExporter` Parameter | OTLP Equivalent Property / Env Var | Notes | +| :--- | :--- | :--- | +| `project_id` | Use resource attribute: `gcp.project_id` | Set via `OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id"`. | +| `client` | N/A | Pre-configured `TraceServiceClient` is replaced by HTTP/gRPC OTLP exporter configuration. | +| `resource_regex` | Resource Attributes / Processors | Use standard OpenTelemetry Resource attributes instead. | + +--- + +## Migrate from OpenTelemetry Google Cloud Monitoring Exporter (`CloudMonitoringMetricsExporter`) to OTLP Exporter + +> [!WARNING] +> **Breaking Change Warning:** Migrating from the legacy Google Cloud Monitoring exporter to the standard OTLP exporter introduces breaking changes to your metric names. +> +> * **Legacy Exporter:** Ingests metrics under the `workload.googleapis.com/` domain (unless a custom prefix was configured). +> * **OTLP Exporter:** Ingests metrics under the `prometheus.googleapis.com/` domain by default. +> +> Because of this domain change, your metric names in Cloud Monitoring will change. **This will break any existing dashboards, alerting policies, and cause data discontinuity** between your historical and new metrics. + +### Why Migrate? + +Transitioning to the standard OTLP exporter is recommended for the following reasons: +* **Standardization:** Aligns your application with the industry-standard OpenTelemetry Protocol (OTLP). +* **Google Managed Prometheus (GMP):** Standard OTLP metrics are ingested into Google Managed Prometheus, offering robust, scalable, and cost-effective monitoring. +* **Future-proofing:** The legacy Google Cloud exporters in this repository are deprecated. + +### Strategy & Steps + +#### 1. Add Dependencies + +```bash +pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio +``` + +#### 2. Configure Environment Variables + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" +export OTEL_METRICS_EXPORTER="otlp" +export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=$PROJECT_ID,location=us-central1,service.name=otlp-sample,service.instance.id=1" +``` + +#### 3. Programmatic Configuration + +```python +import google.auth +import google.auth.transport.grpc +import google.auth.transport.requests +import grpc +from google.auth.transport.grpc import AuthMetadataPlugin +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + +# Configure GCP Authentication +credentials, project_id = google.auth.default() +request = google.auth.transport.requests.Request() +auth_metadata_plugin = AuthMetadataPlugin( + credentials=credentials, request=request +) +channel_creds = grpc.composite_channel_credentials( + grpc.ssl_channel_credentials(), + grpc.metadata_call_credentials(auth_metadata_plugin), +) + +exporter = OTLPMetricExporter( + endpoint="telemetry.googleapis.com", + credentials=channel_creds, +) +reader = PeriodicExportingMetricReader(exporter) +provider = MeterProvider(metric_readers=[reader]) +metrics.set_meter_provider(provider) +``` + +--- + +## Migrate from OpenTelemetry Google Cloud Logging Exporter (`CloudLoggingExporter`) to OTLP Exporter + +The OTLP `LogRecord` to Cloud Logging `LogEntry` conversion logic in the `CloudLoggingExporter` is similar but not identical to the Google OTLP endpoint conversion logic. + +The Google OTLP endpoint `LogRecord` to Cloud Logging `LogEntry` conversion logic is described here: https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry. + +The conversion logic in the `CloudLoggingExporter` can be found here: https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py. + +You can send the same OTLP `LogRecord` to the Google OTLP endpoint and to Cloud Logging via the `CloudLoggingExporter` to see exactly how the conversion logic diverges for a particular log if at all. + +To migrate from `opentelemetry-exporter-gcp-logging` (`CloudLoggingExporter`) to the standard OpenTelemetry OTLP log exporter, follow these steps: + +### 1. Add Dependencies + +Install the standard OpenTelemetry OTLP exporter package: + +```bash +pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio +``` + +### 2. Configure Environment Variables + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" +export OTEL_LOGS_EXPORTER="otlp" +export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=$PROJECT_ID,service.name=otlp-sample,service.instance.id=1" +``` + +### 3. Programmatic Configuration + +```python +import logging +import google.auth +import google.auth.transport.grpc +import google.auth.transport.requests +import grpc +from google.auth.transport.grpc import AuthMetadataPlugin +from opentelemetry import _logs +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + +# Configure GCP Authentication +credentials, project_id = google.auth.default() +request = google.auth.transport.requests.Request() +auth_metadata_plugin = AuthMetadataPlugin( + credentials=credentials, request=request +) +channel_creds = grpc.composite_channel_credentials( + grpc.ssl_channel_credentials(), + grpc.metadata_call_credentials(auth_metadata_plugin), +) + +logger_provider = LoggerProvider() +_logs.set_logger_provider(logger_provider) + +otlp_log_exporter = OTLPLogExporter( + endpoint="telemetry.googleapis.com", + credentials=channel_creds, +) +logger_provider.add_log_record_processor( + BatchLogRecordProcessor(otlp_log_exporter) +) +``` diff --git a/opentelemetry-exporter-gcp-logging/setup.cfg b/opentelemetry-exporter-gcp-logging/setup.cfg index bdbd8013..e97e5ab0 100644 --- a/opentelemetry-exporter-gcp-logging/setup.cfg +++ b/opentelemetry-exporter-gcp-logging/setup.cfg @@ -31,6 +31,8 @@ install_requires = opentelemetry-api >= 1.39.0 opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.* + typing-extensions + [options.packages.find] where = src diff --git a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py index 51755742..a2d68793 100644 --- a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py +++ b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py @@ -18,6 +18,7 @@ import json import logging import re +from typing_extensions import deprecated from base64 import b64encode from functools import partial from typing import ( @@ -254,6 +255,10 @@ def _get_monitored_resource( ) +@deprecated( + "CloudLoggingExporter is deprecated. See migration guide at " + "https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/MIGRATION.md" +) class CloudLoggingExporter(LogRecordExporter): def __init__( self, diff --git a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py index e7fbf2ac..1118f1c4 100644 --- a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py +++ b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py @@ -446,3 +446,12 @@ def test_structured_json_lines(): {"logging.googleapis.com/labels":{"event.name":"foo","key":"4"},"logging.googleapis.com/spanId":"0000000000000016","logging.googleapis.com/trace":"projects/fakeproject/traces/00000000000000000000000000000019","logging.googleapis.com/trace_sampled":false,"message":"hello","severity":"ERROR","time":"2025-01-15T21:25:10.997977393Z"} """ ), "Each `LogData` should be on its own line" + + +def test_deprecation_warning(): + buf = StringIO() + with pytest.deprecated_call(): + CloudLoggingExporter( + project_id=PROJECT_ID, structured_json_file=buf + ) + diff --git a/opentelemetry-exporter-gcp-monitoring/setup.cfg b/opentelemetry-exporter-gcp-monitoring/setup.cfg index 9f1886a0..e57f0b5b 100644 --- a/opentelemetry-exporter-gcp-monitoring/setup.cfg +++ b/opentelemetry-exporter-gcp-monitoring/setup.cfg @@ -30,6 +30,8 @@ install_requires = opentelemetry-api ~= 1.30 opentelemetry-sdk ~= 1.30 opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.* + typing-extensions + [options.packages.find] where = src diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index c5dff791..fa7b3e38 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -15,6 +15,7 @@ import logging import math import random +from typing_extensions import deprecated from dataclasses import replace from time import time_ns from typing import Dict, List, NoReturn, Optional, Set, Union @@ -92,6 +93,10 @@ # pylint: disable=no-member # pylint: disable=too-many-branches # pylint: disable=too-many-locals +@deprecated( + "CloudMonitoringMetricsExporter is deprecated. See migration guide at " + "https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/MIGRATION.md" +) class CloudMonitoringMetricsExporter(MetricExporter): """Implementation of Metrics Exporter to Google Cloud Monitoring diff --git a/opentelemetry-exporter-gcp-monitoring/tests/test_cloud_monitoring.py b/opentelemetry-exporter-gcp-monitoring/tests/test_cloud_monitoring.py index aeec456b..2ac35d1e 100644 --- a/opentelemetry-exporter-gcp-monitoring/tests/test_cloud_monitoring.py +++ b/opentelemetry-exporter-gcp-monitoring/tests/test_cloud_monitoring.py @@ -54,6 +54,12 @@ } +def test_deprecation_warning() -> None: + client = MetricServiceClient(credentials=AnonymousCredentials()) + with pytest.deprecated_call(): + CloudMonitoringMetricsExporter(project_id=PROJECT_ID, client=client) + + def test_create_monitoring_exporter() -> None: client = MetricServiceClient(credentials=AnonymousCredentials()) CloudMonitoringMetricsExporter(project_id=PROJECT_ID, client=client) diff --git a/opentelemetry-exporter-gcp-trace/setup.cfg b/opentelemetry-exporter-gcp-trace/setup.cfg index c7340d49..5219ec47 100644 --- a/opentelemetry-exporter-gcp-trace/setup.cfg +++ b/opentelemetry-exporter-gcp-trace/setup.cfg @@ -30,6 +30,8 @@ install_requires = opentelemetry-api ~= 1.30 opentelemetry-sdk ~= 1.30 opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.* + typing-extensions + [options.packages.find] where = src diff --git a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index fb7853dd..cf42df51 100644 --- a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -72,6 +72,7 @@ import logging import re +from typing_extensions import deprecated from collections.abc import Sequence as SequenceABC from os import environ from typing import ( @@ -152,6 +153,10 @@ def _create_default_client() -> TraceServiceClient: ) +@deprecated( + "CloudTraceSpanExporter is deprecated. See migration guide at " + "https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/MIGRATION.md" +) class CloudTraceSpanExporter(SpanExporter): """Cloud Trace span exporter for OpenTelemetry. diff --git a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py index 3dcbaf6b..f4537cf9 100644 --- a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py +++ b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py @@ -63,6 +63,11 @@ def tearDown(self): for patcher in self.patchers: patcher.stop() + def test_deprecation_warning(self): + with self.assertWarns(DeprecationWarning): + CloudTraceSpanExporter(project_id=self.project_id) + + @classmethod def setUpClass(cls): cls.project_id = "PROJECT" From 663b18529dc45402c57c8da1b4ce4b923a63561e Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 22 Jul 2026 20:27:04 +0000 Subject: [PATCH 02/16] Delete resource detector --- .../CHANGELOG.md | 146 ---------- opentelemetry-resourcedetector-gcp/LICENSE | 201 -------------- .../MANIFEST.in | 9 - opentelemetry-resourcedetector-gcp/mypy.ini | 4 - opentelemetry-resourcedetector-gcp/setup.cfg | 43 --- opentelemetry-resourcedetector-gcp/setup.py | 34 --- .../src/opentelemetry/py.typed | 0 .../gcp_resource_detector/__init__.py | 143 ---------- .../gcp_resource_detector/_constants.py | 69 ----- .../gcp_resource_detector/_faas.py | 60 ---- .../gcp_resource_detector/_gae.py | 88 ------ .../gcp_resource_detector/_gce.py | 72 ----- .../gcp_resource_detector/_gke.py | 56 ---- .../gcp_resource_detector/_mapping.py | 222 --------------- .../gcp_resource_detector/_metadata.py | 93 ------- .../gcp_resource_detector/version.py | 15 - .../test_gcp_resource_detector.ambr | 86 ------ .../tests/__snapshots__/test_mapping.ambr | 228 --------------- .../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 ------------------ 25 files changed, 2351 deletions(-) delete mode 100644 opentelemetry-resourcedetector-gcp/CHANGELOG.md delete mode 100644 opentelemetry-resourcedetector-gcp/LICENSE delete mode 100644 opentelemetry-resourcedetector-gcp/MANIFEST.in delete mode 100644 opentelemetry-resourcedetector-gcp/mypy.ini delete mode 100644 opentelemetry-resourcedetector-gcp/setup.cfg delete mode 100644 opentelemetry-resourcedetector-gcp/setup.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py delete mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr delete mode 100644 opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr delete mode 100644 opentelemetry-resourcedetector-gcp/tests/conftest.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_faas.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gae.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gce.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gke.py delete mode 100644 opentelemetry-resourcedetector-gcp/tests/test_mapping.py diff --git a/opentelemetry-resourcedetector-gcp/CHANGELOG.md b/opentelemetry-resourcedetector-gcp/CHANGELOG.md deleted file mode 100644 index a83afa46..00000000 --- a/opentelemetry-resourcedetector-gcp/CHANGELOG.md +++ /dev/null @@ -1,146 +0,0 @@ -# Changelog - -## Unreleased - -## Version 1.12.0a0 - -Released 2026-04-28 - -## Version 1.11.0a0 - -Released 2025-11-04 - -## Version 1.10.0a0 - -Released 2025-10-13 - -- Update opentelemetry-api/sdk dependencies to 1.3. - -- This is a breaking change which moves our official recource detector from `opentelemetry.resourcedetector.gcp_resource_detector._detector` -into `opentelemetry.resourcedetector.gcp_resource_detector` and deletes the outdated duplicate resource detector -which used to be there. Use `from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector` -to import it. See ([#389](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/389)) for details. - -## Version 1.9.0a0 - -Released 2025-02-03 - -## Version 1.8.0a0 - -Released 2025-01-08 - -- Use a shorter connection timeout for reading metadata - ([#362](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/362)) -- Fix creation of resources in _detector - ([#366](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/366)) - -## Version 1.7.0a0 - -Released 2024-08-27 - -- Implement GAE resource detection - ([#351](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/351)) -- Implement Cloud Run and Cloud Functions faas resource detection - ([#346](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/346)) -- Small fixups for resource detector code and tests - ([#345](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/345)) -- gcp_resource_detector: add missing timeout to requests call - ([#344](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/344)) -- Add support for Python 3.12 (#343) - ([#343](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/343)) -- Don't throw and exception when raise on error is set to false - ([#293](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/293)) - -## Version 1.6.0a0 - -Released 2023-10-16 - -- Use faas.instance instead of faas.id, and bump e2e test image - ([#271](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/271)) -- Map faas.* attributes to generic_task in resource mapping - ([#273](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/273)) - -## Version 1.5.0a0 - -Released 2023-05-17 - -- Add spec compliant GCE detection - ([#231](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/231)) -- Add support for Python 3.11 - ([#240](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/240)) - -## Version 1.4.0a0 - -Released 2022-12-05 - -- Drop support for Python 3.6, add 3.10 - ([#203](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/203)) -- Update no container name behaviour - ([#186](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/186)) - -## Version 1.3.0a0 - -Released 2022-04-21 - -- remove google-auth dependency for resource detection - ([#176](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/176)) - -## Version 1.2.0a0 - -Released 2022-04-05 - -## Version 1.1.0a0 - -Released 2022-01-13 - -## Version 1.0.0a0 - -Released 2021-04-22 - -- Drop support for Python 3.5 - ([#123](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/123)) -- Split tools package into separate propagator and resource detector packages - ([#124](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/124)) - -## Version 0.18b0 - -Released 2021-03-31 - -- Map span status code properly to GCT - ([#113](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/113)) -- Handle mixed case cloud propagator headers - ([#112](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/112)) - -## Version 0.17b0 - -Released 2021-02-04 - -## Version 0.16b1 - -Released 2021-01-14 - -## Version 0.15b0 - -Released 2020-11-04 - -## Version 0.14b0 - -Released 2020-10-27 - -- Fix breakages for opentelemetry-python v0.14b0 - ([#79](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/79)) - -## Version 0.13b0 - -Released 2020-09-17 - -## Version 0.12b0 - -Released 2020-08-17 - -## Version 0.11b0 - -Released 2020-08-05 - -- Add support for resources - ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) diff --git a/opentelemetry-resourcedetector-gcp/LICENSE b/opentelemetry-resourcedetector-gcp/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/opentelemetry-resourcedetector-gcp/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/opentelemetry-resourcedetector-gcp/MANIFEST.in b/opentelemetry-resourcedetector-gcp/MANIFEST.in deleted file mode 100644 index aed3e332..00000000 --- a/opentelemetry-resourcedetector-gcp/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -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/opentelemetry-resourcedetector-gcp/mypy.ini b/opentelemetry-resourcedetector-gcp/mypy.ini deleted file mode 100644 index 257b2b77..00000000 --- a/opentelemetry-resourcedetector-gcp/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -namespace_packages = True -explicit_package_bases = True -mypy_path = $MYPY_CONFIG_FILE_DIR/src diff --git a/opentelemetry-resourcedetector-gcp/setup.cfg b/opentelemetry-resourcedetector-gcp/setup.cfg deleted file mode 100644 index 22a35d19..00000000 --- a/opentelemetry-resourcedetector-gcp/setup.cfg +++ /dev/null @@ -1,43 +0,0 @@ -[metadata] -name = opentelemetry-resourcedetector-gcp -description = Google Cloud resource detector for OpenTelemetry -long_description = file: README.rst -long_description_content_type = text/x-rst -author = Google -author_email = opentelemetry-pypi@google.com -url = https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/tree/main/opentelemetry-resourcedetector-gcp -platforms = any -license = Apache-2.0 -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.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - -[options] -python_requires = >=3.9 -package_dir= - =src -packages=find_namespace: -install_requires = - opentelemetry-api ~= 1.30 - opentelemetry-sdk ~= 1.30 - requests ~= 2.24 - # TODO: remove when Python 3.7 is dropped - typing_extensions ~= 4.0 - -[options.packages.find] -where = src - -[options.extras_require] -test = - -[options.entry_points] -opentelemetry_resource_detector = - gcp_resource_detector = opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector \ No newline at end of file diff --git a/opentelemetry-resourcedetector-gcp/setup.py b/opentelemetry-resourcedetector-gcp/setup.py deleted file mode 100644 index 9d78a766..00000000 --- a/opentelemetry-resourcedetector-gcp/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2021 The OpenTelemetry Authors -# -# 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. -import os - -import setuptools - -BASE_DIR = os.path.dirname(__file__) -VERSION_FILENAME = os.path.join( - BASE_DIR, - "src", - "opentelemetry", - "resourcedetector", - "gcp_resource_detector", - "version.py", -) -PACKAGE_INFO = {} -with open(VERSION_FILENAME) as f: - exec(f.read(), PACKAGE_INFO) - -setuptools.setup( - version=PACKAGE_INFO["__version__"], - package_data={"opentelemetry": ["py.typed"]}, -) diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed b/opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py deleted file mode 100644 index 0b9b9cbf..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py +++ /dev/null @@ -1,143 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector import ( - _faas, - _gae, - _gce, - _gke, - _metadata, -) -from opentelemetry.resourcedetector.gcp_resource_detector._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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py deleted file mode 100644 index 2828b6d6..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py deleted file mode 100644 index 93af57fb..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Implementation in this file copied from -# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/faas.go - -import os - -from opentelemetry.resourcedetector.gcp_resource_detector import _metadata - -_CLOUD_RUN_CONFIG_ENV = "K_CONFIGURATION" -_CLOUD_FUNCTION_TARGET_ENV = "FUNCTION_TARGET" -_FAAS_SERVICE_ENV = "K_SERVICE" -_FAAS_REVISION_ENV = "K_REVISION" - - -def on_cloud_run() -> bool: - return _CLOUD_RUN_CONFIG_ENV in os.environ - - -def on_cloud_functions() -> bool: - return _CLOUD_FUNCTION_TARGET_ENV in os.environ - - -def faas_name() -> str: - """The name of the Cloud Run or Cloud Function. - - Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may - throw exceptions. - """ - return os.environ[_FAAS_SERVICE_ENV] - - -def faas_version() -> str: - """The version/revision of the Cloud Run or Cloud Function. - - Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may - throw exceptions. - """ - return os.environ[_FAAS_REVISION_ENV] - - -def faas_instance() -> str: - return str(_metadata.get_metadata()["instance"]["id"]) - - -def faas_cloud_region() -> str: - region = _metadata.get_metadata()["instance"]["region"] - return region[region.rfind("/") + 1 :] diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py deleted file mode 100644 index b67dc994..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Implementation in this file copied from -# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/app_engine.go - -import os - -from opentelemetry.resourcedetector.gcp_resource_detector import ( - _faas, - _gce, - _metadata, -) - -_GAE_SERVICE_ENV = "GAE_SERVICE" -_GAE_VERSION_ENV = "GAE_VERSION" -_GAE_INSTANCE_ENV = "GAE_INSTANCE" -_GAE_ENV = "GAE_ENV" -_GAE_STANDARD = "standard" - - -def on_app_engine_standard() -> bool: - return os.environ.get(_GAE_ENV) == _GAE_STANDARD - - -def on_app_engine() -> bool: - return _GAE_SERVICE_ENV in os.environ - - -def service_name() -> str: - """The service name of the app engine service. - - Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. - """ - return os.environ[_GAE_SERVICE_ENV] - - -def service_version() -> str: - """The service version of the app engine service. - - Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. - """ - return os.environ[_GAE_VERSION_ENV] - - -def service_instance() -> str: - """The service instance of the app engine service. - - Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. - """ - return os.environ[_GAE_INSTANCE_ENV] - - -def flex_availability_zone_and_region() -> _gce.ZoneAndRegion: - """The zone and region in which this program is running. - - Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. - """ - return _gce.availability_zone_and_region() - - -def standard_availability_zone() -> str: - """The zone the app engine service is running in. - - Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. - """ - zone = _metadata.get_metadata()["instance"]["zone"] - # zone is of the form "projects/233510669999/zones/us15" - return zone[zone.rfind("/") + 1 :] - - -def standard_cloud_region() -> str: - """The region the app engine service is running in. - - Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. - """ - return _faas.faas_cloud_region() diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py deleted file mode 100644 index 597fdb22..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py +++ /dev/null @@ -1,72 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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 Exception( - "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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py deleted file mode 100644 index 489e1947..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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 Exception( - f"unrecognized format for cluster location: {cluster_location}" - ) diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py deleted file mode 100644 index 1e054b92..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py +++ /dev/null @@ -1,222 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector import _constants -from opentelemetry.resourcedetector.gcp_resource_detector._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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py deleted file mode 100644 index 51db5613..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py deleted file mode 100644 index fad7719a..00000000 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 The OpenTelemetry Authors -# -# 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. - -__version__ = "1.13.0.dev0" diff --git a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr deleted file mode 100644 index fcc343a0..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr +++ /dev/null @@ -1,86 +0,0 @@ -# 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/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr deleted file mode 100644 index 381a726b..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr +++ /dev/null @@ -1,228 +0,0 @@ -# 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 fallback global] - dict({ - 'labels': dict({ - 'job': 'servicename', - 'location': 'global', - 'namespace': 'servicens', - 'task_id': 'serviceinstanceid', - }), - '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 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 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/opentelemetry-resourcedetector-gcp/tests/conftest.py b/opentelemetry-resourcedetector-gcp/tests/conftest.py deleted file mode 100644 index 7ad7a833..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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/opentelemetry-resourcedetector-gcp/tests/test_faas.py b/opentelemetry-resourcedetector-gcp/tests/test_faas.py deleted file mode 100644 index d1a47a70..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_faas.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import MagicMock - -import pytest -from opentelemetry.resourcedetector.gcp_resource_detector import _faas - - -# Reset stuff before every test -# pylint: disable=unused-argument -@pytest.fixture(autouse=True) -def autouse(fake_get_metadata): - pass - - -def test_detects_on_cloud_run(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") - assert _faas.on_cloud_run() - - -def test_detects_not_on_cloud_run() -> None: - assert not _faas.on_cloud_run() - - -def test_detects_on_cloud_functions(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") - assert _faas.on_cloud_functions() - - -def test_detects_not_on_cloud_functions() -> None: - assert not _faas.on_cloud_functions() - - -def test_detects_faas_name(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("K_SERVICE", "fake-service") - assert _faas.faas_name() == "fake-service" - - -def test_detects_faas_version(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("K_REVISION", "fake-revision") - assert _faas.faas_version() == "fake-revision" - - -def test_detects_faas_instance(fake_get_metadata: MagicMock) -> None: - fake_get_metadata.return_value = {"instance": {"id": "0087244a"}} - assert _faas.faas_instance() == "0087244a" - - -def test_detects_faas_region(fake_get_metadata: MagicMock) -> None: - fake_get_metadata.return_value = { - "instance": {"region": "projects/233510669999/regions/us-east4"} - } - assert _faas.faas_cloud_region() == "us-east4" diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gae.py b/opentelemetry-resourcedetector-gcp/tests/test_gae.py deleted file mode 100644 index e418881f..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_gae.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import MagicMock - -import pytest -from opentelemetry.resourcedetector.gcp_resource_detector import _gae - - -# Reset stuff before every test -# pylint: disable=unused-argument -@pytest.fixture(autouse=True) -def autouse(fake_get_metadata): - pass - - -def test_detects_on_gae(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_SERVICE", "fake-service") - assert _gae.on_app_engine() - - -def test_detects_not_on_gae() -> None: - assert not _gae.on_app_engine() - - -def test_detects_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_ENV", "standard") - assert _gae.on_app_engine_standard() - - -def test_detects_not_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_SERVICE", "fake-service") - assert _gae.on_app_engine() - assert not _gae.on_app_engine_standard() - - -def test_detects_gae_service_name(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_SERVICE", "fake-service") - assert _gae.service_name() == "fake-service" - - -def test_detects_gae_service_version(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_VERSION", "fake-version") - assert _gae.service_version() == "fake-version" - - -def test_detects_gae_service_instance(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GAE_INSTANCE", "fake-instance") - assert _gae.service_instance() == "fake-instance" - - -def test_detects_gae_flex_zone_and_region( - fake_get_metadata: MagicMock, -) -> None: - fake_get_metadata.return_value = { - "instance": {"zone": "projects/233510669999/zones/us-east4-b"} - } - zone_and_region = _gae.flex_availability_zone_and_region() - assert zone_and_region.zone == "us-east4-b" - assert zone_and_region.region == "us-east4" - - -def test_gae_standard_zone( - fake_get_metadata: MagicMock, -) -> None: - fake_get_metadata.return_value = { - "instance": {"zone": "projects/233510669999/zones/us15"} - } - assert _gae.standard_availability_zone() == "us15" - - -def test_gae_standard_region( - fake_get_metadata: MagicMock, -) -> None: - fake_get_metadata.return_value = { - "instance": {"region": "projects/233510669999/regions/us-east4"} - } - assert _gae.standard_cloud_region() == "us-east4" diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gce.py b/opentelemetry-resourcedetector-gcp/tests/test_gce.py deleted file mode 100644 index 693ae0b1..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_gce.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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(Exception, match="zone was not in the expected format"): - _gce.availability_zone_and_region() diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py b/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py deleted file mode 100644 index d1ef14cb..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py +++ /dev/null @@ -1,199 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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/opentelemetry-resourcedetector-gcp/tests/test_gke.py b/opentelemetry-resourcedetector-gcp/tests/test_gke.py deleted file mode 100644 index 057fe85d..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_gke.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector 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( - Exception, match="unrecognized format for cluster location" - ): - _gke.availability_zone_or_region() diff --git a/opentelemetry-resourcedetector-gcp/tests/test_mapping.py b/opentelemetry-resourcedetector-gcp/tests/test_mapping.py deleted file mode 100644 index eda679d9..00000000 --- a/opentelemetry-resourcedetector-gcp/tests/test_mapping.py +++ /dev/null @@ -1,260 +0,0 @@ -# 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.resourcedetector.gcp_resource_detector._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 From 81d6f1b383304e32cf66ab392717c17c3dace33c Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Wed, 22 Jul 2026 20:28:05 +0000 Subject: [PATCH 03/16] Delete resource detector --- cloudbuild-e2e-cloud-functions-gen2.yaml | 1 - cloudbuild-e2e-gae-standard.yaml | 1 - docs-requirements.txt | 3 +- docs/apireference.rst | 2 - .../examples/cloud_monitoring/requirements.in | 2 +- .../cloud_resource_detector/requirements.in | 2 +- .../cloud_trace_exporter/requirements.in | 2 +- .../cloud_trace_propagator/requirements.in | 2 +- docs/examples/flask_e2e/requirements.in | 2 +- e2e-test-server/Dockerfile | 1 - e2e-test-server/requirements-dockerfile.txt | 2 +- opentelemetry-resourcedetector-gcp/README.md | 5 ++ opentelemetry-resourcedetector-gcp/README.rst | 53 +------------------ release.py | 5 +- tox.ini | 24 +++------ 15 files changed, 24 insertions(+), 83 deletions(-) create mode 100644 opentelemetry-resourcedetector-gcp/README.md diff --git a/cloudbuild-e2e-cloud-functions-gen2.yaml b/cloudbuild-e2e-cloud-functions-gen2.yaml index d58d40dc..4de0a3a5 100644 --- a/cloudbuild-e2e-cloud-functions-gen2.yaml +++ b/cloudbuild-e2e-cloud-functions-gen2.yaml @@ -27,7 +27,6 @@ steps: --no-deps \ --wheel-dir wheels \ ../opentelemetry-exporter-gcp-trace/ \ - ../opentelemetry-resourcedetector-gcp/ \ ../opentelemetry-propagator-gcp zip -qr function-source.zip . diff --git a/cloudbuild-e2e-gae-standard.yaml b/cloudbuild-e2e-gae-standard.yaml index 911d5e27..9f4f25a4 100644 --- a/cloudbuild-e2e-gae-standard.yaml +++ b/cloudbuild-e2e-gae-standard.yaml @@ -27,7 +27,6 @@ steps: --no-deps \ --wheel-dir wheels \ ../opentelemetry-exporter-gcp-trace/ \ - ../opentelemetry-resourcedetector-gcp/ \ ../opentelemetry-propagator-gcp zip -qr appsource.zip . diff --git a/docs-requirements.txt b/docs-requirements.txt index a78f84f6..4a368da9 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,5 +3,6 @@ Sphinx sphinx-autodoc-typehints -e ./opentelemetry-exporter-gcp-trace -e ./opentelemetry-exporter-gcp-monitoring +-e ./opentelemetry-exporter-gcp-logging -e ./opentelemetry-propagator-gcp --e ./opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp diff --git a/docs/apireference.rst b/docs/apireference.rst index 5c11cd4e..eff8b8d6 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -13,7 +13,6 @@ API Reference opentelemetry.exporter opentelemetry.propagators.cloud_trace_propagator - opentelemetry.resourcedetector .. toctree:: @@ -23,4 +22,3 @@ API Reference _autosummary/opentelemetry.exporter _autosummary/opentelemetry.propagators.cloud_trace_propagator - _autosummary/opentelemetry.resourcedetector diff --git a/docs/examples/cloud_monitoring/requirements.in b/docs/examples/cloud_monitoring/requirements.in index 04ccf3c9..8ee899f0 100644 --- a/docs/examples/cloud_monitoring/requirements.in +++ b/docs/examples/cloud_monitoring/requirements.in @@ -1,4 +1,4 @@ -e ../../../opentelemetry-exporter-gcp-monitoring --e ../../../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_resource_detector/requirements.in b/docs/examples/cloud_resource_detector/requirements.in index 78a09650..d4b43e1d 100644 --- a/docs/examples/cloud_resource_detector/requirements.in +++ b/docs/examples/cloud_resource_detector/requirements.in @@ -1,5 +1,5 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace --e ../../../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_trace_exporter/requirements.in b/docs/examples/cloud_trace_exporter/requirements.in index 2d062d4d..8fb9dd77 100644 --- a/docs/examples/cloud_trace_exporter/requirements.in +++ b/docs/examples/cloud_trace_exporter/requirements.in @@ -1,4 +1,4 @@ -e ../../../opentelemetry-exporter-gcp-trace --e ../../../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_trace_propagator/requirements.in b/docs/examples/cloud_trace_propagator/requirements.in index 68238d86..a1e8d21d 100644 --- a/docs/examples/cloud_trace_propagator/requirements.in +++ b/docs/examples/cloud_trace_propagator/requirements.in @@ -1,7 +1,7 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace -e ../../../opentelemetry-propagator-gcp --e ../../../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp flask opentelemetry-api opentelemetry-instrumentation-flask diff --git a/docs/examples/flask_e2e/requirements.in b/docs/examples/flask_e2e/requirements.in index 68238d86..a1e8d21d 100644 --- a/docs/examples/flask_e2e/requirements.in +++ b/docs/examples/flask_e2e/requirements.in @@ -1,7 +1,7 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace -e ../../../opentelemetry-propagator-gcp --e ../../../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp flask opentelemetry-api opentelemetry-instrumentation-flask diff --git a/e2e-test-server/Dockerfile b/e2e-test-server/Dockerfile index 800db1c0..fca43b45 100644 --- a/e2e-test-server/Dockerfile +++ b/e2e-test-server/Dockerfile @@ -28,7 +28,6 @@ FROM python-base as build-base # copy local dependencies COPY opentelemetry-exporter-gcp-trace opentelemetry-exporter-gcp-trace COPY opentelemetry-propagator-gcp opentelemetry-propagator-gcp -COPY opentelemetry-resourcedetector-gcp opentelemetry-resourcedetector-gcp WORKDIR $SRC/e2e-test-server # copy requirements/constraints COPY e2e-test-server/*.txt ./ diff --git a/e2e-test-server/requirements-dockerfile.txt b/e2e-test-server/requirements-dockerfile.txt index 11f15f9b..593c54dc 100644 --- a/e2e-test-server/requirements-dockerfile.txt +++ b/e2e-test-server/requirements-dockerfile.txt @@ -2,4 +2,4 @@ -r requirements-shared.txt ../opentelemetry-exporter-gcp-trace ../opentelemetry-propagator-gcp -../opentelemetry-resourcedetector-gcp +opentelemetry-resourcedetector-gcp diff --git a/opentelemetry-resourcedetector-gcp/README.md b/opentelemetry-resourcedetector-gcp/README.md new file mode 100644 index 00000000..de4b8773 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/README.md @@ -0,0 +1,5 @@ +# OpenTelemetry Google Cloud Resource Detector + +The OpenTelemetry Google Cloud Resource Detector has moved to the [opentelemetry-python-contrib](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp) repository: + +https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp diff --git a/opentelemetry-resourcedetector-gcp/README.rst b/opentelemetry-resourcedetector-gcp/README.rst index 3c7f958b..d93f3b24 100644 --- a/opentelemetry-resourcedetector-gcp/README.rst +++ b/opentelemetry-resourcedetector-gcp/README.rst @@ -1,55 +1,6 @@ OpenTelemetry Google Cloud Resource Detector ============================================ -.. image:: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp.svg - :target: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp +The OpenTelemetry Google Cloud Resource Detector has moved to the `opentelemetry-python-contrib `_ repository: -.. image:: https://readthedocs.org/projects/google-cloud-opentelemetry/badge/?version=latest - :target: https://google-cloud-opentelemetry.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -This library provides support for detecting GCP resources like GCE, GKE, etc. - -To get started with instrumentation in Google Cloud, see `Generate traces and metrics with -Python `_. - -To learn more about instrumentation and observability, including opinionated recommendations -for Google Cloud Observability, visit `Instrumentation and observability -`_. - -Installation ------------- - -.. code:: bash - - pip install opentelemetry-resourcedetector-gcp - -Usage ------ - -.. code:: 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 `_ +https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp diff --git a/release.py b/release.py index db87abc9..877b4091 100755 --- a/release.py +++ b/release.py @@ -52,10 +52,9 @@ # Map of different suffixes to use instead of the given ones in release_version ALTERNATE_SUFFIXES = { - # Mark monitoring and resource detector alpha + # Mark monitoring and logging alpha "opentelemetry-exporter-gcp-monitoring": "a0", "opentelemetry-exporter-gcp-logging": "a0", - "opentelemetry-resourcedetector-gcp": "a0", } @@ -138,6 +137,8 @@ def create_release_commit(release_version: str,) -> None: } for package_root in repo_root().glob("opentelemetry-*/"): + if not (package_root / "CHANGELOG.md").exists(): + continue if package_root in alternate_suffix_paths: suffix = alternate_suffix_paths[package_root] release_version_use = release_version_parsed.base_version + suffix diff --git a/tox.ini b/tox.ini index e7e7a4eb..a8e96cc8 100644 --- a/tox.ini +++ b/tox.ini @@ -5,13 +5,13 @@ requires = tox>=4 envlist = ; Add the `ci` factor to any env that should be running during CI. - py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} - {lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} + py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,cloudlogging} + {lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,cloudlogging} docs-ci ; These are development commands and share the same virtualenv within each ; package root directory - {fix}-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} + {fix}-{cloudtrace,cloudmonitoring,propagator,cloudlogging} ; Installs dev depenedencies and all packages in this repo with editable ; install into a single env. Useful for editor autocompletion @@ -23,12 +23,6 @@ base_deps = -c {toxinidir}/dev-constraints.txt -e {toxinidir}/test-common -; the inter-monorepo dependencies -monorepo_deps = - cloudmonitoring: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ - cloudtrace: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ - cloudlogging: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ - dev_basepython = python3.10 dev_deps = {[constants]base_deps} @@ -58,14 +52,12 @@ setenv = cloudmonitoring: PACKAGE_NAME = opentelemetry-exporter-gcp-monitoring cloudlogging: PACKAGE_NAME = opentelemetry-exporter-gcp-logging propagator: PACKAGE_NAME = opentelemetry-propagator-gcp - resourcedetector: PACKAGE_NAME = opentelemetry-resourcedetector-gcp -[testenv:py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] +[testenv:py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] deps = ; editable install the package itself -e {toxinidir}/{env:PACKAGE_NAME} test: {[constants]base_deps} - test: {[constants]monorepo_deps} ; test specific deps test: pytest test: syrupy @@ -81,13 +73,12 @@ allowlist_externals = bash {toxinidir}/get_mock_server.sh -[testenv:{lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] +[testenv:{lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] basepython = {[constants]dev_basepython} deps = ; editable install the package itself -e {toxinidir}/{env:PACKAGE_NAME} {[constants]dev_deps} - {[constants]monorepo_deps} changedir = {env:PACKAGE_NAME} commands = lint: black . --diff --check @@ -109,17 +100,15 @@ allowlist_externals = make bash -[testenv:{fix}-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] +[testenv:{fix}-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] basepython = {[constants]dev_basepython} envdir = cloudtrace: opentelemetry-exporter-gcp-trace/venv cloudmonitoring: opentelemetry-exporter-gcp-monitoring/venv propagator: opentelemetry-propagator-gcp/venv - resourcedetector: opentelemetry-resourcedetector-gcp/venv cloudlogging: opentelemetry-exporter-gcp-logging/venv deps = {[constants]dev_deps} - {[constants]monorepo_deps} -e {env:PACKAGE_NAME} changedir = {env:PACKAGE_NAME} @@ -136,5 +125,4 @@ deps = -e opentelemetry-exporter-gcp-monitoring -e opentelemetry-exporter-gcp-trace -e opentelemetry-propagator-gcp - -e opentelemetry-resourcedetector-gcp -e opentelemetry-exporter-gcp-logging From a2cffb8aa0a0012112e85098bbba8b9dc5043f3d Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 19:55:37 +0000 Subject: [PATCH 04/16] Make proposed changes --- MIGRATION.md | 95 ++++++++++++++++++++++++++++++++++++++++------------ README.md | 8 +++++ 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 3d728e32..1abd95ee 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -4,12 +4,25 @@ This guide provides instructions on how to migrate from the custom exporters in ## Overview -Google Cloud supports native OTLP (OpenTelemetry Protocol) ingestion for Cloud Trace and Cloud Monitoring via the [Telemetry API](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/overview). This allows you to use standard OpenTelemetry OTLP exporters for sending telemetry data to Google Cloud. +Google Cloud supports native OTLP (OpenTelemetry Protocol) ingestion for Cloud Trace, Cloud Monitoring, and Cloud Logging via the [Telemetry API](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/overview). This allows you to use standard OpenTelemetry OTLP exporters for sending telemetry data to Google Cloud. ## Deprecation Notice -All exporters in this repository are deprecated. Please migrate to the standard OTLP exporters using standard OpenTelemetry libraries. This repository will be -archived soon. +All exporters in this repository (`opentelemetry-exporter-gcp-trace`, `opentelemetry-exporter-gcp-monitoring`, and `opentelemetry-exporter-gcp-logging`) are deprecated. Please migrate to standard OTLP exporters using standard OpenTelemetry libraries. + +--- + +## Resource Detection (Recommended for All Signals) + +When migrating to OTLP exporters, installing the GCP Resource Detector package (`opentelemetry-resourcedetector-gcp`) automatically populates Google Cloud resource attributes (such as `gcp.project_id`, `cloud.account.id`, `host.id`, `k8s.pod.name`, etc.) for OpenTelemetry SDK providers (`TracerProvider`, `MeterProvider`, `LoggerProvider`). + +### Installation + +```bash +pip install opentelemetry-resourcedetector-gcp +``` + +Once installed, the GCP resource detector entrypoint is automatically discovered and loaded by the OpenTelemetry SDK without requiring explicit manual resource setup code. --- @@ -19,10 +32,10 @@ To migrate from `opentelemetry-exporter-gcp-trace` (`CloudTraceSpanExporter`) to ### 1. Add Dependencies -Install the standard OpenTelemetry OTLP exporter and GCP authentication dependencies: +Install the standard OpenTelemetry OTLP exporter, GCP resource detector, and GCP authentication dependencies: ```bash -pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio requests +pip install opentelemetry-exporter-otlp-proto-grpc opentelemetry-resourcedetector-gcp google-auth grpcio requests ``` ### 2. Configure the SDK @@ -83,9 +96,14 @@ For more details, follow the official Google Cloud guide: [Migrate from the Trac | `CloudTraceSpanExporter` Parameter | OTLP Equivalent Property / Env Var | Notes | | :--- | :--- | :--- | -| `project_id` | Use resource attribute: `gcp.project_id` | Set via `OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id"`. | -| `client` | N/A | Pre-configured `TraceServiceClient` is replaced by HTTP/gRPC OTLP exporter configuration. | -| `resource_regex` | Resource Attributes / Processors | Use standard OpenTelemetry Resource attributes instead. | +| `project_id` / `OTEL_EXPORTER_GCP_TRACE_PROJECT_ID` | Resource attribute: `gcp.project_id` | Set via `OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id"` or detected automatically via `opentelemetry-resourcedetector-gcp`. | +| `client` | N/A | Pre-configured `TraceServiceClient` is replaced by OTLP gRPC/HTTP channel credentials. | +| `resource_regex` | Standard Resource Attributes | Standard OpenTelemetry exports resource attributes attached to the `TracerProvider`. Filtering/copying resource attributes via regex is not supported in OTLP. | + +#### Unsupported Features + +* **Attribute Mapping & Regex Filtering (`resource_regex`)**: `CloudTraceSpanExporter` allowed filtering resource attributes via regex to copy matching keys into span attributes. The standard OTLP exporter exports standard OpenTelemetry resource attributes attached to the `TracerProvider` directly. +* **Custom Trace Service Client (`client`)**: You cannot pass a pre-configured `TraceServiceClient` instance directly to `OTLPSpanExporter`. If custom gRPC channels or metadata credentials are required, configure gRPC channel credentials programmatically as shown above. --- @@ -101,17 +119,31 @@ For more details, follow the official Google Cloud guide: [Migrate from the Trac ### Why Migrate? -Transitioning to the standard OTLP exporter is recommended for the following reasons: -* **Standardization:** Aligns your application with the industry-standard OpenTelemetry Protocol (OTLP). -* **Google Managed Prometheus (GMP):** Standard OTLP metrics are ingested into Google Managed Prometheus, offering robust, scalable, and cost-effective monitoring. -* **Future-proofing:** The legacy Google Cloud exporters in this repository are deprecated. +While this migration introduces breaking changes, transitioning to the standard OTLP exporter is recommended for the following reasons: +* **Standardization:** Aligns your application with the industry-standard OpenTelemetry Protocol (OTLP), ensuring vendor neutrality and compatibility with the broader OpenTelemetry ecosystem. +* **Google Managed Prometheus (GMP) Cost Savings:** Standard OTLP metrics are ingested into Google Managed Prometheus. GMP offers a robust, scalable, and cost-effective monitoring solution (~20x cheaper ingestion cost than legacy Cloud Monitoring API ingestion). +* **Future-proofing:** The legacy Google Cloud Monitoring exporter is deprecated. Migrating now ensures your monitoring pipeline remains supported. + +--- + +### Migration Strategies + +We recommend three paths for migration, depending on your operational requirements: + +1. **Direct Migration (Recommended):** Migrate fully to the OTLP exporter and update your dashboards and alerts to use the new metric names under the `prometheus.googleapis.com/` domain. +2. **Transition via Double-Writing (Alternative):** Run both the legacy exporter and the OTLP exporter in parallel. This allows you to validate the new OTLP pipeline and update dashboards/alerts without any monitoring downtime, at the cost of temporary double-ingestion charges. +3. **Custom Metric Renaming / View Configuration (Alternative):** Use OpenTelemetry SDK Views or metric processors (or OpenTelemetry Collector relabeling) to map metric names and attributes, allowing you to maintain compatibility with existing dashboards. -### Strategy & Steps +--- + +### Strategy 1: Direct Migration (Recommended) + +Follow these steps to fully transition to the standard OTLP exporter. #### 1. Add Dependencies ```bash -pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio +pip install opentelemetry-exporter-otlp-proto-grpc opentelemetry-resourcedetector-gcp google-auth grpcio ``` #### 2. Configure Environment Variables @@ -158,26 +190,36 @@ provider = MeterProvider(metric_readers=[reader]) metrics.set_meter_provider(provider) ``` ---- +### Mapping and Limitations -## Migrate from OpenTelemetry Google Cloud Logging Exporter (`CloudLoggingExporter`) to OTLP Exporter +#### Configuration Mapping + +| `CloudMonitoringMetricsExporter` Parameter | OTLP Equivalent Property / Env Var | Notes | +| :--- | :--- | :--- | +| `prefix` (default `workload.googleapis.com`) | N/A | Legacy exporter ingested under `workload.googleapis.com/` (or custom prefix). OTLP exporter ingests into Google Managed Prometheus under `prometheus.googleapis.com/` by default. | +| `add_unique_identifier` | Resource Attributes (e.g. `service.instance.id`, `host.id`) | Legacy exporter appended a random identifier to time series. OTLP relies on standard OpenTelemetry resource attributes to distinguish instances. | +| `project_id` | Resource attribute: `gcp.project_id` | Set via `OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id"` or detected automatically via `opentelemetry-resourcedetector-gcp`. | +| `client` | N/A | Pre-configured `MetricServiceClient` cannot be passed directly to `OTLPMetricExporter`. | + +#### Limitations & Breaking Changes -The OTLP `LogRecord` to Cloud Logging `LogEntry` conversion logic in the `CloudLoggingExporter` is similar but not identical to the Google OTLP endpoint conversion logic. +* **Metric Domain & Prefix:** Metric names in Cloud Monitoring will change from `workload.googleapis.com/` to `prometheus.googleapis.com//`. Existing Cloud Monitoring dashboards and alerts relying on `workload.googleapis.com/` metrics must be updated to query `prometheus.googleapis.com/` metrics. +* **Unique Identifier Handling:** The `add_unique_identifier` parameter in `CloudMonitoringMetricsExporter` is not supported. Use standard resource attributes like `service.instance.id` or `host.id` to separate metric streams from distinct exporter instances. -The Google OTLP endpoint `LogRecord` to Cloud Logging `LogEntry` conversion logic is described here: https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry. +--- -The conversion logic in the `CloudLoggingExporter` can be found here: https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py. +## Migrate from OpenTelemetry Google Cloud Logging Exporter (`CloudLoggingExporter`) to OTLP Exporter -You can send the same OTLP `LogRecord` to the Google OTLP endpoint and to Cloud Logging via the `CloudLoggingExporter` to see exactly how the conversion logic diverges for a particular log if at all. +The OTLP `LogRecord` to Cloud Logging `LogEntry` conversion logic in standard OTLP endpoints is described in the [Google OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). To migrate from `opentelemetry-exporter-gcp-logging` (`CloudLoggingExporter`) to the standard OpenTelemetry OTLP log exporter, follow these steps: ### 1. Add Dependencies -Install the standard OpenTelemetry OTLP exporter package: +Install the standard OpenTelemetry OTLP log exporter package and GCP resource detector: ```bash -pip install opentelemetry-exporter-otlp-proto-grpc google-auth grpcio +pip install opentelemetry-exporter-otlp-proto-grpc opentelemetry-resourcedetector-gcp google-auth grpcio ``` ### 2. Configure Environment Variables @@ -227,3 +269,12 @@ logger_provider.add_log_record_processor( BatchLogRecordProcessor(otlp_log_exporter) ) ``` + +### Mapping and Limitations + +#### Conversion Logic & Log Entry Mapping + +* **Log Entry Conversion:** `CloudLoggingExporter` translated `LogRecord` objects locally using the Python `google-cloud-logging` client library. The standard OTLP endpoint converts OTLP `LogRecord` payloads server-side according to the [OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). +* **Log Names & Resources:** The OTLP endpoint maps log names from resource attributes (e.g. `gcp.log_name` or defaults to `projects//logs/otel`). +* **Query Impact:** If your existing Cloud Logging log queries filter by specific `logName` values (such as python logger names mapped by `CloudLoggingExporter`), you may need to update your Cloud Logging query filters to match the OTLP log names and attributes. +* **GCP Monitored Resource Association:** Installing `opentelemetry-resourcedetector-gcp` ensures log records contain appropriate GCP resource attributes, allowing Cloud Logging to associate logs with standard monitored resources (GCE instances, GKE pods, Cloud Run services, etc.). diff --git a/README.md b/README.md index fe686c5c..cbb7ff9f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ for Google Cloud Platform. To get started with instrumentation in Google Cloud, see [Generate traces and metrics with Python](https://cloud.google.com/stackdriver/docs/instrumentation/setup/python). +## ⚠️ Deprecation Notice + +**All custom Google Cloud exporters in this repository (`opentelemetry-exporter-gcp-trace`, `opentelemetry-exporter-gcp-monitoring`, and `opentelemetry-exporter-gcp-logging`) are deprecated.** + +Google Cloud supports native OpenTelemetry Protocol (OTLP) ingestion for Cloud Trace, Cloud Monitoring, and Cloud Logging via the [Telemetry API](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/overview). + +Please refer to the [Migration Guide](MIGRATION.md) for detailed instructions on migrating your application to standard OpenTelemetry OTLP exporters. + ## Documentation To learn more about instrumentation and observability, including opinionated recommendations From 8d80981f93cdff5b46ad2106b44616618af810ed Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 19:58:14 +0000 Subject: [PATCH 05/16] Update section on resource detector --- MIGRATION.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 1abd95ee..a781aef2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -14,7 +14,7 @@ All exporters in this repository (`opentelemetry-exporter-gcp-trace`, `opentelem ## Resource Detection (Recommended for All Signals) -When migrating to OTLP exporters, installing the GCP Resource Detector package (`opentelemetry-resourcedetector-gcp`) automatically populates Google Cloud resource attributes (such as `gcp.project_id`, `cloud.account.id`, `host.id`, `k8s.pod.name`, etc.) for OpenTelemetry SDK providers (`TracerProvider`, `MeterProvider`, `LoggerProvider`). +When migrating to OTLP exporters, using the GCP Resource Detector package (`opentelemetry-resourcedetector-gcp`) automatically populates Google Cloud resource attributes (such as `gcp.project_id`, `cloud.account.id`, `host.id`, `k8s.pod.name`, etc.) for OpenTelemetry SDK providers (`TracerProvider`, `MeterProvider`, `LoggerProvider`). ### Installation @@ -22,7 +22,21 @@ When migrating to OTLP exporters, installing the GCP Resource Detector package ( pip install opentelemetry-resourcedetector-gcp ``` -Once installed, the GCP resource detector entrypoint is automatically discovered and loaded by the OpenTelemetry SDK without requiring explicit manual resource setup code. +### Configuration via Environment Variable + +When using OpenTelemetry autoconfiguration (`opentelemetry-sdk-extension-autoconfigure` or zero-code `opentelemetry-instrument`), enable the GCP resource detector via the `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` environment variable: + +```bash +export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS="gcp" +``` + +You can also specify additional resource attributes via `OTEL_RESOURCE_ATTRIBUTES`: + +```bash +export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id,service.name=my-service" +``` + +Once installed and configured, the GCP resource detector entrypoint is automatically discovered and loaded by the OpenTelemetry SDK without requiring explicit manual resource setup code. --- @@ -47,6 +61,7 @@ You can configure the SDK using environment variables: export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" export OTEL_TRACES_EXPORTER="otlp" export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS="gcp" ``` Or programmatically in Python using GCP authentication: @@ -152,6 +167,7 @@ pip install opentelemetry-exporter-otlp-proto-grpc opentelemetry-resourcedetecto export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" export OTEL_METRICS_EXPORTER="otlp" export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS="gcp" export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=$PROJECT_ID,location=us-central1,service.name=otlp-sample,service.instance.id=1" ``` @@ -228,6 +244,7 @@ pip install opentelemetry-exporter-otlp-proto-grpc opentelemetry-resourcedetecto export OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.googleapis.com" export OTEL_LOGS_EXPORTER="otlp" export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS="gcp" export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=$PROJECT_ID,service.name=otlp-sample,service.instance.id=1" ``` From f3817343fbce83d216a11686060c2294e53619ac Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 20:00:17 +0000 Subject: [PATCH 06/16] Clarify resource detector further.. --- MIGRATION.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index a781aef2..a5a6f9c1 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -14,7 +14,7 @@ All exporters in this repository (`opentelemetry-exporter-gcp-trace`, `opentelem ## Resource Detection (Recommended for All Signals) -When migrating to OTLP exporters, using the GCP Resource Detector package (`opentelemetry-resourcedetector-gcp`) automatically populates Google Cloud resource attributes (such as `gcp.project_id`, `cloud.account.id`, `host.id`, `k8s.pod.name`, etc.) for OpenTelemetry SDK providers (`TracerProvider`, `MeterProvider`, `LoggerProvider`). +When migrating to OTLP exporters, installing the GCP Resource Detector package (`opentelemetry-resourcedetector-gcp`) automatically populates Google Cloud resource attributes (such as `gcp.project_id`, `cloud.account.id`, `host.id`, `k8s.pod.name`, etc.) for OpenTelemetry SDK providers (`TracerProvider`, `MeterProvider`, `LoggerProvider`). ### Installation @@ -22,9 +22,10 @@ When migrating to OTLP exporters, using the GCP Resource Detector package (`open pip install opentelemetry-resourcedetector-gcp ``` -### Configuration via Environment Variable +### Usage & Configuration -When using OpenTelemetry autoconfiguration (`opentelemetry-sdk-extension-autoconfigure` or zero-code `opentelemetry-instrument`), enable the GCP resource detector via the `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` environment variable: +* **Manual SDK Setup (In Code):** When manually setting up the SDK in Python (e.g., instantiating `TracerProvider()`, `MeterProvider()`, or `LoggerProvider()`), the GCP resource detector is **automatically discovered and applied** simply by installing `opentelemetry-resourcedetector-gcp`. No additional code changes or environment variables are required. +* **Autoconfiguration / Zero-Code Instrumentation:** When using OpenTelemetry autoconfiguration (`opentelemetry-sdk-extension-autoconfigure` or `opentelemetry-instrument`), enable the GCP resource detector via the `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` environment variable: ```bash export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS="gcp" @@ -36,8 +37,6 @@ You can also specify additional resource attributes via `OTEL_RESOURCE_ATTRIBUTE export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id,service.name=my-service" ``` -Once installed and configured, the GCP resource detector entrypoint is automatically discovered and loaded by the OpenTelemetry SDK without requiring explicit manual resource setup code. - --- ## Migrate from OpenTelemetry Google Cloud Trace Exporter (`CloudTraceSpanExporter`) to OTLP Exporter From fd0d5278ac47e958a2a140899a3b328086a59304 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 20:17:49 +0000 Subject: [PATCH 07/16] ADd link to conversion logic --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index a5a6f9c1..8f853fdd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -290,7 +290,7 @@ logger_provider.add_log_record_processor( #### Conversion Logic & Log Entry Mapping -* **Log Entry Conversion:** `CloudLoggingExporter` translated `LogRecord` objects locally using the Python `google-cloud-logging` client library. The standard OTLP endpoint converts OTLP `LogRecord` payloads server-side according to the [OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). +* **Log Entry Conversion:** The conversion logic in `CloudLoggingExporter` can be found in [`opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py`](opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py#L331-L380). Standard OTLP endpoints convert OTLP `LogRecord` payloads server-side according to the [OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). * **Log Names & Resources:** The OTLP endpoint maps log names from resource attributes (e.g. `gcp.log_name` or defaults to `projects//logs/otel`). * **Query Impact:** If your existing Cloud Logging log queries filter by specific `logName` values (such as python logger names mapped by `CloudLoggingExporter`), you may need to update your Cloud Logging query filters to match the OTLP log names and attributes. * **GCP Monitored Resource Association:** Installing `opentelemetry-resourcedetector-gcp` ensures log records contain appropriate GCP resource attributes, allowing Cloud Logging to associate logs with standard monitored resources (GCE instances, GKE pods, Cloud Run services, etc.). From cf3bc1c9646820d424da65bf00a752356f57179b Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 20:20:45 +0000 Subject: [PATCH 08/16] Clarify the format will change when switching over.. --- MIGRATION.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 8f853fdd..f087ef87 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -225,7 +225,8 @@ metrics.set_meter_provider(provider) ## Migrate from OpenTelemetry Google Cloud Logging Exporter (`CloudLoggingExporter`) to OTLP Exporter -The OTLP `LogRecord` to Cloud Logging `LogEntry` conversion logic in standard OTLP endpoints is described in the [Google OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). +The OTLP `LogRecord` to Cloud Logging `LogEntry` conversion logic in standard OTLP endpoints is described in the [Google OTLP LogRecord to LogEntry specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/otlp-log-record-to-log-entry). The conversion logic used in `CloudLoggingExporter` can be found in [`opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py`](opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py#L331-L380) and is different in some ways, you should not expect +the format of the log to look exactly the same after switching over. To migrate from `opentelemetry-exporter-gcp-logging` (`CloudLoggingExporter`) to the standard OpenTelemetry OTLP log exporter, follow these steps: From 091a238b97bfab112131069474270dfb76c9faf6 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Thu, 23 Jul 2026 20:38:34 +0000 Subject: [PATCH 09/16] Add a detailed breakdown of mapping differences --- MIGRATION.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index f087ef87..0a47f723 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -221,6 +221,29 @@ metrics.set_meter_provider(provider) * **Metric Domain & Prefix:** Metric names in Cloud Monitoring will change from `workload.googleapis.com/` to `prometheus.googleapis.com//`. Existing Cloud Monitoring dashboards and alerts relying on `workload.googleapis.com/` metrics must be updated to query `prometheus.googleapis.com/` metrics. * **Unique Identifier Handling:** The `add_unique_identifier` parameter in `CloudMonitoringMetricsExporter` is not supported. Use standard resource attributes like `service.instance.id` or `host.id` to separate metric streams from distinct exporter instances. +#### Conversion Logic & Metric Mapping Differences + +The conversion logic used in `CloudMonitoringMetricsExporter` can be found in [`opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py`](opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L184-L365). Standard OTLP endpoints convert OTLP metric data server-side according to the [Google Cloud Telemetry API Metric Mapping specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/v1.metrics#metric-mapping-reference-info). + +Key differences include: + +* **Metric Domain & Name Structure:** + - **`CloudMonitoringMetricsExporter`:** Ingests metrics under `workload.googleapis.com/` (or custom `prefix`). + - **Telemetry API:** Ingests metrics under `prometheus.googleapis.com//` (e.g. `/counter`, `/gauge`, `/histogram`, `/delta`, `/summary`). +* **Value Types (`INT64` vs `DOUBLE`):** + - **`CloudMonitoringMetricsExporter`:** Preserves `INT64` value types when integer data points are passed (`TypedValue(int64_value=...)`). + - **Telemetry API:** Translates **all OTLP `INT64` scalar metrics to `DOUBLE`** in Cloud Monitoring to prevent Monarch value-type schema collisions. +* **Metric Kind & Temporality:** + - Monotonic cumulative sums map to `CUMULATIVE` (suffixed with `/counter`). + - Monotonic delta sums map to `DELTA` (suffixed with `/delta`). + - Non-monotonic sums map to `GAUGE` (suffixed with `/gauge`). Non-monotonic delta sums are not supported. + - Histograms support both `CUMULATIVE` (`/histogram`) and `DELTA` (`/histogram:delta`) distributions. + - Summary metrics are expanded into individual time series for `_count` (`CUMULATIVE`), `_sum` (`CUMULATIVE`), and `quantile` (`GAUGE` with a `quantile` label). +* **Resource Attributes & `target_info` Metric:** + - **`CloudMonitoringMetricsExporter`:** Maps OpenTelemetry resources to GCP `MonitoredResource` on the client side using `get_monitored_resource()`. Optionally appends a random `opentelemetry_id` label when `add_unique_identifier=True`. + - **Telemetry API:** Maps resources server-side and automatically generates a `target_info` metric for each unique OpenTelemetry resource containing non-identifying resource attributes. +* **Special Characters:** The Telemetry API preserves `.` and `/` characters in OTLP metric names rather than replacing them with underscores (`_`). + --- ## Migrate from OpenTelemetry Google Cloud Logging Exporter (`CloudLoggingExporter`) to OTLP Exporter From d7417bfd52a74a2b070a2ce44ac2978451f89c6d Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 14:16:27 +0000 Subject: [PATCH 10/16] Update migration guide --- MIGRATION.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index 0a47f723..ad5bf9bd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -119,6 +119,31 @@ For more details, follow the official Google Cloud guide: [Migrate from the Trac * **Attribute Mapping & Regex Filtering (`resource_regex`)**: `CloudTraceSpanExporter` allowed filtering resource attributes via regex to copy matching keys into span attributes. The standard OTLP exporter exports standard OpenTelemetry resource attributes attached to the `TracerProvider` directly. * **Custom Trace Service Client (`client`)**: You cannot pass a pre-configured `TraceServiceClient` instance directly to `OTLPSpanExporter`. If custom gRPC channels or metadata credentials are required, configure gRPC channel credentials programmatically as shown above. +#### Data Model differences & Data Limit Improvements + +Cloud Trace’s internal storage system uses the OpenTelemetry data model natively for organizing and storing your trace data. + +##### Data Model Comparison + +* **Payload Hierarchy & Resource Model:** + - **`CloudTraceSpanExporter` (API v2):** Sent a flat list of `Span` objects (`BatchWriteSpansRequest`). Because Cloud Trace v2 spans had no native resource container, resource attributes were flattened on the client side into span labels prefixed with `g.co/r//`. + - **OTLP Exporter (Native OTel Storage Model):** Uses the structured OTLP hierarchy (`ExportTraceServiceRequest` → `ResourceSpans` → `ScopeSpans` → `Span`). Resource attributes (`gcp.project_id`, `host.id`, `k8s.pod.name`, etc.) are stored natively in the `ResourceSpans` envelope and mapped server-side. +* **Attribute Keys & Semantic Conventions:** + - **`CloudTraceSpanExporter`:** Performed client-side remapping of OpenTelemetry HTTP keys to legacy Cloud Trace `/http/` label keys (e.g., `http.method` → `/http/method`, `http.status_code` → `/http/status_code`). + - **OTLP Exporter:** Preserves standard OpenTelemetry semantic convention keys (e.g., `http.request.method`, `http.response.status_code`, `url.full`) verbatim, stored and indexed natively in Cloud Trace. + +##### Expanded Limits Comparison + +| Limit / Metric | `CloudTraceSpanExporter` (API v2) | OTLP Exporter / Native OTel Storage | +| :--- | :--- | :--- | +| **Span Name Length** | Capped at **128 bytes** | Up to **1,024 bytes** (1 KiB) | +| **Attributes Per Span** | Hard limit of **32 attributes** | Up to **1,024 attributes** per span | +| **Attribute Key Size** | Capped at **128 bytes** | Up to **512 bytes** | +| **Attribute Value Size** | Truncated to **256 bytes** | Up to **64 KiB** | +| **Events Per Span** | Capped at **32 events** | Up to **256 events** per span | +| **Links Per Span** | Capped at **128 links** | Up to **128 links** per span | +| **Truncation Processing** | **Client-side:** Pre-emptively truncates keys/values and drops attributes in Python SDK code. | **Server-side:** Transmits raw OTLP payloads natively without client-side truncation. | + --- ## Migrate from OpenTelemetry Google Cloud Monitoring Exporter (`CloudMonitoringMetricsExporter`) to OTLP Exporter From eb2e0c7e7b0b9ef8950a112165c6b51a38714d4a Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 14:24:32 +0000 Subject: [PATCH 11/16] Revert resource detector deletion.. too much in one PR --- cloudbuild-e2e-cloud-functions-gen2.yaml | 1 + cloudbuild-e2e-gae-standard.yaml | 1 + docs-requirements.txt | 3 +- docs/apireference.rst | 2 + .../examples/cloud_monitoring/requirements.in | 2 +- .../cloud_resource_detector/requirements.in | 2 +- .../cloud_trace_exporter/requirements.in | 2 +- .../cloud_trace_propagator/requirements.in | 2 +- docs/examples/flask_e2e/requirements.in | 2 +- e2e-test-server/Dockerfile | 1 + e2e-test-server/requirements-dockerfile.txt | 2 +- .../CHANGELOG.md | 146 ++++++++++ opentelemetry-resourcedetector-gcp/LICENSE | 201 ++++++++++++++ .../MANIFEST.in | 9 + opentelemetry-resourcedetector-gcp/README.md | 5 - opentelemetry-resourcedetector-gcp/README.rst | 53 +++- opentelemetry-resourcedetector-gcp/mypy.ini | 4 + opentelemetry-resourcedetector-gcp/setup.cfg | 43 +++ opentelemetry-resourcedetector-gcp/setup.py | 34 +++ .../src/opentelemetry/py.typed | 0 .../gcp_resource_detector/__init__.py | 143 ++++++++++ .../gcp_resource_detector/_constants.py | 69 +++++ .../gcp_resource_detector/_faas.py | 60 ++++ .../gcp_resource_detector/_gae.py | 88 ++++++ .../gcp_resource_detector/_gce.py | 72 +++++ .../gcp_resource_detector/_gke.py | 56 ++++ .../gcp_resource_detector/_mapping.py | 222 +++++++++++++++ .../gcp_resource_detector/_metadata.py | 93 +++++++ .../gcp_resource_detector/version.py | 15 + .../test_gcp_resource_detector.ambr | 86 ++++++ .../tests/__snapshots__/test_mapping.ambr | 228 +++++++++++++++ .../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 ++++++++++++++++++ release.py | 5 +- tox.ini | 24 +- 40 files changed, 2434 insertions(+), 24 deletions(-) create mode 100644 opentelemetry-resourcedetector-gcp/CHANGELOG.md create mode 100644 opentelemetry-resourcedetector-gcp/LICENSE create mode 100644 opentelemetry-resourcedetector-gcp/MANIFEST.in delete mode 100644 opentelemetry-resourcedetector-gcp/README.md create mode 100644 opentelemetry-resourcedetector-gcp/mypy.ini create mode 100644 opentelemetry-resourcedetector-gcp/setup.cfg create mode 100644 opentelemetry-resourcedetector-gcp/setup.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py create mode 100644 opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr create mode 100644 opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr create mode 100644 opentelemetry-resourcedetector-gcp/tests/conftest.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_faas.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gae.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gce.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_gke.py create mode 100644 opentelemetry-resourcedetector-gcp/tests/test_mapping.py diff --git a/cloudbuild-e2e-cloud-functions-gen2.yaml b/cloudbuild-e2e-cloud-functions-gen2.yaml index 4de0a3a5..d58d40dc 100644 --- a/cloudbuild-e2e-cloud-functions-gen2.yaml +++ b/cloudbuild-e2e-cloud-functions-gen2.yaml @@ -27,6 +27,7 @@ steps: --no-deps \ --wheel-dir wheels \ ../opentelemetry-exporter-gcp-trace/ \ + ../opentelemetry-resourcedetector-gcp/ \ ../opentelemetry-propagator-gcp zip -qr function-source.zip . diff --git a/cloudbuild-e2e-gae-standard.yaml b/cloudbuild-e2e-gae-standard.yaml index 9f4f25a4..911d5e27 100644 --- a/cloudbuild-e2e-gae-standard.yaml +++ b/cloudbuild-e2e-gae-standard.yaml @@ -27,6 +27,7 @@ steps: --no-deps \ --wheel-dir wheels \ ../opentelemetry-exporter-gcp-trace/ \ + ../opentelemetry-resourcedetector-gcp/ \ ../opentelemetry-propagator-gcp zip -qr appsource.zip . diff --git a/docs-requirements.txt b/docs-requirements.txt index 4a368da9..a78f84f6 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -3,6 +3,5 @@ Sphinx sphinx-autodoc-typehints -e ./opentelemetry-exporter-gcp-trace -e ./opentelemetry-exporter-gcp-monitoring --e ./opentelemetry-exporter-gcp-logging -e ./opentelemetry-propagator-gcp -opentelemetry-resourcedetector-gcp +-e ./opentelemetry-resourcedetector-gcp diff --git a/docs/apireference.rst b/docs/apireference.rst index eff8b8d6..5c11cd4e 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -13,6 +13,7 @@ API Reference opentelemetry.exporter opentelemetry.propagators.cloud_trace_propagator + opentelemetry.resourcedetector .. toctree:: @@ -22,3 +23,4 @@ API Reference _autosummary/opentelemetry.exporter _autosummary/opentelemetry.propagators.cloud_trace_propagator + _autosummary/opentelemetry.resourcedetector diff --git a/docs/examples/cloud_monitoring/requirements.in b/docs/examples/cloud_monitoring/requirements.in index 8ee899f0..04ccf3c9 100644 --- a/docs/examples/cloud_monitoring/requirements.in +++ b/docs/examples/cloud_monitoring/requirements.in @@ -1,4 +1,4 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -opentelemetry-resourcedetector-gcp +-e ../../../opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_resource_detector/requirements.in b/docs/examples/cloud_resource_detector/requirements.in index d4b43e1d..78a09650 100644 --- a/docs/examples/cloud_resource_detector/requirements.in +++ b/docs/examples/cloud_resource_detector/requirements.in @@ -1,5 +1,5 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace -opentelemetry-resourcedetector-gcp +-e ../../../opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_trace_exporter/requirements.in b/docs/examples/cloud_trace_exporter/requirements.in index 8fb9dd77..2d062d4d 100644 --- a/docs/examples/cloud_trace_exporter/requirements.in +++ b/docs/examples/cloud_trace_exporter/requirements.in @@ -1,4 +1,4 @@ -e ../../../opentelemetry-exporter-gcp-trace -opentelemetry-resourcedetector-gcp +-e ../../../opentelemetry-resourcedetector-gcp opentelemetry-api opentelemetry-sdk diff --git a/docs/examples/cloud_trace_propagator/requirements.in b/docs/examples/cloud_trace_propagator/requirements.in index a1e8d21d..68238d86 100644 --- a/docs/examples/cloud_trace_propagator/requirements.in +++ b/docs/examples/cloud_trace_propagator/requirements.in @@ -1,7 +1,7 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace -e ../../../opentelemetry-propagator-gcp -opentelemetry-resourcedetector-gcp +-e ../../../opentelemetry-resourcedetector-gcp flask opentelemetry-api opentelemetry-instrumentation-flask diff --git a/docs/examples/flask_e2e/requirements.in b/docs/examples/flask_e2e/requirements.in index a1e8d21d..68238d86 100644 --- a/docs/examples/flask_e2e/requirements.in +++ b/docs/examples/flask_e2e/requirements.in @@ -1,7 +1,7 @@ -e ../../../opentelemetry-exporter-gcp-monitoring -e ../../../opentelemetry-exporter-gcp-trace -e ../../../opentelemetry-propagator-gcp -opentelemetry-resourcedetector-gcp +-e ../../../opentelemetry-resourcedetector-gcp flask opentelemetry-api opentelemetry-instrumentation-flask diff --git a/e2e-test-server/Dockerfile b/e2e-test-server/Dockerfile index fca43b45..800db1c0 100644 --- a/e2e-test-server/Dockerfile +++ b/e2e-test-server/Dockerfile @@ -28,6 +28,7 @@ FROM python-base as build-base # copy local dependencies COPY opentelemetry-exporter-gcp-trace opentelemetry-exporter-gcp-trace COPY opentelemetry-propagator-gcp opentelemetry-propagator-gcp +COPY opentelemetry-resourcedetector-gcp opentelemetry-resourcedetector-gcp WORKDIR $SRC/e2e-test-server # copy requirements/constraints COPY e2e-test-server/*.txt ./ diff --git a/e2e-test-server/requirements-dockerfile.txt b/e2e-test-server/requirements-dockerfile.txt index 593c54dc..11f15f9b 100644 --- a/e2e-test-server/requirements-dockerfile.txt +++ b/e2e-test-server/requirements-dockerfile.txt @@ -2,4 +2,4 @@ -r requirements-shared.txt ../opentelemetry-exporter-gcp-trace ../opentelemetry-propagator-gcp -opentelemetry-resourcedetector-gcp +../opentelemetry-resourcedetector-gcp diff --git a/opentelemetry-resourcedetector-gcp/CHANGELOG.md b/opentelemetry-resourcedetector-gcp/CHANGELOG.md new file mode 100644 index 00000000..a83afa46 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/CHANGELOG.md @@ -0,0 +1,146 @@ +# Changelog + +## Unreleased + +## Version 1.12.0a0 + +Released 2026-04-28 + +## Version 1.11.0a0 + +Released 2025-11-04 + +## Version 1.10.0a0 + +Released 2025-10-13 + +- Update opentelemetry-api/sdk dependencies to 1.3. + +- This is a breaking change which moves our official recource detector from `opentelemetry.resourcedetector.gcp_resource_detector._detector` +into `opentelemetry.resourcedetector.gcp_resource_detector` and deletes the outdated duplicate resource detector +which used to be there. Use `from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector` +to import it. See ([#389](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/389)) for details. + +## Version 1.9.0a0 + +Released 2025-02-03 + +## Version 1.8.0a0 + +Released 2025-01-08 + +- Use a shorter connection timeout for reading metadata + ([#362](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/362)) +- Fix creation of resources in _detector + ([#366](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/366)) + +## Version 1.7.0a0 + +Released 2024-08-27 + +- Implement GAE resource detection + ([#351](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/351)) +- Implement Cloud Run and Cloud Functions faas resource detection + ([#346](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/346)) +- Small fixups for resource detector code and tests + ([#345](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/345)) +- gcp_resource_detector: add missing timeout to requests call + ([#344](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/344)) +- Add support for Python 3.12 (#343) + ([#343](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/343)) +- Don't throw and exception when raise on error is set to false + ([#293](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/293)) + +## Version 1.6.0a0 + +Released 2023-10-16 + +- Use faas.instance instead of faas.id, and bump e2e test image + ([#271](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/271)) +- Map faas.* attributes to generic_task in resource mapping + ([#273](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/273)) + +## Version 1.5.0a0 + +Released 2023-05-17 + +- Add spec compliant GCE detection + ([#231](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/231)) +- Add support for Python 3.11 + ([#240](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/240)) + +## Version 1.4.0a0 + +Released 2022-12-05 + +- Drop support for Python 3.6, add 3.10 + ([#203](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/203)) +- Update no container name behaviour + ([#186](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/186)) + +## Version 1.3.0a0 + +Released 2022-04-21 + +- remove google-auth dependency for resource detection + ([#176](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/176)) + +## Version 1.2.0a0 + +Released 2022-04-05 + +## Version 1.1.0a0 + +Released 2022-01-13 + +## Version 1.0.0a0 + +Released 2021-04-22 + +- Drop support for Python 3.5 + ([#123](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/123)) +- Split tools package into separate propagator and resource detector packages + ([#124](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/124)) + +## Version 0.18b0 + +Released 2021-03-31 + +- Map span status code properly to GCT + ([#113](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/113)) +- Handle mixed case cloud propagator headers + ([#112](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/112)) + +## Version 0.17b0 + +Released 2021-02-04 + +## Version 0.16b1 + +Released 2021-01-14 + +## Version 0.15b0 + +Released 2020-11-04 + +## Version 0.14b0 + +Released 2020-10-27 + +- Fix breakages for opentelemetry-python v0.14b0 + ([#79](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/79)) + +## Version 0.13b0 + +Released 2020-09-17 + +## Version 0.12b0 + +Released 2020-08-17 + +## Version 0.11b0 + +Released 2020-08-05 + +- Add support for resources + ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) diff --git a/opentelemetry-resourcedetector-gcp/LICENSE b/opentelemetry-resourcedetector-gcp/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/opentelemetry-resourcedetector-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/opentelemetry-resourcedetector-gcp/MANIFEST.in b/opentelemetry-resourcedetector-gcp/MANIFEST.in new file mode 100644 index 00000000..aed3e332 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/MANIFEST.in @@ -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/opentelemetry-resourcedetector-gcp/README.md b/opentelemetry-resourcedetector-gcp/README.md deleted file mode 100644 index de4b8773..00000000 --- a/opentelemetry-resourcedetector-gcp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# OpenTelemetry Google Cloud Resource Detector - -The OpenTelemetry Google Cloud Resource Detector has moved to the [opentelemetry-python-contrib](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp) repository: - -https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp diff --git a/opentelemetry-resourcedetector-gcp/README.rst b/opentelemetry-resourcedetector-gcp/README.rst index d93f3b24..3c7f958b 100644 --- a/opentelemetry-resourcedetector-gcp/README.rst +++ b/opentelemetry-resourcedetector-gcp/README.rst @@ -1,6 +1,55 @@ OpenTelemetry Google Cloud Resource Detector ============================================ -The OpenTelemetry Google Cloud Resource Detector has moved to the `opentelemetry-python-contrib `_ repository: +.. image:: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp.svg + :target: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp -https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp +.. image:: https://readthedocs.org/projects/google-cloud-opentelemetry/badge/?version=latest + :target: https://google-cloud-opentelemetry.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +This library provides support for detecting GCP resources like GCE, GKE, etc. + +To get started with instrumentation in Google Cloud, see `Generate traces and metrics with +Python `_. + +To learn more about instrumentation and observability, including opinionated recommendations +for Google Cloud Observability, visit `Instrumentation and observability +`_. + +Installation +------------ + +.. code:: bash + + pip install opentelemetry-resourcedetector-gcp + +Usage +----- + +.. code:: 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/opentelemetry-resourcedetector-gcp/mypy.ini b/opentelemetry-resourcedetector-gcp/mypy.ini new file mode 100644 index 00000000..257b2b77 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +namespace_packages = True +explicit_package_bases = True +mypy_path = $MYPY_CONFIG_FILE_DIR/src diff --git a/opentelemetry-resourcedetector-gcp/setup.cfg b/opentelemetry-resourcedetector-gcp/setup.cfg new file mode 100644 index 00000000..22a35d19 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/setup.cfg @@ -0,0 +1,43 @@ +[metadata] +name = opentelemetry-resourcedetector-gcp +description = Google Cloud resource detector for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = Google +author_email = opentelemetry-pypi@google.com +url = https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/tree/main/opentelemetry-resourcedetector-gcp +platforms = any +license = Apache-2.0 +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.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 + +[options] +python_requires = >=3.9 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api ~= 1.30 + opentelemetry-sdk ~= 1.30 + requests ~= 2.24 + # TODO: remove when Python 3.7 is dropped + typing_extensions ~= 4.0 + +[options.packages.find] +where = src + +[options.extras_require] +test = + +[options.entry_points] +opentelemetry_resource_detector = + gcp_resource_detector = opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector \ No newline at end of file diff --git a/opentelemetry-resourcedetector-gcp/setup.py b/opentelemetry-resourcedetector-gcp/setup.py new file mode 100644 index 00000000..9d78a766 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/setup.py @@ -0,0 +1,34 @@ +# Copyright 2021 The OpenTelemetry Authors +# +# 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. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "resourcedetector", + "gcp_resource_detector", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup( + version=PACKAGE_INFO["__version__"], + package_data={"opentelemetry": ["py.typed"]}, +) diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed b/opentelemetry-resourcedetector-gcp/src/opentelemetry/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__init__.py new file mode 100644 index 00000000..0b9b9cbf --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/__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.resourcedetector.gcp_resource_detector import ( + _faas, + _gae, + _gce, + _gke, + _metadata, +) +from opentelemetry.resourcedetector.gcp_resource_detector._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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py new file mode 100644 index 00000000..2828b6d6 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py new file mode 100644 index 00000000..93af57fb --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_faas.py @@ -0,0 +1,60 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Implementation in this file copied from +# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/faas.go + +import os + +from opentelemetry.resourcedetector.gcp_resource_detector import _metadata + +_CLOUD_RUN_CONFIG_ENV = "K_CONFIGURATION" +_CLOUD_FUNCTION_TARGET_ENV = "FUNCTION_TARGET" +_FAAS_SERVICE_ENV = "K_SERVICE" +_FAAS_REVISION_ENV = "K_REVISION" + + +def on_cloud_run() -> bool: + return _CLOUD_RUN_CONFIG_ENV in os.environ + + +def on_cloud_functions() -> bool: + return _CLOUD_FUNCTION_TARGET_ENV in os.environ + + +def faas_name() -> str: + """The name of the Cloud Run or Cloud Function. + + Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may + throw exceptions. + """ + return os.environ[_FAAS_SERVICE_ENV] + + +def faas_version() -> str: + """The version/revision of the Cloud Run or Cloud Function. + + Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may + throw exceptions. + """ + return os.environ[_FAAS_REVISION_ENV] + + +def faas_instance() -> str: + return str(_metadata.get_metadata()["instance"]["id"]) + + +def faas_cloud_region() -> str: + region = _metadata.get_metadata()["instance"]["region"] + return region[region.rfind("/") + 1 :] diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py new file mode 100644 index 00000000..b67dc994 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gae.py @@ -0,0 +1,88 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Implementation in this file copied from +# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/app_engine.go + +import os + +from opentelemetry.resourcedetector.gcp_resource_detector import ( + _faas, + _gce, + _metadata, +) + +_GAE_SERVICE_ENV = "GAE_SERVICE" +_GAE_VERSION_ENV = "GAE_VERSION" +_GAE_INSTANCE_ENV = "GAE_INSTANCE" +_GAE_ENV = "GAE_ENV" +_GAE_STANDARD = "standard" + + +def on_app_engine_standard() -> bool: + return os.environ.get(_GAE_ENV) == _GAE_STANDARD + + +def on_app_engine() -> bool: + return _GAE_SERVICE_ENV in os.environ + + +def service_name() -> str: + """The service name of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_SERVICE_ENV] + + +def service_version() -> str: + """The service version of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_VERSION_ENV] + + +def service_instance() -> str: + """The service instance of the app engine service. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return os.environ[_GAE_INSTANCE_ENV] + + +def flex_availability_zone_and_region() -> _gce.ZoneAndRegion: + """The zone and region in which this program is running. + + Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. + """ + return _gce.availability_zone_and_region() + + +def standard_availability_zone() -> str: + """The zone the app engine service is running in. + + Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. + """ + zone = _metadata.get_metadata()["instance"]["zone"] + # zone is of the form "projects/233510669999/zones/us15" + return zone[zone.rfind("/") + 1 :] + + +def standard_cloud_region() -> str: + """The region the app engine service is running in. + + Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. + """ + return _faas.faas_cloud_region() diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py new file mode 100644 index 00000000..597fdb22 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_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.resourcedetector.gcp_resource_detector 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 Exception( + "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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gke.py new file mode 100644 index 00000000..489e1947 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_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.resourcedetector.gcp_resource_detector 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 Exception( + f"unrecognized format for cluster location: {cluster_location}" + ) diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py new file mode 100644 index 00000000..1e054b92 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_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.resourcedetector.gcp_resource_detector import _constants +from opentelemetry.resourcedetector.gcp_resource_detector._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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py new file mode 100644 index 00000000..51db5613 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_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/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py new file mode 100644 index 00000000..fad7719a --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/version.py @@ -0,0 +1,15 @@ +# Copyright 2021 The OpenTelemetry Authors +# +# 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. + +__version__ = "1.13.0.dev0" diff --git a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr new file mode 100644 index 00000000..fcc343a0 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_gcp_resource_detector.ambr @@ -0,0 +1,86 @@ +# 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/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr new file mode 100644 index 00000000..381a726b --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr @@ -0,0 +1,228 @@ +# 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 fallback global] + dict({ + 'labels': dict({ + 'job': 'servicename', + 'location': 'global', + 'namespace': 'servicens', + 'task_id': 'serviceinstanceid', + }), + '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 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 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/opentelemetry-resourcedetector-gcp/tests/conftest.py b/opentelemetry-resourcedetector-gcp/tests/conftest.py new file mode 100644 index 00000000..7ad7a833 --- /dev/null +++ b/opentelemetry-resourcedetector-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.resourcedetector.gcp_resource_detector 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/opentelemetry-resourcedetector-gcp/tests/test_faas.py b/opentelemetry-resourcedetector-gcp/tests/test_faas.py new file mode 100644 index 00000000..d1a47a70 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/test_faas.py @@ -0,0 +1,65 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import MagicMock + +import pytest +from opentelemetry.resourcedetector.gcp_resource_detector import _faas + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(fake_get_metadata): + pass + + +def test_detects_on_cloud_run(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") + assert _faas.on_cloud_run() + + +def test_detects_not_on_cloud_run() -> None: + assert not _faas.on_cloud_run() + + +def test_detects_on_cloud_functions(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") + assert _faas.on_cloud_functions() + + +def test_detects_not_on_cloud_functions() -> None: + assert not _faas.on_cloud_functions() + + +def test_detects_faas_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_SERVICE", "fake-service") + assert _faas.faas_name() == "fake-service" + + +def test_detects_faas_version(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("K_REVISION", "fake-revision") + assert _faas.faas_version() == "fake-revision" + + +def test_detects_faas_instance(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = {"instance": {"id": "0087244a"}} + assert _faas.faas_instance() == "0087244a" + + +def test_detects_faas_region(fake_get_metadata: MagicMock) -> None: + fake_get_metadata.return_value = { + "instance": {"region": "projects/233510669999/regions/us-east4"} + } + assert _faas.faas_cloud_region() == "us-east4" diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gae.py b/opentelemetry-resourcedetector-gcp/tests/test_gae.py new file mode 100644 index 00000000..e418881f --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/tests/test_gae.py @@ -0,0 +1,89 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import MagicMock + +import pytest +from opentelemetry.resourcedetector.gcp_resource_detector import _gae + + +# Reset stuff before every test +# pylint: disable=unused-argument +@pytest.fixture(autouse=True) +def autouse(fake_get_metadata): + pass + + +def test_detects_on_gae(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.on_app_engine() + + +def test_detects_not_on_gae() -> None: + assert not _gae.on_app_engine() + + +def test_detects_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_ENV", "standard") + assert _gae.on_app_engine_standard() + + +def test_detects_not_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.on_app_engine() + assert not _gae.on_app_engine_standard() + + +def test_detects_gae_service_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_SERVICE", "fake-service") + assert _gae.service_name() == "fake-service" + + +def test_detects_gae_service_version(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_VERSION", "fake-version") + assert _gae.service_version() == "fake-version" + + +def test_detects_gae_service_instance(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GAE_INSTANCE", "fake-instance") + assert _gae.service_instance() == "fake-instance" + + +def test_detects_gae_flex_zone_and_region( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"zone": "projects/233510669999/zones/us-east4-b"} + } + zone_and_region = _gae.flex_availability_zone_and_region() + assert zone_and_region.zone == "us-east4-b" + assert zone_and_region.region == "us-east4" + + +def test_gae_standard_zone( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"zone": "projects/233510669999/zones/us15"} + } + assert _gae.standard_availability_zone() == "us15" + + +def test_gae_standard_region( + fake_get_metadata: MagicMock, +) -> None: + fake_get_metadata.return_value = { + "instance": {"region": "projects/233510669999/regions/us-east4"} + } + assert _gae.standard_cloud_region() == "us-east4" diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gce.py b/opentelemetry-resourcedetector-gcp/tests/test_gce.py new file mode 100644 index 00000000..693ae0b1 --- /dev/null +++ b/opentelemetry-resourcedetector-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.resourcedetector.gcp_resource_detector 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(Exception, match="zone was not in the expected format"): + _gce.availability_zone_and_region() diff --git a/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py b/opentelemetry-resourcedetector-gcp/tests/test_gcp_resource_detector.py new file mode 100644 index 00000000..d1ef14cb --- /dev/null +++ b/opentelemetry-resourcedetector-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.resourcedetector.gcp_resource_detector 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/opentelemetry-resourcedetector-gcp/tests/test_gke.py b/opentelemetry-resourcedetector-gcp/tests/test_gke.py new file mode 100644 index 00000000..057fe85d --- /dev/null +++ b/opentelemetry-resourcedetector-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.resourcedetector.gcp_resource_detector 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( + Exception, match="unrecognized format for cluster location" + ): + _gke.availability_zone_or_region() diff --git a/opentelemetry-resourcedetector-gcp/tests/test_mapping.py b/opentelemetry-resourcedetector-gcp/tests/test_mapping.py new file mode 100644 index 00000000..eda679d9 --- /dev/null +++ b/opentelemetry-resourcedetector-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.resourcedetector.gcp_resource_detector._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/release.py b/release.py index 877b4091..db87abc9 100755 --- a/release.py +++ b/release.py @@ -52,9 +52,10 @@ # Map of different suffixes to use instead of the given ones in release_version ALTERNATE_SUFFIXES = { - # Mark monitoring and logging alpha + # Mark monitoring and resource detector alpha "opentelemetry-exporter-gcp-monitoring": "a0", "opentelemetry-exporter-gcp-logging": "a0", + "opentelemetry-resourcedetector-gcp": "a0", } @@ -137,8 +138,6 @@ def create_release_commit(release_version: str,) -> None: } for package_root in repo_root().glob("opentelemetry-*/"): - if not (package_root / "CHANGELOG.md").exists(): - continue if package_root in alternate_suffix_paths: suffix = alternate_suffix_paths[package_root] release_version_use = release_version_parsed.base_version + suffix diff --git a/tox.ini b/tox.ini index a8e96cc8..e7e7a4eb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,13 +5,13 @@ requires = tox>=4 envlist = ; Add the `ci` factor to any env that should be running during CI. - py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,cloudlogging} - {lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,cloudlogging} + py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} + {lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} docs-ci ; These are development commands and share the same virtualenv within each ; package root directory - {fix}-{cloudtrace,cloudmonitoring,propagator,cloudlogging} + {fix}-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging} ; Installs dev depenedencies and all packages in this repo with editable ; install into a single env. Useful for editor autocompletion @@ -23,6 +23,12 @@ base_deps = -c {toxinidir}/dev-constraints.txt -e {toxinidir}/test-common +; the inter-monorepo dependencies +monorepo_deps = + cloudmonitoring: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ + cloudtrace: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ + cloudlogging: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ + dev_basepython = python3.10 dev_deps = {[constants]base_deps} @@ -52,12 +58,14 @@ setenv = cloudmonitoring: PACKAGE_NAME = opentelemetry-exporter-gcp-monitoring cloudlogging: PACKAGE_NAME = opentelemetry-exporter-gcp-logging propagator: PACKAGE_NAME = opentelemetry-propagator-gcp + resourcedetector: PACKAGE_NAME = opentelemetry-resourcedetector-gcp -[testenv:py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] +[testenv:py3{9,10,11,12,13}-ci-test-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] deps = ; editable install the package itself -e {toxinidir}/{env:PACKAGE_NAME} test: {[constants]base_deps} + test: {[constants]monorepo_deps} ; test specific deps test: pytest test: syrupy @@ -73,12 +81,13 @@ allowlist_externals = bash {toxinidir}/get_mock_server.sh -[testenv:{lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] +[testenv:{lint,mypy}-ci-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] basepython = {[constants]dev_basepython} deps = ; editable install the package itself -e {toxinidir}/{env:PACKAGE_NAME} {[constants]dev_deps} + {[constants]monorepo_deps} changedir = {env:PACKAGE_NAME} commands = lint: black . --diff --check @@ -100,15 +109,17 @@ allowlist_externals = make bash -[testenv:{fix}-{cloudtrace,cloudmonitoring,propagator,cloudlogging}] +[testenv:{fix}-{cloudtrace,cloudmonitoring,propagator,resourcedetector, cloudlogging}] basepython = {[constants]dev_basepython} envdir = cloudtrace: opentelemetry-exporter-gcp-trace/venv cloudmonitoring: opentelemetry-exporter-gcp-monitoring/venv propagator: opentelemetry-propagator-gcp/venv + resourcedetector: opentelemetry-resourcedetector-gcp/venv cloudlogging: opentelemetry-exporter-gcp-logging/venv deps = {[constants]dev_deps} + {[constants]monorepo_deps} -e {env:PACKAGE_NAME} changedir = {env:PACKAGE_NAME} @@ -125,4 +136,5 @@ deps = -e opentelemetry-exporter-gcp-monitoring -e opentelemetry-exporter-gcp-trace -e opentelemetry-propagator-gcp + -e opentelemetry-resourcedetector-gcp -e opentelemetry-exporter-gcp-logging From 768f9b5da543510329abbdd7dc596dbaeff5f801 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 14:43:14 +0000 Subject: [PATCH 12/16] Fix linter --- .../src/opentelemetry/exporter/cloud_logging/__init__.py | 3 ++- .../tests/test_cloud_logging.py | 5 ++--- .../src/opentelemetry/exporter/cloud_monitoring/__init__.py | 4 ++-- .../src/opentelemetry/exporter/cloud_trace/__init__.py | 3 ++- .../tests/test_cloud_trace_exporter.py | 1 - 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py index a2d68793..37f9dbe3 100644 --- a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py +++ b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py @@ -18,7 +18,6 @@ import json import logging import re -from typing_extensions import deprecated from base64 import b64encode from functools import partial from typing import ( @@ -69,6 +68,8 @@ from proto.datetime_helpers import ( # type: ignore[import] DatetimeWithNanoseconds, ) +from typing_extensions import deprecated # type: ignore[attr-defined] + DEFAULT_MAX_ENTRY_SIZE = 256000 # 256 KB DEFAULT_MAX_REQUEST_SIZE = 10000000 # 10 MB diff --git a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py index 1118f1c4..7309a195 100644 --- a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py +++ b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py @@ -451,7 +451,6 @@ def test_structured_json_lines(): def test_deprecation_warning(): buf = StringIO() with pytest.deprecated_call(): - CloudLoggingExporter( - project_id=PROJECT_ID, structured_json_file=buf - ) + CloudLoggingExporter(project_id=PROJECT_ID, structured_json_file=buf) + diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index fa7b3e38..f2669b14 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -15,11 +15,11 @@ import logging import math import random -from typing_extensions import deprecated from dataclasses import replace from time import time_ns from typing import Dict, List, NoReturn, Optional, Set, Union + import google.auth from google.api.distribution_pb2 import ( # pylint: disable=no-name-in-module Distribution, @@ -65,10 +65,10 @@ Metric, MetricExporter, MetricExportResult, - MetricsData, NumberDataPoint, Sum, ) +from typing_extensions import deprecated # type: ignore[attr-defined] logger = logging.getLogger(__name__) MAX_BATCH_WRITE = 200 diff --git a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index cf42df51..c6e9ef3f 100644 --- a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -72,9 +72,9 @@ import logging import re -from typing_extensions import deprecated from collections.abc import Sequence as SequenceABC from os import environ + from typing import ( Any, Dict, @@ -120,6 +120,7 @@ from opentelemetry.trace import format_span_id, format_trace_id from opentelemetry.trace.status import StatusCode from opentelemetry.util import types +from typing_extensions import deprecated # type: ignore[attr-defined] logger = logging.getLogger(__name__) diff --git a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py index f4537cf9..69c3bced 100644 --- a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py +++ b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py @@ -67,7 +67,6 @@ def test_deprecation_warning(self): with self.assertWarns(DeprecationWarning): CloudTraceSpanExporter(project_id=self.project_id) - @classmethod def setUpClass(cls): cls.project_id = "PROJECT" From e3e0fb8586e26afbc672f2302cad90cbf0ff7a3b Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 14:52:22 +0000 Subject: [PATCH 13/16] Fix linter --- .../src/opentelemetry/exporter/cloud_monitoring/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index f2669b14..97fdf18a 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -65,6 +65,7 @@ Metric, MetricExporter, MetricExportResult, + MetricsData, NumberDataPoint, Sum, ) From 058b05741840f191ae91b145b94d4089136e121b Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 14:59:45 +0000 Subject: [PATCH 14/16] Fix linter again --- opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py | 2 -- .../src/opentelemetry/exporter/cloud_monitoring/__init__.py | 1 - .../src/opentelemetry/exporter/cloud_trace/__init__.py | 1 - 3 files changed, 4 deletions(-) diff --git a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py index 7309a195..50079a1d 100644 --- a/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py +++ b/opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py @@ -452,5 +452,3 @@ def test_deprecation_warning(): buf = StringIO() with pytest.deprecated_call(): CloudLoggingExporter(project_id=PROJECT_ID, structured_json_file=buf) - - diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index 97fdf18a..7bd9f406 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -19,7 +19,6 @@ from time import time_ns from typing import Dict, List, NoReturn, Optional, Set, Union - import google.auth from google.api.distribution_pb2 import ( # pylint: disable=no-name-in-module Distribution, diff --git a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index c6e9ef3f..4b06e25e 100644 --- a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -74,7 +74,6 @@ import re from collections.abc import Sequence as SequenceABC from os import environ - from typing import ( Any, Dict, From da2d61425d33b9e580826678c941da5be7df5a7b Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 15:10:05 +0000 Subject: [PATCH 15/16] Fix lint --- .../src/opentelemetry/exporter/cloud_logging/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py index 37f9dbe3..5faa415b 100644 --- a/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py +++ b/opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py @@ -70,7 +70,6 @@ ) from typing_extensions import deprecated # type: ignore[attr-defined] - DEFAULT_MAX_ENTRY_SIZE = 256000 # 256 KB DEFAULT_MAX_REQUEST_SIZE = 10000000 # 10 MB From 732913b366b7d170575be10490b4033208671b9a Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Fri, 24 Jul 2026 19:36:59 +0000 Subject: [PATCH 16/16] Update/fix MIGRATION.md --- MIGRATION.md | 172 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 18 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index ad5bf9bd..30deaa78 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -121,7 +121,8 @@ For more details, follow the official Google Cloud guide: [Migrate from the Trac #### Data Model differences & Data Limit Improvements -Cloud Trace’s internal storage system uses the OpenTelemetry data model natively for organizing and storing your trace data. +Cloud Trace’s internal storage system uses the OpenTelemetry data model natively for organizing and storing your trace data. For complete documentation on OTLP trace mapping and limits, see [Migrate from the Trace exporter to the OTLP endpoint](https://cloud.google.com/trace/docs/migrate-to-otlp-endpoints). + ##### Data Model Comparison @@ -134,17 +135,7 @@ Cloud Trace’s internal storage system uses the OpenTelemetry data model native ##### Expanded Limits Comparison -| Limit / Metric | `CloudTraceSpanExporter` (API v2) | OTLP Exporter / Native OTel Storage | -| :--- | :--- | :--- | -| **Span Name Length** | Capped at **128 bytes** | Up to **1,024 bytes** (1 KiB) | -| **Attributes Per Span** | Hard limit of **32 attributes** | Up to **1,024 attributes** per span | -| **Attribute Key Size** | Capped at **128 bytes** | Up to **512 bytes** | -| **Attribute Value Size** | Truncated to **256 bytes** | Up to **64 KiB** | -| **Events Per Span** | Capped at **32 events** | Up to **256 events** per span | -| **Links Per Span** | Capped at **128 links** | Up to **128 links** per span | -| **Truncation Processing** | **Client-side:** Pre-emptively truncates keys/values and drops attributes in Python SDK code. | **Server-side:** Transmits raw OTLP payloads natively without client-side truncation. | - ---- +For complete documentation on OTLP trace mapping and limits, see [Migrate from the Trace exporter to the OTLP endpoint](https://cloud.google.com/trace/docs/migrate-to-otlp-endpoints). ## Migrate from OpenTelemetry Google Cloud Monitoring Exporter (`CloudMonitoringMetricsExporter`) to OTLP Exporter @@ -230,6 +221,156 @@ provider = MeterProvider(metric_readers=[reader]) metrics.set_meter_provider(provider) ``` +### Strategy 2: Transition via Double-Writing + +To avoid monitoring gaps, run both the legacy exporter (`CloudMonitoringMetricsExporter`) and the standard OTLP exporter (`OTLPMetricExporter`) concurrently. This sends metrics to both `workload.googleapis.com/` and `prometheus.googleapis.com/` simultaneously, allowing you to update dashboards and alerting policies without any monitoring downtime. + +> [!NOTE] +> **Cost Consideration:** Double-writing metrics will double your metric ingestion volume, which will increase your Google Cloud Monitoring costs during the transition period. It also increases CPU and memory usage on your application. + +```python +import google.auth +import google.auth.transport.grpc +import google.auth.transport.requests +import grpc +from google.auth.transport.grpc import AuthMetadataPlugin +from opentelemetry import metrics +from opentelemetry.exporter.cloud_monitoring import ( + CloudMonitoringMetricsExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + +credentials, project_id = google.auth.default() +request = google.auth.transport.requests.Request() +auth_metadata_plugin = AuthMetadataPlugin( + credentials=credentials, request=request +) +channel_creds = grpc.composite_channel_credentials( + grpc.ssl_channel_credentials(), + grpc.metadata_call_credentials(auth_metadata_plugin), +) + +# 1. Instantiate legacy exporter (writes to workload.googleapis.com/) +legacy_exporter = CloudMonitoringMetricsExporter(project_id=project_id) + +# 2. Instantiate OTLP exporter (writes to prometheus.googleapis.com/) +otlp_exporter = OTLPMetricExporter( + endpoint="telemetry.googleapis.com", + credentials=channel_creds, +) + +# 3. Register both readers with MeterProvider +provider = MeterProvider( + metric_readers=[ + PeriodicExportingMetricReader(legacy_exporter), + PeriodicExportingMetricReader(otlp_exporter), + ] +) +metrics.set_meter_provider(provider) +``` + +#### Verification and Cutover + +1. **Verify New Metrics Ingestion:** Once double-writing is deployed, verify in Metrics Explorer that new metrics are arriving under the `prometheus.googleapis.com/` domain. +2. **Update Dashboards & Alerts:** Duplicate or update existing Cloud Monitoring dashboards, charts, and alerting policies to query `prometheus.googleapis.com/` metric names instead of `workload.googleapis.com/`. +3. **Cutover & Decommission:** Once all dashboards and alerting rules are updated and verified against the new OTLP metric data streams, remove `CloudMonitoringMetricsExporter` from your `MeterProvider` to complete the migration and eliminate double-ingestion. + +### Strategy 3: Custom Metric Prefixing / Wrapped Exporter + +If you want to preserve legacy metric prefixes (such as `workload.googleapis.com/`) during migration, wrap your `MetricExporter` with a custom exporter wrapper that prepends the prefix to metric names before export. + +```python +from opentelemetry import metrics +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricExporter, + MetricExportResult, + MetricsData, + ResourceMetrics, + ScopeMetrics, + Metric, + PeriodicExportingMetricReader, +) + + +class PrefixMetricExporter(MetricExporter): + """Wraps a MetricExporter to prepend a prefix to metric names before export.""" + + def __init__( + self, + delegate: MetricExporter, + prefix: str = "workload.googleapis.com/", + ): + super().__init__( + preferred_temporality=getattr(delegate, "_preferred_temporality", None), + preferred_aggregation=getattr(delegate, "_preferred_aggregation", None), + ) + self._delegate = delegate + self._prefix = prefix + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + new_resource_metrics = [] + for rm in metrics_data.resource_metrics: + new_scope_metrics = [] + for sm in rm.scope_metrics: + new_metrics = [] + for m in sm.metrics: + new_metrics.append( + Metric( + name=f"{self._prefix}{m.name}", + description=m.description, + unit=m.unit, + data=m.data, + ) + ) + new_scope_metrics.append( + ScopeMetrics( + scope=sm.scope, + metrics=new_metrics, + schema_url=sm.schema_url, + ) + ) + new_resource_metrics.append( + ResourceMetrics( + resource=rm.resource, + scope_metrics=new_scope_metrics, + schema_url=rm.schema_url, + ) + ) + new_metrics_data = MetricsData(resource_metrics=new_resource_metrics) + return self._delegate.export( + new_metrics_data, timeout_millis=timeout_millis, **kwargs + ) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + self._delegate.shutdown(timeout_millis=timeout_millis, **kwargs) + + def force_flush(self, timeout_millis: float = 10_000, **kwargs) -> bool: + return self._delegate.force_flush(timeout_millis=timeout_millis, **kwargs) + + +# Wrap the OTLPMetricExporter with the prefix exporter +exporter = PrefixMetricExporter( + delegate=otlp_exporter, + prefix="workload.googleapis.com/", +) + +provider = MeterProvider( + metric_readers=[PeriodicExportingMetricReader(exporter)], +) +metrics.set_meter_provider(provider) +``` + + ### Mapping and Limitations #### Configuration Mapping @@ -241,11 +382,6 @@ metrics.set_meter_provider(provider) | `project_id` | Resource attribute: `gcp.project_id` | Set via `OTEL_RESOURCE_ATTRIBUTES="gcp.project_id=your-project-id"` or detected automatically via `opentelemetry-resourcedetector-gcp`. | | `client` | N/A | Pre-configured `MetricServiceClient` cannot be passed directly to `OTLPMetricExporter`. | -#### Limitations & Breaking Changes - -* **Metric Domain & Prefix:** Metric names in Cloud Monitoring will change from `workload.googleapis.com/` to `prometheus.googleapis.com//`. Existing Cloud Monitoring dashboards and alerts relying on `workload.googleapis.com/` metrics must be updated to query `prometheus.googleapis.com/` metrics. -* **Unique Identifier Handling:** The `add_unique_identifier` parameter in `CloudMonitoringMetricsExporter` is not supported. Use standard resource attributes like `service.instance.id` or `host.id` to separate metric streams from distinct exporter instances. - #### Conversion Logic & Metric Mapping Differences The conversion logic used in `CloudMonitoringMetricsExporter` can be found in [`opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py`](opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L184-L365). Standard OTLP endpoints convert OTLP metric data server-side according to the [Google Cloud Telemetry API Metric Mapping specification](https://docs.cloud.google.com/stackdriver/docs/reference/telemetry/v1.metrics#metric-mapping-reference-info). @@ -254,7 +390,7 @@ Key differences include: * **Metric Domain & Name Structure:** - **`CloudMonitoringMetricsExporter`:** Ingests metrics under `workload.googleapis.com/` (or custom `prefix`). - - **Telemetry API:** Ingests metrics under `prometheus.googleapis.com//` (e.g. `/counter`, `/gauge`, `/histogram`, `/delta`, `/summary`). + - **Telemetry API:** Ingests metrics under `prometheus.googleapis.com//` (e.g. `/counter`, `/gauge`, `/histogram`, `/delta`, `/summary`). You can override the prefix to `workload.googleapis.com/` or `custom.google.com/` using the strategies mentioned above, but no other prefix is accepted by the API. * **Value Types (`INT64` vs `DOUBLE`):** - **`CloudMonitoringMetricsExporter`:** Preserves `INT64` value types when integer data points are passed (`TypedValue(int64_value=...)`). - **Telemetry API:** Translates **all OTLP `INT64` scalar metrics to `DOUBLE`** in Cloud Monitoring to prevent Monarch value-type schema collisions.