|
1 | | -# Copyright 2021 Google LLC |
| 1 | +# Copyright 2025 Google LLC |
2 | 2 | # |
3 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
4 | 4 | # you may not use this file except in compliance with the License. |
|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | | -import logging |
16 | | -import os |
| 15 | +from typing import Mapping |
17 | 16 |
|
18 | | -import requests |
19 | | -from opentelemetry.context import attach, detach, set_value |
| 17 | +from opentelemetry.resourcedetector.gcp_resource_detector import ( |
| 18 | + _faas, |
| 19 | + _gae, |
| 20 | + _gce, |
| 21 | + _gke, |
| 22 | + _metadata, |
| 23 | +) |
| 24 | +from opentelemetry.resourcedetector.gcp_resource_detector._constants import ( |
| 25 | + ResourceAttributes, |
| 26 | +) |
20 | 27 | from opentelemetry.sdk.resources import Resource, ResourceDetector |
| 28 | +from opentelemetry.util.types import AttributeValue |
21 | 29 |
|
22 | | -_GCP_METADATA_URL = ( |
23 | | - "http://metadata.google.internal/computeMetadata/v1/?recursive=true" |
24 | | -) |
25 | | -_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"} |
26 | | -_TIMEOUT_SEC = 5 |
27 | | - |
28 | | -logger = logging.getLogger(__name__) |
29 | | - |
30 | | - |
31 | | -def _get_google_metadata_and_common_attributes(): |
32 | | - token = attach(set_value("suppress_instrumentation", True)) |
33 | | - all_metadata = requests.get( |
34 | | - _GCP_METADATA_URL, |
35 | | - headers=_GCP_METADATA_URL_HEADER, |
36 | | - timeout=_TIMEOUT_SEC, |
37 | | - ).json() |
38 | | - detach(token) |
39 | | - common_attributes = { |
40 | | - "cloud.account.id": all_metadata["project"]["projectId"], |
41 | | - "cloud.provider": "gcp", |
42 | | - "cloud.zone": all_metadata["instance"]["zone"].split("/")[-1], |
43 | | - } |
44 | | - return common_attributes, all_metadata |
45 | | - |
46 | | - |
47 | | -def get_gce_resources(): |
48 | | - """Resource finder for common GCE attributes |
49 | | -
|
50 | | - See: https://cloud.google.com/compute/docs/storing-retrieving-metadata |
51 | | - """ |
52 | | - ( |
53 | | - common_attributes, |
54 | | - all_metadata, |
55 | | - ) = _get_google_metadata_and_common_attributes() |
56 | | - common_attributes.update( |
| 30 | + |
| 31 | +class GoogleCloudResourceDetector(ResourceDetector): |
| 32 | + def detect(self) -> Resource: |
| 33 | + # pylint: disable=too-many-return-statements |
| 34 | + if not _metadata.is_available(): |
| 35 | + return Resource.get_empty() |
| 36 | + |
| 37 | + if _gke.on_gke(): |
| 38 | + return _gke_resource() |
| 39 | + if _faas.on_cloud_functions(): |
| 40 | + return _cloud_functions_resource() |
| 41 | + if _faas.on_cloud_run(): |
| 42 | + return _cloud_run_resource() |
| 43 | + if _gae.on_app_engine(): |
| 44 | + return _gae_resource() |
| 45 | + if _gce.on_gce(): |
| 46 | + return _gce_resource() |
| 47 | + |
| 48 | + return Resource.get_empty() |
| 49 | + |
| 50 | + |
| 51 | +def _gke_resource() -> Resource: |
| 52 | + zone_or_region = _gke.availability_zone_or_region() |
| 53 | + zone_or_region_key = ( |
| 54 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE |
| 55 | + if zone_or_region.type == "zone" |
| 56 | + else ResourceAttributes.CLOUD_REGION |
| 57 | + ) |
| 58 | + return _make_resource( |
57 | 59 | { |
58 | | - "host.id": all_metadata["instance"]["id"], |
59 | | - "gcp.resource_type": "gce_instance", |
| 60 | + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_KUBERNETES_ENGINE, |
| 61 | + zone_or_region_key: zone_or_region.value, |
| 62 | + ResourceAttributes.K8S_CLUSTER_NAME: _gke.cluster_name(), |
| 63 | + ResourceAttributes.HOST_ID: _gke.host_id(), |
60 | 64 | } |
61 | 65 | ) |
62 | | - return common_attributes |
63 | | - |
64 | 66 |
|
65 | | -def get_gke_resources(): |
66 | | - """Resource finder for GKE attributes""" |
67 | 67 |
|
68 | | - if os.getenv("KUBERNETES_SERVICE_HOST") is None: |
69 | | - return {} |
70 | | - |
71 | | - ( |
72 | | - common_attributes, |
73 | | - all_metadata, |
74 | | - ) = _get_google_metadata_and_common_attributes() |
75 | | - |
76 | | - container_name = os.getenv("CONTAINER_NAME") |
77 | | - if container_name is not None: |
78 | | - common_attributes["container.name"] = container_name |
79 | | - |
80 | | - # Fallback to reading namespace from a file is the env var is not set |
81 | | - pod_namespace = os.getenv("NAMESPACE") |
82 | | - if pod_namespace is None: |
83 | | - try: |
84 | | - with open( |
85 | | - "/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r" |
86 | | - ) as namespace_file: |
87 | | - pod_namespace = namespace_file.read().strip() |
88 | | - except FileNotFoundError: |
89 | | - pod_namespace = "" |
90 | | - |
91 | | - common_attributes.update( |
| 68 | +def _gce_resource() -> Resource: |
| 69 | + zone_and_region = _gce.availability_zone_and_region() |
| 70 | + return _make_resource( |
92 | 71 | { |
93 | | - "k8s.cluster.name": all_metadata["instance"]["attributes"][ |
94 | | - "cluster-name" |
95 | | - ], |
96 | | - "k8s.namespace.name": pod_namespace, |
97 | | - "k8s.pod.name": os.getenv("POD_NAME", os.getenv("HOSTNAME", "")), |
98 | | - "host.id": all_metadata["instance"]["id"], |
99 | | - "gcp.resource_type": "gke_container", |
| 72 | + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_COMPUTE_ENGINE, |
| 73 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone_and_region.zone, |
| 74 | + ResourceAttributes.CLOUD_REGION: zone_and_region.region, |
| 75 | + ResourceAttributes.HOST_TYPE: _gce.host_type(), |
| 76 | + ResourceAttributes.HOST_ID: _gce.host_id(), |
| 77 | + ResourceAttributes.HOST_NAME: _gce.host_name(), |
100 | 78 | } |
101 | 79 | ) |
102 | | - return common_attributes |
103 | | - |
104 | | - |
105 | | -def get_cloudrun_resources(): |
106 | | - """Resource finder for Cloud Run attributes""" |
107 | 80 |
|
108 | | - if os.getenv("K_CONFIGURATION") is None: |
109 | | - return {} |
110 | 81 |
|
111 | | - ( |
112 | | - common_attributes, |
113 | | - all_metadata, |
114 | | - ) = _get_google_metadata_and_common_attributes() |
115 | | - |
116 | | - faas_name = os.getenv("K_SERVICE") |
117 | | - if faas_name is not None: |
118 | | - common_attributes["faas.name"] = str(faas_name) |
119 | | - |
120 | | - faas_version = os.getenv("K_REVISION") |
121 | | - if faas_version is not None: |
122 | | - common_attributes["faas.version"] = str(faas_version) |
123 | | - |
124 | | - common_attributes.update( |
| 82 | +def _cloud_run_resource() -> Resource: |
| 83 | + return _make_resource( |
125 | 84 | { |
126 | | - "cloud.platform": "gcp_cloud_run", |
127 | | - "cloud.region": all_metadata["instance"]["region"].split("/")[-1], |
128 | | - "faas.instance": all_metadata["instance"]["id"], |
129 | | - "gcp.resource_type": "cloud_run", |
| 85 | + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_RUN, |
| 86 | + ResourceAttributes.FAAS_NAME: _faas.faas_name(), |
| 87 | + ResourceAttributes.FAAS_VERSION: _faas.faas_version(), |
| 88 | + ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), |
| 89 | + ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), |
130 | 90 | } |
131 | 91 | ) |
132 | | - return common_attributes |
133 | | - |
134 | 92 |
|
135 | | -def get_cloudfunctions_resources(): |
136 | | - """Resource finder for Cloud Functions attributes""" |
137 | 93 |
|
138 | | - if os.getenv("FUNCTION_TARGET") is None: |
139 | | - return {} |
| 94 | +def _cloud_functions_resource() -> Resource: |
| 95 | + return _make_resource( |
| 96 | + { |
| 97 | + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_FUNCTIONS, |
| 98 | + ResourceAttributes.FAAS_NAME: _faas.faas_name(), |
| 99 | + ResourceAttributes.FAAS_VERSION: _faas.faas_version(), |
| 100 | + ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), |
| 101 | + ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), |
| 102 | + } |
| 103 | + ) |
140 | 104 |
|
141 | | - ( |
142 | | - common_attributes, |
143 | | - all_metadata, |
144 | | - ) = _get_google_metadata_and_common_attributes() |
145 | 105 |
|
146 | | - faas_name = os.getenv("K_SERVICE") |
147 | | - if faas_name is not None: |
148 | | - common_attributes["faas.name"] = str(faas_name) |
| 106 | +def _gae_resource() -> Resource: |
| 107 | + if _gae.on_app_engine_standard(): |
| 108 | + zone = _gae.standard_availability_zone() |
| 109 | + region = _gae.standard_cloud_region() |
| 110 | + else: |
| 111 | + zone_and_region = _gae.flex_availability_zone_and_region() |
| 112 | + zone = zone_and_region.zone |
| 113 | + region = zone_and_region.region |
149 | 114 |
|
150 | | - faas_version = os.getenv("K_REVISION") |
151 | | - if faas_version is not None: |
152 | | - common_attributes["faas.version"] = str(faas_version) |
| 115 | + faas_name = _gae.service_name() |
| 116 | + faas_version = _gae.service_version() |
| 117 | + faas_instance = _gae.service_instance() |
153 | 118 |
|
154 | | - common_attributes.update( |
| 119 | + return _make_resource( |
155 | 120 | { |
156 | | - "cloud.platform": "gcp_cloud_functions", |
157 | | - "cloud.region": all_metadata["instance"]["region"].split("/")[-1], |
158 | | - "faas.instance": all_metadata["instance"]["id"], |
159 | | - "gcp.resource_type": "cloud_functions", |
| 121 | + ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_APP_ENGINE, |
| 122 | + ResourceAttributes.FAAS_NAME: faas_name, |
| 123 | + ResourceAttributes.FAAS_VERSION: faas_version, |
| 124 | + ResourceAttributes.FAAS_INSTANCE: faas_instance, |
| 125 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone, |
| 126 | + ResourceAttributes.CLOUD_REGION: region, |
160 | 127 | } |
161 | 128 | ) |
162 | | - return common_attributes |
163 | 129 |
|
164 | 130 |
|
165 | | -# Order here matters. Since a GKE_CONTAINER is a specialized type of GCE_INSTANCE |
166 | | -# We need to first check if it matches the criteria for being a GKE_CONTAINER |
167 | | -# before falling back and checking if its a GCE_INSTANCE. |
168 | | -# This list should be sorted from most specialized to least specialized. |
169 | | -_RESOURCE_FINDERS = [ |
170 | | - ("gke_container", get_gke_resources), |
171 | | - ("cloud_run", get_cloudrun_resources), |
172 | | - ("cloud_functions", get_cloudfunctions_resources), |
173 | | - ("gce_instance", get_gce_resources), |
174 | | -] |
175 | | - |
176 | | - |
177 | | -class NoGoogleResourcesFound(Exception): |
178 | | - pass |
| 131 | +def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource: |
| 132 | + return Resource( |
| 133 | + { |
| 134 | + ResourceAttributes.CLOUD_PROVIDER: "gcp", |
| 135 | + ResourceAttributes.CLOUD_ACCOUNT_ID: _metadata.get_metadata()[ |
| 136 | + "project" |
| 137 | + ]["projectId"], |
| 138 | + **attrs, |
| 139 | + } |
| 140 | + ) |
179 | 141 |
|
180 | 142 |
|
181 | | -class GoogleCloudResourceDetector(ResourceDetector): |
182 | | - def __init__(self, raise_on_error=False): |
183 | | - super().__init__(raise_on_error) |
184 | | - self.cached = False |
185 | | - self.gcp_resources = {} |
186 | | - |
187 | | - def detect(self) -> "Resource": |
188 | | - if not self.cached: |
189 | | - self.cached = True |
190 | | - for resource_type, resource_finder in _RESOURCE_FINDERS: |
191 | | - try: |
192 | | - found_resources = resource_finder() |
193 | | - # pylint: disable=broad-except |
194 | | - except Exception as ex: |
195 | | - logger.warning( |
196 | | - "Exception %s occured attempting %s resource detection", |
197 | | - ex, |
198 | | - resource_type, |
199 | | - ) |
200 | | - found_resources = None |
201 | | - if found_resources: |
202 | | - self.gcp_resources = found_resources |
203 | | - break |
204 | | - if self.raise_on_error and not self.gcp_resources: |
205 | | - raise NoGoogleResourcesFound() |
206 | | - return Resource(self.gcp_resources) |
| 143 | +__all__ = ["GoogleCloudResourceDetector"] |
0 commit comments