Skip to content

Commit 756f003

Browse files
author
Andrew Xue
authored
support GKE resource detection (#24)
add support for GKE resource detection
1 parent 1c7d458 commit 756f003

File tree

4 files changed

+285
-21
lines changed

4 files changed

+285
-21
lines changed

opentelemetry-exporter-cloud-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,19 @@
2323

2424
OT_RESOURCE_LABEL_TO_GCP = {
2525
"gce_instance": {
26+
"host.id": "instance_id",
2627
"cloud.account.id": "project_id",
28+
"cloud.zone": "zone",
29+
},
30+
"gke_container": {
31+
"k8s.cluster.name": "cluster_name",
32+
"k8s.namespace.name": "namespace_id",
33+
"k8s.pod.name": "pod_id",
2734
"host.id": "instance_id",
35+
"container.name": "container_name",
36+
"cloud.account.id": "project_id",
2837
"cloud.zone": "zone",
29-
}
38+
},
3039
}
3140

3241

opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,19 @@ def _strip_characters(ot_version):
303303

304304
OT_RESOURCE_LABEL_TO_GCP = {
305305
"gce_instance": {
306+
"host.id": "instance_id",
306307
"cloud.account.id": "project_id",
308+
"cloud.zone": "zone",
309+
},
310+
"gke_container": {
311+
"k8s.cluster.name": "cluster_name",
312+
"k8s.namespace.name": "namespace_id",
313+
"k8s.pod.name": "pod_id",
307314
"host.id": "instance_id",
315+
"container.name": "container_name",
316+
"cloud.account.id": "project_id",
308317
"cloud.zone": "zone",
309-
}
318+
},
310319
}
311320

312321

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import requests
24
from opentelemetry.context import attach, detach, set_value
35
from opentelemetry.sdk.resources import Resource, ResourceDetector
@@ -8,27 +10,82 @@
810
_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"}
911

1012

11-
def get_gce_resources():
12-
""" Resource finder for common GCE attributes
13-
14-
See: https://cloud.google.com/compute/docs/storing-retrieving-metadata
15-
"""
13+
def _get_google_metadata_and_common_attributes():
1614
token = attach(set_value("suppress_instrumentation", True))
1715
all_metadata = requests.get(
1816
_GCP_METADATA_URL, headers=_GCP_METADATA_URL_HEADER
1917
).json()
2018
detach(token)
21-
gce_resources = {
22-
"host.id": all_metadata["instance"]["id"],
19+
common_attributes = {
2320
"cloud.account.id": all_metadata["project"]["projectId"],
24-
"cloud.zone": all_metadata["instance"]["zone"].split("/")[-1],
2521
"cloud.provider": "gcp",
26-
"gcp.resource_type": "gce_instance",
22+
"cloud.zone": all_metadata["instance"]["zone"].split("/")[-1],
2723
}
28-
return gce_resources
24+
return common_attributes, all_metadata
25+
26+
27+
def get_gce_resources():
28+
""" Resource finder for common GCE attributes
29+
30+
See: https://cloud.google.com/compute/docs/storing-retrieving-metadata
31+
"""
32+
(
33+
common_attributes,
34+
all_metadata,
35+
) = _get_google_metadata_and_common_attributes()
36+
common_attributes.update(
37+
{
38+
"host.id": all_metadata["instance"]["id"],
39+
"gcp.resource_type": "gce_instance",
40+
}
41+
)
42+
return common_attributes
43+
44+
45+
def get_gke_resources():
46+
""" Resource finder for GKE attributes
47+
48+
"""
49+
# The user must specify the container name via the Downward API
50+
container_name = os.getenv("CONTAINER_NAME")
51+
if container_name is None:
52+
return {}
53+
(
54+
common_attributes,
55+
all_metadata,
56+
) = _get_google_metadata_and_common_attributes()
57+
58+
# Fallback to reading namespace from a file is the env var is not set
59+
pod_namespace = os.getenv("NAMESPACE")
60+
if pod_namespace is None:
61+
try:
62+
with open(
63+
"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r"
64+
) as namespace_file:
65+
pod_namespace = namespace_file.read().strip()
66+
except FileNotFoundError:
67+
pod_namespace = ""
68+
69+
common_attributes.update(
70+
{
71+
"k8s.cluster.name": all_metadata["instance"]["attributes"][
72+
"cluster-name"
73+
],
74+
"k8s.namespace.name": pod_namespace,
75+
"k8s.pod.name": os.getenv("POD_NAME", os.getenv("HOSTNAME", "")),
76+
"host.id": all_metadata["instance"]["id"],
77+
"container.name": container_name,
78+
"gcp.resource_type": "gke_container",
79+
}
80+
)
81+
return common_attributes
2982

3083

31-
_RESOURCE_FINDERS = [get_gce_resources]
84+
# Order here matters. Since a GKE_CONTAINER is a specialized type of GCE_INSTANCE
85+
# We need to first check if it matches the criteria for being a GKE_CONTAINER
86+
# before falling back and checking if its a GCE_INSTANCE.
87+
# This list should be sorted from most specialized to least specialized.
88+
_RESOURCE_FINDERS = [get_gke_resources, get_gce_resources]
3289

3390

3491
class GoogleCloudResourceDetector(ResourceDetector):
@@ -41,6 +98,12 @@ def detect(self) -> "Resource":
4198
if not self.cached:
4299
self.cached = True
43100
for resource_finder in _RESOURCE_FINDERS:
44-
found_resources = resource_finder()
45-
self.gcp_resources.update(found_resources)
101+
try:
102+
found_resources = resource_finder()
103+
# pylint: disable=broad-except
104+
except Exception:
105+
found_resources = None
106+
if found_resources:
107+
self.gcp_resources = found_resources
108+
break
46109
return Resource(self.gcp_resources)

opentelemetry-exporter-cloud-trace/tests/test_gcp_resource_detector.py

+189-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
1516
import unittest
1617
from unittest import mock
1718

@@ -20,18 +21,35 @@
2021
_GCP_METADATA_URL,
2122
GoogleCloudResourceDetector,
2223
get_gce_resources,
24+
get_gke_resources,
2325
)
2426

