# Copyright 2021 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.
import logging
import os
import requests
from opentelemetry.context import attach, detach, set_value
from opentelemetry.sdk.resources import Resource, ResourceDetector
_GCP_METADATA_URL = (
"http://metadata.google.internal/computeMetadata/v1/?recursive=true"
)
_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"}
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
).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
[docs]
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(
{
"host.id": all_metadata["instance"]["id"],
"gcp.resource_type": "gce_instance",
}
)
return common_attributes
[docs]
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(
{
"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",
}
)
return common_attributes
[docs]
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(
{
"cloud.platform": "gcp_cloud_run",
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
"faas.instance": all_metadata["instance"]["id"],
"gcp.resource_type": "cloud_run",
}
)
return common_attributes
[docs]
def get_cloudfunctions_resources():
"""Resource finder for Cloud Functions attributes"""
if os.getenv("FUNCTION_TARGET") 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(
{
"cloud.platform": "gcp_cloud_functions",
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
"faas.instance": all_metadata["instance"]["id"],
"gcp.resource_type": "cloud_functions",
}
)
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),
]
[docs]
class NoGoogleResourcesFound(Exception):
pass
[docs]
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)