Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions e2e-test-server/e2e_test_server/scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
from opentelemetry.propagators.cloud_trace_propagator import (
CloudTraceFormatPropagator,
)
from opentelemetry.resourcedetector.gcp_resource_detector._detector import (
GoogleCloudResourceDetector,
)
from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
Expand Down
7 changes: 6 additions & 1 deletion opentelemetry-resourcedetector-gcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

## Unreleased

Update opentelemetry-api/sdk dependencies to 1.3.
- 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

Expand Down
26 changes: 23 additions & 3 deletions opentelemetry-resourcedetector-gcp/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,29 @@ Installation

pip install opentelemetry-resourcedetector-gcp

..
TODO: Add usage info here

Usage
-----

.. code:: python
Comment thread
DylanRussell marked this conversation as resolved.

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
----------
Expand Down
4 changes: 4 additions & 0 deletions opentelemetry-resourcedetector-gcp/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ where = src

[options.extras_require]
test =

[options.entry_points]
opentelemetry_resource_detector =
gcp_resource_detector = opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector
Comment thread
DylanRussell marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 Google LLC
# 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.
Expand All @@ -12,195 +12,132 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from typing import Mapping

import requests
from opentelemetry.context import attach, detach, set_value
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

_GCP_METADATA_URL = (
"http://metadata.google.internal/computeMetadata/v1/?recursive=true"
)
_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"}
_TIMEOUT_SEC = 5

logger = logging.getLogger(__name__)


def _get_google_metadata_and_common_attributes():
token = attach(set_value("suppress_instrumentation", True))
all_metadata = requests.get(
_GCP_METADATA_URL,
headers=_GCP_METADATA_URL_HEADER,
timeout=_TIMEOUT_SEC,
).json()
detach(token)
common_attributes = {
"cloud.account.id": all_metadata["project"]["projectId"],
"cloud.provider": "gcp",
"cloud.zone": all_metadata["instance"]["zone"].split("/")[-1],
}
return common_attributes, all_metadata


def get_gce_resources():
"""Resource finder for common GCE attributes

See: https://cloud.google.com/compute/docs/storing-retrieving-metadata
"""
(
common_attributes,
all_metadata,
) = _get_google_metadata_and_common_attributes()
common_attributes.update(

class GoogleCloudResourceDetector(ResourceDetector):
Comment thread
DylanRussell marked this conversation as resolved.
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(
{
"host.id": all_metadata["instance"]["id"],
"gcp.resource_type": "gce_instance",
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(),
}
)
return common_attributes


def get_gke_resources():
"""Resource finder for GKE attributes"""

if os.getenv("KUBERNETES_SERVICE_HOST") is None:
return {}

(
common_attributes,
all_metadata,
) = _get_google_metadata_and_common_attributes()

container_name = os.getenv("CONTAINER_NAME")
if container_name is not None:
common_attributes["container.name"] = container_name

# Fallback to reading namespace from a file is the env var is not set
pod_namespace = os.getenv("NAMESPACE")
if pod_namespace is None:
try:
with open(
"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r"
) as namespace_file:
pod_namespace = namespace_file.read().strip()
except FileNotFoundError:
pod_namespace = ""

common_attributes.update(
def _gce_resource() -> Resource:
zone_and_region = _gce.availability_zone_and_region()
return _make_resource(
{
"k8s.cluster.name": all_metadata["instance"]["attributes"][
"cluster-name"
],
"k8s.namespace.name": pod_namespace,
"k8s.pod.name": os.getenv("POD_NAME", os.getenv("HOSTNAME", "")),
"host.id": all_metadata["instance"]["id"],
"gcp.resource_type": "gke_container",
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(),
}
)
return common_attributes


def get_cloudrun_resources():
"""Resource finder for Cloud Run attributes"""

if os.getenv("K_CONFIGURATION") is None:
return {}

(
common_attributes,
all_metadata,
) = _get_google_metadata_and_common_attributes()

faas_name = os.getenv("K_SERVICE")
if faas_name is not None:
common_attributes["faas.name"] = str(faas_name)

faas_version = os.getenv("K_REVISION")
if faas_version is not None:
common_attributes["faas.version"] = str(faas_version)

common_attributes.update(
def _cloud_run_resource() -> Resource:
return _make_resource(
{
"cloud.platform": "gcp_cloud_run",
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
"faas.instance": all_metadata["instance"]["id"],
"gcp.resource_type": "cloud_run",
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(),
}
)
return common_attributes


def get_cloudfunctions_resources():
"""Resource finder for Cloud Functions attributes"""

if os.getenv("FUNCTION_TARGET") is None:
return {}
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(),
}
)

(
common_attributes,
all_metadata,
) = _get_google_metadata_and_common_attributes()

faas_name = os.getenv("K_SERVICE")
if faas_name is not None:
common_attributes["faas.name"] = str(faas_name)
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_version = os.getenv("K_REVISION")
if faas_version is not None:
common_attributes["faas.version"] = str(faas_version)
faas_name = _gae.service_name()
faas_version = _gae.service_version()
faas_instance = _gae.service_instance()

common_attributes.update(
return _make_resource(
{
"cloud.platform": "gcp_cloud_functions",
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
"faas.instance": all_metadata["instance"]["id"],
"gcp.resource_type": "cloud_functions",
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,
}
)
return common_attributes


# Order here matters. Since a GKE_CONTAINER is a specialized type of GCE_INSTANCE
# We need to first check if it matches the criteria for being a GKE_CONTAINER
# before falling back and checking if its a GCE_INSTANCE.
# This list should be sorted from most specialized to least specialized.
_RESOURCE_FINDERS = [
("gke_container", get_gke_resources),
("cloud_run", get_cloudrun_resources),
("cloud_functions", get_cloudfunctions_resources),
("gce_instance", get_gce_resources),
]


class NoGoogleResourcesFound(Exception):
pass
def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource:
return Resource(
{
ResourceAttributes.CLOUD_PROVIDER: "gcp",
ResourceAttributes.CLOUD_ACCOUNT_ID: _metadata.get_metadata()[
"project"
]["projectId"],
**attrs,
}
)


class GoogleCloudResourceDetector(ResourceDetector):
def __init__(self, raise_on_error=False):
super().__init__(raise_on_error)
self.cached = False
self.gcp_resources = {}

def detect(self) -> "Resource":
if not self.cached:
self.cached = True
for resource_type, resource_finder in _RESOURCE_FINDERS:
try:
found_resources = resource_finder()
# pylint: disable=broad-except
except Exception as ex:
logger.warning(
"Exception %s occured attempting %s resource detection",
ex,
resource_type,
)
found_resources = None
if found_resources:
self.gcp_resources = found_resources
break
if self.raise_on_error and not self.gcp_resources:
raise NoGoogleResourcesFound()
return Resource(self.gcp_resources)
__all__ = ["GoogleCloudResourceDetector"]
Loading