25-
RESOURCES_JSON_STRING = {
27+
NAMESPACE = "NAMESPACE"
28+
CONTAINER_NAME = "CONTAINER_NAME"
29+
HOSTNAME = "HOSTNAME"
30+
POD_NAME = "POD_NAME"
31+
32+
GCE_RESOURCES_JSON_STRING = {
2633
"instance": {"id": "instance_id", "zone": "projects/123/zones/zone"},
2734
"project": {"projectId": "project_id"},
2835
}
2936

37+
GKE_RESOURCES_JSON_STRING = {
38+
"instance": {
39+
"id": "instance_id",
40+
"zone": "projects/123/zones/zone",
41+
"attributes": {"cluster-name": "cluster_name"},
42+
},
43+
"project": {"projectId": "project_id"},
44+
}
45+
3046

47+
@mock.patch(
48+
"opentelemetry.tools.resource_detector.requests.get",
49+
**{"return_value.json.return_value": GCE_RESOURCES_JSON_STRING}
50+
)
3151
class TestGCEResourceFinder(unittest.TestCase):
32-
@mock.patch("opentelemetry.tools.resource_detector.requests.get")
3352
def test_finding_gce_resources(self, getter):
34-
getter.return_value.json.return_value = RESOURCES_JSON_STRING
3553
found_resources = get_gce_resources()
3654
self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL)
3755
self.assertEqual(
@@ -46,11 +64,123 @@ def test_finding_gce_resources(self, getter):
4664
)
4765

4866

