Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit 913d588

Browse files
authored
Merge custom resources on detection (#515)
1 parent 26fd836 commit 913d588

File tree

4 files changed

+153
-49
lines changed

4 files changed

+153
-49
lines changed

opencensus/common/monitored_resource/monitored_resource.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,25 @@ def get_instance():
4343
:rtype: :class:`opencensus.common.resource.Resource` or None
4444
:return: A `Resource` configured for the current environment.
4545
"""
46+
resources = []
47+
env_resource = resource.get_from_env()
48+
if env_resource is not None:
49+
resources.append(env_resource)
50+
4651
if k8s_utils.is_k8s_environment():
47-
return resource.Resource(_K8S_CONTAINER, k8s_utils.get_k8s_metadata())
52+
resources.append(resource.Resource(
53+
_K8S_CONTAINER, k8s_utils.get_k8s_metadata()))
54+
4855
if is_gce_environment():
49-
return resource.Resource(
56+
resources.append(resource.Resource(
5057
_GCE_INSTANCE,
51-
gcp_metadata_config.GcpMetadataConfig().get_gce_metadata())
52-
if is_aws_environment():
53-
return resource.Resource(
58+
gcp_metadata_config.GcpMetadataConfig().get_gce_metadata()))
59+
elif is_aws_environment():
60+
resources.append(resource.Resource(
5461
_AWS_EC2_INSTANCE,
5562
(aws_identity_doc_utils.AwsIdentityDocumentUtils()
56-
.get_aws_metadata()))
63+
.get_aws_metadata())))
5764

58-
return None
65+
if not resources:
66+
return None
67+
return resource.merge_resources(resources)

opencensus/common/resource/__init__.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,31 @@
4949
_UNQUOTE_RE = re.compile(r'^([\'"]?)([^\1]*)(\1)$')
5050

5151

52-
def merge_resources(r1, r2):
53-
"""Merge two resources to get a new resource.
52+
def merge_resources(resource_list):
53+
"""Merge multiple resources to get a new resource.
5454
55-
:type r1: :class:`Resource`
56-
:param r1: The first resource to merge, takes priority in conflicts.
55+
Resources earlier in the list take precedence: if multiple resources share
56+
a label key, use the value from the first resource in the list with that
57+
key. The combined resource's type will be the first non-null type in the
58+
list.
5759
58-
:type r2: :class:`Resource`
59-
:param r2: The second resource to merge.
60+
:type resource_list: list(:class:`Resource`)
61+
:param resource_list: The list of resources to combine.
6062
6163
:rtype: :class:`Resource`
6264
:return: The new combined resource.
6365
"""
64-
type_ = r1.type or r2.type
65-
labels = copy(r2.labels)
66-
labels.update(r1.labels)
67-
return Resource(type_, labels)
66+
if not resource_list:
67+
raise ValueError
68+
rtype = None
69+
for rr in resource_list:
70+
if rr.type:
71+
rtype = rr.type
72+
break
73+
labels = {}
74+
for rr in reversed(resource_list):
75+
labels.update(rr.labels)
76+
return Resource(rtype, labels)
6877

6978

7079
def check_ascii_256(string):
@@ -150,7 +159,7 @@ def merge(self, other):
150159
:rtype: :class:`Resource`
151160
:return: The new combined resource.
152161
"""
153-
return merge_resources(self, other)
162+
return merge_resources([self, other])
154163

155164

156165
def unquote(string):

tests/unit/common/monitored_resource_util/test_monitored_resource.py

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@
1313
# limitations under the License.
1414

1515
from contextlib import contextmanager
16-
import mock
1716
import os
18-
import unittest
17+
import sys
1918

2019
from opencensus.common.monitored_resource import monitored_resource
2120

21+
if sys.version_info < (3,):
22+
import unittest2 as unittest
23+
import mock
24+
else:
25+
import unittest
26+
from unittest import mock
27+
2228

2329
@contextmanager
2430
def mock_mr_method(method, use):
@@ -39,50 +45,71 @@ def mock_use_aws(use):
3945
return mock_mr_method('is_aws_environment', use)
4046

4147

48+
def mock_oc_env():
49+
return mock.patch.dict('os.environ', {
50+
'OC_RESOURCE_TYPE': 'mock_resource_type',
51+
'OC_RESOURCE_LABELS': 'mock_label_key=mock_label_value'
52+
})
53+
54+
4255
@contextmanager
4356
def mock_k8s_env():
4457
with mock_use_k8s(True):
45-
with mock_use_gce(False):
46-
with mock_use_aws(False):
47-
yield
58+
yield
4859

4960

5061
@contextmanager
5162
def mock_gce_env():
52-
with mock_use_k8s(False):
53-
with mock_use_gce(True):
54-
with mock_use_aws(False):
55-
yield
63+
with mock_use_gce(True):
64+
with mock_use_aws(False):
65+
yield
5666

5767

5868
@contextmanager
5969
def mock_aws_env():
60-
with mock_use_k8s(False):
61-
with mock_use_gce(False):
62-
with mock_use_aws(True):
63-
yield
70+
with mock_use_gce(False):
71+
with mock_use_aws(True):
72+
yield
6473

6574

6675
class TestMonitoredResource(unittest.TestCase):
6776

77+
def setUp(self):
78+
self.env_mock = mock.patch.dict(os.environ, clear=True)
79+
self.env_mock.start()
80+
81+
def tearDown(self):
82+
self.env_mock.stop()
83+
6884
@mock.patch('opencensus.common.monitored_resource.monitored_resource'
6985
'.gcp_metadata_config.GcpMetadataConfig')
70-
def test_gcp_gce_monitored_resource(self, gcp_metadata_mock):
86+
def test_gcp_gce_monitored_resource(self, gcp_md_mock):
7187
mocked_labels = {
7288
'instance_id': 'my-instance',
7389
'project_id': 'my-project',
7490
'zone': 'us-east1'
7591
}
7692

77-
gcp_metadata_mock.return_value = mock.Mock()
78-
gcp_metadata_mock.return_value.get_gce_metadata.return_value =\
79-
mocked_labels
93+
gcp_md_mock.return_value = mock.Mock()
94+
gcp_md_mock.return_value.get_gce_metadata.return_value = mocked_labels
95+
8096
with mock_gce_env():
8197
resource = monitored_resource.get_instance()
8298
self.assertEqual(resource.get_type(), 'gce_instance')
83-
self.assertDictEqual(resource.get_labels(), mocked_labels)
99+
self.assertEqual(resource.get_labels(), mocked_labels)
100+
101+
with mock_oc_env():
102+
with mock_gce_env():
103+
resource = monitored_resource.get_instance()
104+
self.assertEqual(resource.get_type(), 'mock_resource_type')
105+
self.assertDictContainsSubset(
106+
{'mock_label_key': 'mock_label_value'}, resource.get_labels())
107+
self.assertDictContainsSubset(mocked_labels, resource.get_labels())
108+
109+
@mock.patch('opencensus.common.monitored_resource.monitored_resource'
110+
'.gcp_metadata_config.GcpMetadataConfig')
111+
def test_gcp_k8s_monitored_resource(self, gcp_md_mock):
84112

85-
def test_gcp_k8s_monitored_resource(self):
86113
mocked_labels = {
87114
'instance_id': 'my-instance',
88115
'cluster_name': 'cluster',
@@ -92,31 +119,53 @@ def test_gcp_k8s_monitored_resource(self):
92119
'namespace_id': 'namespace',
93120
'container_name': 'container'
94121
}
122+
cluster_name_key = 'instance/attributes/cluster-name'
123+
cluster_name_val = 'cluster'
124+
gcp_md_mock.return_value = mock.Mock()
125+
gcp_md_mock.return_value.get_gce_metadata.return_value = mocked_labels
126+
gcp_md_mock.get_attribute.return_value = cluster_name_val
127+
128+
with mock_k8s_env():
129+
r1 = monitored_resource.get_instance()
130+
131+
gcp_md_mock.get_attribute.assert_called_once_with(cluster_name_key)
132+
self.assertEqual(r1.get_type(), 'k8s_container')
133+
self.assertDictContainsSubset(mocked_labels, r1.get_labels())
95134

96-
with mock_mr_method('k8s_utils.get_k8s_metadata', mocked_labels):
135+
with mock_oc_env():
97136
with mock_k8s_env():
98-
resource = monitored_resource.get_instance()
99-
self.assertEqual(resource.get_type(), 'k8s_container')
100-
self.assertDictEqual(resource.get_labels(), mocked_labels)
137+
r2 = monitored_resource.get_instance()
138+
139+
self.assertEqual(r1.get_type(), 'k8s_container')
140+
self.assertDictContainsSubset(mocked_labels, r1.get_labels())
141+
self.assertDictContainsSubset(
142+
{'mock_label_key': 'mock_label_value'}, r2.get_labels())
101143

102144
@mock.patch('opencensus.common.monitored_resource.monitored_resource'
103145
'.aws_identity_doc_utils.AwsIdentityDocumentUtils')
104-
def test_aws_monitored_resource(self, aws_metadata_mock):
146+
def test_aws_monitored_resource(self, aws_md_mock):
105147

106148
mocked_labels = {
107149
'instance_id': 'i-1234567890abcdef0',
108150
'aws_account': '123456789012',
109151
'region': 'us-west-2'
110152
}
111153

112-
aws_metadata_mock.return_value = mock.Mock()
113-
aws_metadata_mock.return_value.get_aws_metadata.return_value =\
114-
mocked_labels
154+
aws_md_mock.return_value = mock.Mock()
155+
aws_md_mock.return_value.get_aws_metadata.return_value = mocked_labels
115156

116157
with mock_aws_env():
117158
resource = monitored_resource.get_instance()
118159
self.assertEqual(resource.get_type(), 'aws_ec2_instance')
119-
self.assertDictEqual(resource.get_labels(), mocked_labels)
160+
self.assertEqual(resource.get_labels(), mocked_labels)
161+
162+
with mock_oc_env():
163+
with mock_aws_env():
164+
resource = monitored_resource.get_instance()
165+
self.assertEqual(resource.get_type(), 'mock_resource_type')
166+
self.assertDictContainsSubset(
167+
{'mock_label_key': 'mock_label_value'}, resource.get_labels())
168+
self.assertDictContainsSubset(mocked_labels, resource.get_labels())
120169

121170
def test_k8s_environment(self):
122171
patch = mock.patch.dict(os.environ,
@@ -147,7 +196,7 @@ def test_gce_environment(self):
147196
'aws_identity_doc_utils.AwsIdentityDocumentUtils.'
148197
'is_running_on_aws',
149198
return_value=True)
150-
def test_aws_environment(self, aws_util_mock, gcp_metadata_mock):
199+
def test_aws_environment(self, aws_util_mock, gcp_md_mock):
151200
mr = monitored_resource.get_instance()
152201

153202
self.assertIsNotNone(mr)
@@ -160,7 +209,13 @@ def test_aws_environment(self, aws_util_mock, gcp_metadata_mock):
160209
'aws_identity_doc_utils.AwsIdentityDocumentUtils.'
161210
'is_running_on_aws',
162211
return_value=False)
163-
def test_non_supported_environment(self, aws_util_mock, gcp_metadata_mock):
212+
def test_non_supported_environment(self, aws_util_mock, gcp_md_mock):
164213
mr = monitored_resource.get_instance()
165-
166214
self.assertIsNone(mr)
215+
216+
with mock_oc_env():
217+
mr = monitored_resource.get_instance()
218+
self.assertIsNotNone(mr)
219+
self.assertEqual(mr.get_type(), 'mock_resource_type')
220+
self.assertDictEqual(
221+
mr.get_labels(), {'mock_label_key': 'mock_label_value'})

tests/unit/common/test_resource.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
except ImportError:
2020
from unittest import mock
2121

22+
import os
2223
import unittest
2324

2425
from opencensus.common import resource as resource_module
@@ -123,6 +124,28 @@ def test_merge_overwrite(self):
123124

124125
class TestResourceModule(unittest.TestCase):
125126

127+
def test_merge_resource(self):
128+
with self.assertRaises(ValueError):
129+
resource_module.merge_resources(None)
130+
with self.assertRaises(ValueError):
131+
resource_module.merge_resources([])
132+
133+
r1 = Resource(None, {'lk1': 'lv11'})
134+
r2 = Resource('t2', {'lk1': 'lv12', 'lk2': 'lv22'})
135+
r3 = Resource('t3', {'lk2': 'lv23', 'lk3': 'lv33'})
136+
137+
merged = resource_module.merge_resources([r1, r2, r3])
138+
self.assertEqual(merged.type, 't2')
139+
self.assertDictEqual(
140+
merged.labels, {'lk1': 'lv11', 'lk2': 'lv22', 'lk3': 'lv33'})
141+
142+
def test_merge_resource_no_type(self):
143+
r1 = Resource(None)
144+
r2 = Resource(None)
145+
146+
merged = resource_module.merge_resources([r1, r2])
147+
self.assertEqual(merged.type, None)
148+
126149
def test_check_ascii_256(self):
127150
self.assertIsNone(resource_module.check_ascii_256(None))
128151

@@ -156,12 +179,20 @@ def test_get_from_env_no_type(self):
156179
with mock.patch.dict('os.environ', {
157180
'OC_RESOURCE_LABELS': 'k1=v1,k2=v2'
158181
}):
182+
try:
183+
del os.environ['OC_RESOURCE_TYPE']
184+
except KeyError:
185+
pass
159186
self.assertIsNone(resource_module.get_from_env())
160187

161188
def test_get_from_env_no_labels(self):
162189
with mock.patch.dict('os.environ', {
163190
'OC_RESOURCE_TYPE': 'opencensus.io/example',
164191
}):
192+
try:
193+
del os.environ['OC_RESOURCE_LABELS']
194+
except KeyError:
195+
pass
165196
resource = resource_module.get_from_env()
166197
self.assertEqual(resource.type, 'opencensus.io/example')
167198
self.assertDictEqual(resource.labels, {})

0 commit comments

Comments
 (0)