Skip to content

Commit 85b4620

Browse files
authored
Add support for custom resources (census-instrumentation#513)
1 parent ed82f64 commit 85b4620

File tree

2 files changed

+166
-5
lines changed

2 files changed

+166
-5
lines changed

opencensus/common/resource.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,40 @@
1414

1515

1616
from copy import copy
17+
import logging
18+
import os
1719
import re
1820

1921

22+
logger = logging.getLogger(__name__)
23+
24+
25+
OC_RESOURCE_TYPE = 'OC_RESOURCE_TYPE'
26+
OC_RESOURCE_LABELS = 'OC_RESOURCE_LABELS'
27+
2028
# Matches anything outside ASCII 32-126 inclusive
21-
NON_PRINTABLE_ASCII = re.compile(
29+
_NON_PRINTABLE_ASCII = re.compile(
2230
r'[^ !"#$%&\'()*+,\-./:;<=>?@\[\\\]^_`{|}~0-9a-zA-Z]')
2331

32+
# Label key/value tokens, may be quoted
33+
_WORD_RES = r'(\'[^\']*\'|"[^"]*"|[^\s,=]+)'
34+
35+
_KV_RE = re.compile(r"""
36+
\s* # ignore leading spaces
37+
(?P<key>{word_re}) # capture the key word
38+
\s*=\s*
39+
(?P<val>{word_re}) # capture the value word
40+
\s* # ignore trailing spaces
41+
""".format(word_re=_WORD_RES), re.VERBOSE)
42+
43+
_LABELS_RE = re.compile(r"""
44+
^\s*{word_re}\s*=\s*{word_re}\s* # _KV_RE without the named groups
45+
(,\s*{word_re}\s*=\s*{word_re}\s*)* # more KV pairs, comma delimited
46+
$
47+
""".format(word_re=_WORD_RES), re.VERBOSE)
48+
49+
_UNQUOTE_RE = re.compile(r'^([\'"]?)([^\1]*)(\1)$')
50+
2451

2552
def merge_resources(r1, r2):
2653
"""Merge two resources to get a new resource.
@@ -53,7 +80,7 @@ def check_ascii_256(string):
5380
return
5481
if len(string) > 256:
5582
raise ValueError("Value is longer than 256 characters")
56-
bad_char = NON_PRINTABLE_ASCII.search(string)
83+
bad_char = _NON_PRINTABLE_ASCII.search(string)
5784
if bad_char:
5885
raise ValueError(u'Character "{}" at position {} is not printable '
5986
'ASCII'
@@ -124,3 +151,60 @@ def merge(self, other):
124151
:return: The new combined resource.
125152
"""
126153
return merge_resources(self, other)
154+
155+
156+
def unquote(string):
157+
"""Strip quotes surrounding `string` if they exist.
158+
159+
>>> unquote('abc')
160+
'abc'
161+
>>> unquote('"abc"')
162+
'abc'
163+
>>> unquote("'abc'")
164+
'abc'
165+
>>> unquote('"a\\'b\\'c"')
166+
"a'b'c"
167+
"""
168+
return _UNQUOTE_RE.sub(r'\2', string)
169+
170+
171+
def parse_labels(labels_str):
172+
"""Parse label keys and values following the Resource spec.
173+
174+
>>> parse_labels("k=v")
175+
{'k': 'v'}
176+
>>> parse_labels("k1=v1, k2=v2")
177+
{'k1': 'v1', 'k2': 'v2'}
178+
>>> parse_labels("k1='v1,=z1'")
179+
{'k1': 'v1,=z1'}
180+
"""
181+
if not _LABELS_RE.match(labels_str):
182+
return None
183+
labels = {}
184+
for kv in _KV_RE.finditer(labels_str):
185+
gd = kv.groupdict()
186+
key = unquote(gd['key'])
187+
if key in labels:
188+
logger.warning('Duplicate label key "%s"', key)
189+
labels[key] = unquote(gd['val'])
190+
return labels
191+
192+
193+
def get_from_env():
194+
"""Get a Resource from environment variables.
195+
196+
:rtype: :class:`Resource`
197+
:return: A resource with type and labels from the environment.
198+
"""
199+
type_env = os.getenv(OC_RESOURCE_TYPE)
200+
if type_env is None:
201+
return None
202+
type_env = type_env.strip()
203+
204+
labels_env = os.getenv(OC_RESOURCE_LABELS)
205+
if labels_env is None:
206+
return Resource(type_env)
207+
208+
labels = parse_labels(labels_env)
209+
210+
return Resource(type_env, labels)

tests/unit/common/test_resource.py

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
# limitations under the License.
1616

1717
try:
18-
from mock import Mock
18+
import mock
1919
except ImportError:
20-
from unittest.mock import Mock
20+
from unittest import mock
2121

2222
import unittest
2323

@@ -86,7 +86,7 @@ def test_get_labels(self):
8686
self.assertEqual(resource.get_labels(), resource_labels)
8787
self.assertIsNot(resource.get_labels(), resource_labels)
8888
got_labels = resource.get_labels()
89-
got_labels[label_key] = Mock()
89+
got_labels[label_key] = mock.Mock()
9090
self.assertNotEqual(resource.get_labels(), got_labels)
9191
self.assertEqual(resource.get_labels()[label_key], label_value)
9292

@@ -142,3 +142,80 @@ def test_check_ascii_256(self):
142142
resource_module.check_ascii_256('abc' + chr(31))
143143
with self.assertRaises(ValueError):
144144
resource_module.check_ascii_256(u'abc' + chr(31))
145+
146+
def test_get_from_env(self):
147+
with mock.patch.dict('os.environ', {
148+
'OC_RESOURCE_TYPE': 'opencensus.io/example',
149+
'OC_RESOURCE_LABELS': 'k1=v1,k2=v2'
150+
}):
151+
resource = resource_module.get_from_env()
152+
self.assertEqual(resource.type, 'opencensus.io/example')
153+
self.assertDictEqual(resource.labels, {'k1': 'v1', 'k2': 'v2'})
154+
155+
def test_get_from_env_no_type(self):
156+
with mock.patch.dict('os.environ', {
157+
'OC_RESOURCE_LABELS': 'k1=v1,k2=v2'
158+
}):
159+
self.assertIsNone(resource_module.get_from_env())
160+
161+
def test_get_from_env_no_labels(self):
162+
with mock.patch.dict('os.environ', {
163+
'OC_RESOURCE_TYPE': 'opencensus.io/example',
164+
}):
165+
resource = resource_module.get_from_env()
166+
self.assertEqual(resource.type, 'opencensus.io/example')
167+
self.assertDictEqual(resource.labels, {})
168+
169+
def test_get_from_env_outer_spaces(self):
170+
with mock.patch.dict('os.environ', {
171+
'OC_RESOURCE_TYPE': ' opencensus.io/example ',
172+
'OC_RESOURCE_LABELS': 'k1= v1 , k2=v2 '
173+
}):
174+
resource = resource_module.get_from_env()
175+
self.assertEqual(resource.type, 'opencensus.io/example')
176+
self.assertDictEqual(resource.labels, {'k1': 'v1', 'k2': 'v2'})
177+
178+
def test_get_from_env_inner_spaces(self):
179+
# Spaces inside key/label values should be contained by quotes, refuse
180+
# to parse this.
181+
with mock.patch.dict('os.environ', {
182+
'OC_RESOURCE_TYPE': 'opencensus.io / example',
183+
'OC_RESOURCE_LABELS': 'key one=value one, key two= value two'
184+
}):
185+
resource = resource_module.get_from_env()
186+
self.assertEqual(resource.type, 'opencensus.io / example')
187+
self.assertDictEqual(resource.labels, {})
188+
189+
def test_get_from_env_quoted_chars(self):
190+
with mock.patch.dict('os.environ', {
191+
'OC_RESOURCE_TYPE': 'opencensus.io/example',
192+
'OC_RESOURCE_LABELS': '"k1=\'"="v1,,,", "k2"=\'="=??\''
193+
}):
194+
resource = resource_module.get_from_env()
195+
self.assertDictEqual(resource.labels, {"k1='": 'v1,,,', 'k2': '="=??'})
196+
197+
def test_parse_labels(self):
198+
self.assertEqual(resource_module.parse_labels("k1=v1"), {'k1': 'v1'})
199+
self.assertEqual(
200+
resource_module.parse_labels("k1=v1,k2=v2"),
201+
{'k1': 'v1', 'k2': 'v2'})
202+
203+
self.assertEqual(
204+
resource_module.parse_labels('k1="v1"'),
205+
{'k1': 'v1'})
206+
207+
self.assertEqual(
208+
resource_module.parse_labels('k1="v1==,"'),
209+
{'k1': 'v1==,'})
210+
211+
self.assertEqual(
212+
resource_module.parse_labels('k1="one/two/three"'),
213+
{'k1': 'one/two/three'})
214+
215+
self.assertEqual(
216+
resource_module.parse_labels('k1="one\\two\\three"'),
217+
{'k1': 'one\\two\\three'})
218+
219+
with mock.patch('opencensus.common.resource.logger') as mock_logger:
220+
resource_module.parse_labels('k1=v1, k2=v2, k1=v3')
221+
mock_logger.warning.assert_called_once()

0 commit comments

Comments
 (0)