67+
def pop_environ_key(key):
68+
if key in os.environ:
69+
os.environ.pop(key)
70+
71+
72+
def clear_gke_env_vars():
73+
pop_environ_key(CONTAINER_NAME)
74+
pop_environ_key(NAMESPACE)
75+
pop_environ_key(HOSTNAME)
76+
pop_environ_key(POD_NAME)
77+
78+
79+
@mock.patch(
80+
"opentelemetry.tools.resource_detector.requests.get",
81+
**{"return_value.json.return_value": GKE_RESOURCES_JSON_STRING}
82+
)
83+
class TestGKEResourceFinder(unittest.TestCase):
84+
def tearDown(self) -> None:
85+
clear_gke_env_vars()
86+
87+
# pylint: disable=unused-argument
88+
def test_missing_container_name(self, getter):
89+
pop_environ_key(CONTAINER_NAME)
90+
self.assertEqual(get_gke_resources(), {})
91+
92+
# pylint: disable=unused-argument
93+
def test_environment_empty_strings(self, getter):
94+
os.environ[CONTAINER_NAME] = ""
95+
os.environ[NAMESPACE] = ""
96+
found_resources = get_gke_resources()
97+
self.assertEqual(
98+
found_resources,
99+
{
100+
"cloud.account.id": "project_id",
101+
"k8s.cluster.name": "cluster_name",
102+
"k8s.namespace.name": "",
103+
"host.id": "instance_id",
104+
"k8s.pod.name": "",
105+
"container.name": "",
106+
"cloud.zone": "zone",
107+
"cloud.provider": "gcp",
108+
"gcp.resource_type": "gke_container",
109+
},
110+
)
111+
112+
def test_missing_namespace_file(self, getter):
113+
os.environ[CONTAINER_NAME] = "container_name"
114+
found_resources = get_gke_resources()
115+
self.assertEqual(
116+
found_resources,
117+
{
118+
"cloud.account.id": "project_id",
119+
"k8s.cluster.name": "cluster_name",
120+
"k8s.namespace.name": "",
121+
"host.id": "instance_id",
122+
"k8s.pod.name": "",
123+
"container.name": "container_name",
124+
"cloud.zone": "zone",
125+
"cloud.provider": "gcp",
126+
"gcp.resource_type": "gke_container",
127+
},
128+
)
129+
130+
def test_finding_gke_resources(self, getter):
131+
os.environ[NAMESPACE] = "namespace"
132+
os.environ[CONTAINER_NAME] = "container_name"
133+
os.environ[HOSTNAME] = "host_name"
134+
found_resources = get_gke_resources()
135+
self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL)
136+
self.assertEqual(
137+
found_resources,
138+
{
139+
"cloud.account.id": "project_id",
140+
"k8s.cluster.name": "cluster_name",
141+
"k8s.namespace.name": "namespace",
142+
"host.id": "instance_id",
143+
"k8s.pod.name": "host_name",
144+
"container.name": "container_name",
145+
"cloud.zone": "zone",
146+
"cloud.provider": "gcp",
147+
"gcp.resource_type": "gke_container",
148+
},
149+
)
150+
151+
def test_finding_gke_resources_with_pod_name(self, getter):
152+
os.environ[NAMESPACE] = "namespace"
153+
os.environ[CONTAINER_NAME] = "container_name"
154+
os.environ[HOSTNAME] = "host_name"
155+
os.environ[POD_NAME] = "pod_name"
156+
found_resources = get_gke_resources()
157+
self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL)
158+
self.assertEqual(
159+
found_resources,
160+
{
161+
"cloud.account.id": "project_id",
162+
"k8s.cluster.name": "cluster_name",
163+
"k8s.namespace.name": "namespace",
164+
"host.id": "instance_id",
165+
"k8s.pod.name": "pod_name",
166+
"container.name": "container_name",
167+
"cloud.zone": "zone",
168+
"cloud.provider": "gcp",
169+
"gcp.resource_type": "gke_container",
170+
},
171+
)
172+
173+
174+
@mock.patch("opentelemetry.tools.resource_detector.requests.get")
49175
class TestGoogleCloudResourceDetector(unittest.TestCase):
50-
@mock.patch("opentelemetry.tools.resource_detector.requests.get")
51-
def test_finding_resources(self, getter):
176+
def tearDown(self) -> None:
177+
clear_gke_env_vars()
178+
179+
def test_finding_gce_resources(self, getter):
180+
# The necessary env variables were not set for GKE resource detection
181+
# to succeed. We should be falling back to detecting GCE resources
52182
resource_finder = GoogleCloudResourceDetector()
53-
getter.return_value.json.return_value = RESOURCES_JSON_STRING
183+
getter.return_value.json.return_value = GCE_RESOURCES_JSON_STRING
54184
found_resources = resource_finder.detect()
55185
self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL)
56186
self.assertEqual(
@@ -82,3 +212,56 @@ def test_finding_resources(self, getter):
82212
}
83213
),
84214
)
215+
216+
def test_finding_gke_resources(self, getter):
217+
# The necessary env variables were set for GKE resource detection
218+
# to succeed. No GCE resource info should be extracted
219+
220+
os.environ[NAMESPACE] = "namespace"
221+
os.environ[CONTAINER_NAME] = "container_name"
222+
os.environ[HOSTNAME] = "host_name"
223+
224+
resource_finder = GoogleCloudResourceDetector()
225+
getter.return_value.json.return_value = GKE_RESOURCES_JSON_STRING
226+
found_resources = resource_finder.detect()
227+
self.assertEqual(getter.call_args_list[0][0][0], _GCP_METADATA_URL)
228+
self.assertEqual(
229+
found_resources,
230+
Resource(
231+
labels={
232+
"cloud.account.id": "project_id",
233+
"k8s.cluster.name": "cluster_name",
234+
"k8s.namespace.name": "namespace",
235+
"host.id": "instance_id",
236+
"k8s.pod.name": "host_name",
237+
"container.name": "container_name",
238+
"cloud.zone": "zone",
239+
"cloud.provider": "gcp",
240+
"gcp.resource_type": "gke_container",
241+
}
242+
),
243+
)
244+
self.assertEqual(getter.call_count, 1)
245+
246+
def test_resource_finding_fallback(self, getter):
247+
# The environment variables imply its on GKE, but the metadata doesn't
248+
# have GKE information
249+
getter.return_value.json.return_value = GCE_RESOURCES_JSON_STRING
250+
os.environ[CONTAINER_NAME] = "container_name"
251+
252+
# This detection will cause an error in get_gke_resources and should
253+
# swallow the error and fall back to get_gce_resources
254+
resource_finder = GoogleCloudResourceDetector()
255+
found_resources = resource_finder.detect()
256+
self.assertEqual(
257+
found_resources,
258+
Resource(
259+
labels={
260+
"host.id": "instance_id",
261+
"cloud.provider": "gcp",
262+
"cloud.account.id": "project_id",
263+
"cloud.zone": "zone",
264+
"gcp.resource_type": "gce_instance",
265+
}
266+
),
267+
)

0 commit comments

Comments
 (0)