diff --git a/opencensus/common/monitored_resource_util/monitored_resource.py b/opencensus/common/monitored_resource_util/monitored_resource.py index ecb0b160f..befcf60f0 100644 --- a/opencensus/common/monitored_resource_util/monitored_resource.py +++ b/opencensus/common/monitored_resource_util/monitored_resource.py @@ -12,75 +12,70 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os -from opencensus.common.monitored_resource_util.gcp_metadata_config \ - import GcpMetadataConfig -from opencensus.common.monitored_resource_util.aws_identity_doc_utils \ - import AwsIdentityDocumentUtils +from opencensus.common import resource +from opencensus.common.monitored_resource_util import aws_identity_doc_utils +from opencensus.common.monitored_resource_util import gcp_metadata_config -# supported environments (resource types) + +# Supported environments (resource types) _GCE_INSTANCE = "gce_instance" _GKE_CONTAINER = "gke_container" _AWS_EC2_INSTANCE = "aws_ec2_instance" +# Kubenertes environment variables +_KUBERNETES_SERVICE_HOST = 'KUBERNETES_SERVICE_HOST' -class MonitoredResource(object): - """MonitoredResource returns the resource type and resource labels. - """ - @property - def resource_type(self): - """Returns the resource type this MonitoredResource. - :return: - """ - raise NotImplementedError # pragma: NO COVER +def is_gke_environment(): + """Whether the environment is a GKE container instance. - def get_resource_labels(self): - """Returns the resource labels for this MonitoredResource. - :return: - """ - raise NotImplementedError # pragma: NO COVER + The KUBERNETES_SERVICE_HOST environment variable must be set. + """ + return _KUBERNETES_SERVICE_HOST in os.environ -class GcpGceMonitoredResource(MonitoredResource): - """GceMonitoredResource represents gce_instance type monitored resource. - For definition refer to - https://cloud.google.com/monitoring/api/resources#tag_gce_instance - """ +def is_gce_environment(): + """Whether the environment is a virtual machine on GCE.""" + return gcp_metadata_config.GcpMetadataConfig.is_running_on_gcp() - @property - def resource_type(self): - return _GCE_INSTANCE - def get_resource_labels(self): - gcp_config = GcpMetadataConfig() - return gcp_config.get_gce_metadata() +def is_aws_environment(): + """Whether the environment is a virtual machine instance on EC2.""" + return aws_identity_doc_utils.AwsIdentityDocumentUtils.is_running_on_aws() -class GcpGkeMonitoredResource(MonitoredResource): - """GkeMonitoredResource represents gke_container type monitored resource. - For definition refer to - https://cloud.google.com/monitoring/api/resources#tag_gke_container - """ +def get_instance(): + """Get a resource based on the application environment. - @property - def resource_type(self): - return _GKE_CONTAINER + Returns a `Resource` configured for the current environment, or None if the + environment is unknown or unsupported. - def get_resource_labels(self): - gcp_config = GcpMetadataConfig() - return gcp_config.get_gke_metadata() + Supported environments include: + 1. 'gke_container' + - https://cloud.google.com/monitoring/api/resources#tag_gke_container + 2. 'gce_instance' + - https://cloud.google.com/monitoring/api/resources#tag_gce_instance + 3. 'aws_ec2_instance' + - https://cloud.google.com/monitoring/api/resources#tag_aws_ec2_instance -class AwsMonitoredResource(MonitoredResource): - """AwsMonitoredResource represents aws_ec2_instance type monitored resource. - For definition refer to - https://cloud.google.com/monitoring/api/resources#tag_aws_ec2_instance + :rtype: :class:`opencensus.common.resource.Resource` or None + :return: A `Resource` configured for the current environment. """ - @property - def resource_type(self): - return _AWS_EC2_INSTANCE - - def get_resource_labels(self): - aws_util = AwsIdentityDocumentUtils() - return aws_util.get_aws_metadata() + if is_gke_environment(): + return resource.Resource( + _GKE_CONTAINER, + gcp_metadata_config.GcpMetadataConfig().get_gke_metadata()) + if is_gce_environment(): + return resource.Resource( + _GCE_INSTANCE, + gcp_metadata_config.GcpMetadataConfig().get_gce_metadata()) + if is_aws_environment(): + return resource.Resource( + _AWS_EC2_INSTANCE, + (aws_identity_doc_utils.AwsIdentityDocumentUtils() + .get_aws_metadata())) + + return None diff --git a/opencensus/common/monitored_resource_util/monitored_resource_util.py b/opencensus/common/monitored_resource_util/monitored_resource_util.py deleted file mode 100644 index 28360437f..000000000 --- a/opencensus/common/monitored_resource_util/monitored_resource_util.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2018 Google Inc. -# -# 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 os -from opencensus.common.monitored_resource_util import monitored_resource -from opencensus.common.monitored_resource_util.aws_identity_doc_utils \ - import AwsIdentityDocumentUtils - -from opencensus.common.monitored_resource_util.gcp_metadata_config \ - import GcpMetadataConfig - -# Kubenertes environment variables -_KUBERNETES_SERVICE_HOST = 'KUBERNETES_SERVICE_HOST' - - -class MonitoredResourceUtil(object): - """Utilities for auto detecting monitored resource based on the - environment where the application is running. - """ - - @staticmethod - def get_instance(): - """ - Returns a self-configured monitored resource, or None if the - application is not running on a supported environment. - It supports following environments (resource types) - 1. gke_container: - 2. gce_instance: - 3. aws_ec2_instance: - :return: MonitoredResource or None - """ - if is_gke_environment(): - return monitored_resource.GcpGkeMonitoredResource() - elif is_gce_environment(): - return monitored_resource.GcpGceMonitoredResource() - elif is_aws_environment(): - return monitored_resource.AwsMonitoredResource() - - return None - - -def is_gke_environment(): - """A Google Container Engine (GKE) container instance. - KUBERNETES_SERVICE_HOST environment variable must be set. - """ - return _KUBERNETES_SERVICE_HOST in os.environ - - -def is_gce_environment(): - """A virtual machine instance hosted in Google Compute Engine (GCE).""" - return GcpMetadataConfig.is_running_on_gcp() - - -def is_aws_environment(): - """A virtual machine instance in Amazon EC2""" - return AwsIdentityDocumentUtils.is_running_on_aws() diff --git a/opencensus/common/resource.py b/opencensus/common/resource.py new file mode 100644 index 000000000..6b7a32e84 --- /dev/null +++ b/opencensus/common/resource.py @@ -0,0 +1,126 @@ +# Copyright 2019 Google Inc. +# +# 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. + + +from copy import copy +import re + + +# Matches anything outside ASCII 32-126 inclusive +NON_PRINTABLE_ASCII = re.compile( + r'[^ !"#$%&\'()*+,\-./:;<=>?@\[\\\]^_`{|}~0-9a-zA-Z]') + + +def merge_resources(r1, r2): + """Merge two resources to get a new resource. + + :type r1: :class:`Resource` + :param r1: The first resource to merge, takes priority in conflicts. + + :type r2: :class:`Resource` + :param r2: The second resource to merge. + + :rtype: :class:`Resource` + :return: The new combined resource. + """ + type_ = r1.type or r2.type + labels = copy(r2.labels) + labels.update(r1.labels) + return Resource(type_, labels) + + +def check_ascii_256(string): + """Check that `string` is printable ASCII and at most 256 chars. + + Raise a `ValueError` if this check fails. Note that `string` itself doesn't + have to be ASCII-encoded. + + :type string: str + :param string: The string to check. + """ + if string is None: + return + if len(string) > 256: + raise ValueError("Value is longer than 256 characters") + bad_char = NON_PRINTABLE_ASCII.search(string) + if bad_char: + raise ValueError(u'Character "{}" at position {} is not printable ' + 'ASCII' + .format( + string[bad_char.start():bad_char.end()], + bad_char.start())) + + +class Resource(object): + """A description of the entity for which signals are reported. + + `type_` and `labels`' keys and values should contain only printable ASCII + and should be at most 256 characters. + + See: + https://github.com/census-instrumentation/opencensus-specs/blob/master/resource/Resource.md + + :type type_: str + :param type_: The resource type identifier. + + :type labels: dict + :param labels: Key-value pairs that describe the entity. + """ # noqa + + def __init__(self, type_=None, labels=None): + if type_ is not None and not type_: + raise ValueError("Resource type must not be empty") + check_ascii_256(type_) + if labels is None: + labels = {} + for key, value in labels.items(): + if not key: + raise ValueError("Resource key must not be null or empty") + if value is None: + raise ValueError("Resource value must not be null") + check_ascii_256(key) + check_ascii_256(value) + + self.type = type_ + self.labels = copy(labels) + + def get_type(self): + """Get this resource's type. + + :rtype: str + :return: The resource's type. + """ + return self.type + + def get_labels(self): + """Get this resource's labels. + + :rtype: dict + :return: The resource's label dict. + """ + return copy(self.labels) + + def merge(self, other): + """Get a copy of this resource combined with another resource. + + The combined resource will have the union of both resources' labels, + keeping this resource's label values if they conflict. + + :type other: :class:`Resource` + :param other: The other resource to merge. + + :rtype: :class:`Resource` + :return: The new combined resource. + """ + return merge_resources(self, other) diff --git a/opencensus/stats/exporters/stackdriver_exporter.py b/opencensus/stats/exporters/stackdriver_exporter.py index f99995693..1da15938b 100644 --- a/opencensus/stats/exporters/stackdriver_exporter.py +++ b/opencensus/stats/exporters/stackdriver_exporter.py @@ -24,8 +24,7 @@ from google.cloud import monitoring_v3 from opencensus.common import utils -from opencensus.common.monitored_resource_util.monitored_resource_util \ - import MonitoredResourceUtil +from opencensus.common.monitored_resource_util import monitored_resource from opencensus.common.transports import async_ from opencensus.common.version import __version__ from opencensus.stats import aggregation @@ -345,11 +344,11 @@ def set_monitored_resource(series, option_resource_type): resource_type = GLOBAL_RESOURCE_TYPE if option_resource_type == "": - monitored_resource = MonitoredResourceUtil.get_instance() - if monitored_resource is not None: - resource_labels = monitored_resource.get_resource_labels() + resource = monitored_resource.get_instance() + if resource is not None: + resource_labels = resource.get_labels() - if monitored_resource.resource_type == 'gke_container': + if resource.get_type() == 'gke_container': resource_type = 'k8s_container' set_attribute_label(series, resource_labels, 'project_id') set_attribute_label(series, resource_labels, 'cluster_name') @@ -361,14 +360,14 @@ def set_monitored_resource(series, option_resource_type): set_attribute_label(series, resource_labels, 'zone', 'location') - elif monitored_resource.resource_type == 'gce_instance': - resource_type = monitored_resource.resource_type + elif resource.get_type() == 'gce_instance': + resource_type = 'gce_instance' set_attribute_label(series, resource_labels, 'project_id') set_attribute_label(series, resource_labels, 'instance_id') set_attribute_label(series, resource_labels, 'zone') - elif monitored_resource.resource_type == 'aws_ec2_instance': - resource_type = monitored_resource.resource_type + elif resource.get_type() == 'aws_ec2_instance': + resource_type = 'aws_ec2_instance' set_attribute_label(series, resource_labels, 'aws_account') set_attribute_label(series, resource_labels, 'instance_id') set_attribute_label(series, resource_labels, 'region', diff --git a/opencensus/trace/exporters/stackdriver_exporter.py b/opencensus/trace/exporters/stackdriver_exporter.py index 0c28330f7..d82da001e 100644 --- a/opencensus/trace/exporters/stackdriver_exporter.py +++ b/opencensus/trace/exporters/stackdriver_exporter.py @@ -17,8 +17,7 @@ from google.cloud.trace.client import Client -from opencensus.common.monitored_resource_util.monitored_resource_util \ - import MonitoredResourceUtil +from opencensus.common.monitored_resource_util import monitored_resource from opencensus.common.transports import sync from opencensus.common.version import __version__ from opencensus.trace import attributes_helper @@ -79,10 +78,10 @@ def set_monitored_resource_attributes(span): """Set labels to span that can be used for tracing. :param span: Span object """ - monitored_resource = MonitoredResourceUtil.get_instance() - if monitored_resource is not None: - resource_type = monitored_resource.resource_type - resource_labels = monitored_resource.get_resource_labels() + resource = monitored_resource.get_instance() + if resource is not None: + resource_type = resource.get_type() + resource_labels = resource.get_labels() if resource_type == 'gke_container': resource_type = 'k8s_container' diff --git a/tests/unit/common/monitored_resource_util/test_monitored_resource.py b/tests/unit/common/monitored_resource_util/test_monitored_resource.py index ccb42a07e..af42feb5d 100644 --- a/tests/unit/common/monitored_resource_util/test_monitored_resource.py +++ b/tests/unit/common/monitored_resource_util/test_monitored_resource.py @@ -12,16 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. +from contextlib import contextmanager import mock +import os import unittest from opencensus.common.monitored_resource_util import monitored_resource +@contextmanager +def mock_mr_method(method, use): + with mock.patch('{}.{}'.format(monitored_resource.__name__, method)) as mm: + mm.return_value = use + yield + + +def mock_use_gke(use): + return mock_mr_method('is_gke_environment', use) + + +def mock_use_gce(use): + return mock_mr_method('is_gce_environment', use) + + +def mock_use_aws(use): + return mock_mr_method('is_aws_environment', use) + + +@contextmanager +def mock_gke_env(): + with mock_use_gke(True): + with mock_use_gce(False): + with mock_use_aws(False): + yield + + +@contextmanager +def mock_gce_env(): + with mock_use_gke(False): + with mock_use_gce(True): + with mock_use_aws(False): + yield + + +@contextmanager +def mock_aws_env(): + with mock_use_gke(False): + with mock_use_gce(False): + with mock_use_aws(True): + yield + + class TestMonitoredResource(unittest.TestCase): + @mock.patch('opencensus.common.monitored_resource_util.monitored_resource' - '.GcpMetadataConfig') - def test_GcpGceMonitoredResource(self, gcp_metadata_mock): + '.gcp_metadata_config.GcpMetadataConfig') + def test_gcp_gce_monitored_resource(self, gcp_metadata_mock): mocked_labels = { 'instance_id': 'my-instance', 'project_id': 'my-project', @@ -31,13 +77,14 @@ def test_GcpGceMonitoredResource(self, gcp_metadata_mock): gcp_metadata_mock.return_value = mock.Mock() gcp_metadata_mock.return_value.get_gce_metadata.return_value =\ mocked_labels - resource = monitored_resource.GcpGceMonitoredResource() - self.assertEquals(resource.resource_type, 'gce_instance') - self.assertEquals(resource.get_resource_labels(), mocked_labels) + with mock_gce_env(): + resource = monitored_resource.get_instance() + self.assertEquals(resource.get_type(), 'gce_instance') + self.assertEquals(resource.get_labels(), mocked_labels) @mock.patch('opencensus.common.monitored_resource_util.monitored_resource' - '.GcpMetadataConfig') - def test_GcpGkeMonitoredResource(self, gcp_metadata_mock): + '.gcp_metadata_config.GcpMetadataConfig') + def test_gcp_gke_monitored_resource(self, gcp_metadata_mock): mocked_labels = { 'instance_id': 'my-instance', @@ -52,13 +99,14 @@ def test_GcpGkeMonitoredResource(self, gcp_metadata_mock): gcp_metadata_mock.return_value = mock.Mock() gcp_metadata_mock.return_value.get_gke_metadata.return_value =\ mocked_labels - resource = monitored_resource.GcpGkeMonitoredResource() - self.assertEquals(resource.resource_type, 'gke_container') - self.assertEquals(resource.get_resource_labels(), mocked_labels) + with mock_gke_env(): + resource = monitored_resource.get_instance() + self.assertEquals(resource.get_type(), 'gke_container') + self.assertEquals(resource.get_labels(), mocked_labels) @mock.patch('opencensus.common.monitored_resource_util.monitored_resource' - '.AwsIdentityDocumentUtils') - def test_AwsMonitoredResource(self, aws_metadata_mock): + '.aws_identity_doc_utils.AwsIdentityDocumentUtils') + def test_aws_monitored_resource(self, aws_metadata_mock): mocked_labels = { 'instance_id': 'i-1234567890abcdef0', @@ -70,6 +118,54 @@ def test_AwsMonitoredResource(self, aws_metadata_mock): aws_metadata_mock.return_value.get_aws_metadata.return_value =\ mocked_labels - resource = monitored_resource.AwsMonitoredResource() - self.assertEquals(resource.resource_type, 'aws_ec2_instance') - self.assertEquals(resource.get_resource_labels(), mocked_labels) + with mock_aws_env(): + resource = monitored_resource.get_instance() + self.assertEquals(resource.get_type(), 'aws_ec2_instance') + self.assertEquals(resource.get_labels(), mocked_labels) + + def test_gke_environment(self): + patch = mock.patch.dict(os.environ, + {'KUBERNETES_SERVICE_HOST': '127.0.0.1'}) + + with patch: + mr = monitored_resource.get_instance() + + self.assertIsNotNone(mr) + self.assertEqual(mr.get_type(), "gke_container") + + def test_gce_environment(self): + patch = mock.patch( + 'opencensus.common.monitored_resource_util.' + 'gcp_metadata_config.GcpMetadataConfig.' + 'is_running_on_gcp', + return_value=True) + with patch: + mr = monitored_resource.get_instance() + + self.assertIsNotNone(mr) + self.assertEqual(mr.get_type(), "gce_instance") + + @mock.patch('opencensus.common.monitored_resource_util.' + 'gcp_metadata_config.GcpMetadataConfig.is_running_on_gcp', + return_value=False) + @mock.patch('opencensus.common.monitored_resource_util.' + 'aws_identity_doc_utils.AwsIdentityDocumentUtils.' + 'is_running_on_aws', + return_value=True) + def test_aws_environment(self, aws_util_mock, gcp_metadata_mock): + mr = monitored_resource.get_instance() + + self.assertIsNotNone(mr) + self.assertEqual(mr.get_type(), "aws_ec2_instance") + + @mock.patch('opencensus.common.monitored_resource_util.' + 'gcp_metadata_config.GcpMetadataConfig.is_running_on_gcp', + return_value=False) + @mock.patch('opencensus.common.monitored_resource_util.' + 'aws_identity_doc_utils.AwsIdentityDocumentUtils.' + 'is_running_on_aws', + return_value=False) + def test_non_supported_environment(self, aws_util_mock, gcp_metadata_mock): + mr = monitored_resource.get_instance() + + self.assertIsNone(mr) diff --git a/tests/unit/common/monitored_resource_util/test_monitored_resource_util.py b/tests/unit/common/monitored_resource_util/test_monitored_resource_util.py deleted file mode 100644 index bfb40cbe0..000000000 --- a/tests/unit/common/monitored_resource_util/test_monitored_resource_util.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2018 Google Inc. -# -# 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 os -import unittest - -import mock - -from opencensus.common.monitored_resource_util import monitored_resource -from opencensus.common.monitored_resource_util import monitored_resource_util - - -class TestMonitoredResourceUtil(unittest.TestCase): - def test_gke_environment(self): - patch = mock.patch.dict(os.environ, - {'KUBERNETES_SERVICE_HOST': '127.0.0.1'}) - - with patch: - mr = monitored_resource_util.MonitoredResourceUtil.get_instance() - - self.assertIsNotNone(mr) - self.assertIsInstance(mr, - monitored_resource.GcpGkeMonitoredResource) - - def test_gce_environment(self): - patch = mock.patch( - 'opencensus.common.monitored_resource_util.' - 'gcp_metadata_config.GcpMetadataConfig.' - 'is_running_on_gcp', - return_value=True) - with patch: - mr = monitored_resource_util.MonitoredResourceUtil.get_instance() - - self.assertIsNotNone(mr) - self.assertIsInstance(mr, - monitored_resource.GcpGceMonitoredResource) - - @mock.patch('opencensus.common.monitored_resource_util.' - 'gcp_metadata_config.GcpMetadataConfig.is_running_on_gcp', - return_value=False) - @mock.patch('opencensus.common.monitored_resource_util.' - 'aws_identity_doc_utils.AwsIdentityDocumentUtils.' - 'is_running_on_aws', - return_value=True) - def test_aws_environment(self, aws_util_mock, gcp_metadata_mock): - mr = monitored_resource_util.MonitoredResourceUtil.get_instance() - - self.assertIsNotNone(mr) - self.assertIsInstance(mr, monitored_resource.AwsMonitoredResource) - - @mock.patch('opencensus.common.monitored_resource_util.' - 'gcp_metadata_config.GcpMetadataConfig.is_running_on_gcp', - return_value=False) - @mock.patch('opencensus.common.monitored_resource_util.' - 'aws_identity_doc_utils.AwsIdentityDocumentUtils.' - 'is_running_on_aws', - return_value=False) - def test_non_supported_environment(self, aws_util_mock, gcp_metadata_mock): - mr = monitored_resource_util.MonitoredResourceUtil.get_instance() - - self.assertIsNone(mr) diff --git a/tests/unit/common/test_resource.py b/tests/unit/common/test_resource.py new file mode 100644 index 000000000..ec121f7a5 --- /dev/null +++ b/tests/unit/common/test_resource.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019 Google Inc. +# +# 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. + +try: + from mock import Mock +except ImportError: + from unittest.mock import Mock + +import unittest + +from opencensus.common import resource as resource_module +from opencensus.common.resource import Resource + + +class TestResource(unittest.TestCase): + def test_init_bad_args(self): + long_string = ( + "long string is looooooooooooooooooooooooooooooooooooooooooooooooo" + "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong") + with self.assertRaises(ValueError): + Resource('', {}) + with self.assertRaises(ValueError): + Resource(chr(31), {'key': 'value'}) + with self.assertRaises(ValueError): + Resource(long_string, {'key': 'value'}) + with self.assertRaises(ValueError): + Resource('type', {"": 'value'}) + with self.assertRaises(ValueError): + Resource('type', {None: 'value'}) + with self.assertRaises(ValueError): + Resource('type', {'key': None}) + with self.assertRaises(ValueError): + Resource('type', {chr(31): 'value'}) + with self.assertRaises(ValueError): + Resource('type', {'key': chr(31)}) + with self.assertRaises(ValueError): + Resource(long_string, {long_string: 'value'}) + with self.assertRaises(ValueError): + Resource(long_string, {long_string: long_string}) + with self.assertRaises(ValueError): + Resource('type', {'key1': 'value1', None: 'value2'}) + with self.assertRaises(ValueError): + Resource('type', {'key1': 'value1', 'key2': None}) + # Empty (but not null) label values are ok + Resource('type', {'key': ""}) + + def test_default_init(self): + resource = Resource() + self.assertIsNone(resource.type) + self.assertIsNotNone(resource.labels) + self.assertDictEqual(resource.labels, {}) + + def test_init(self): + resource_type = 'type' + resource_labels = {'key': 'value'} + resource = Resource(resource_type, resource_labels) + self.assertEqual(resource.type, resource_type) + self.assertEqual(resource.labels, resource_labels) + self.assertIsNot(resource.labels, resource_labels) + + def test_get_type(self): + resource_type = 'type' + resource = Resource(resource_type, None) + self.assertEqual(resource.get_type(), resource_type) + + def test_get_labels(self): + label_key = 'key' + label_value = 'value' + resource_labels = {label_key: label_value} + resource = Resource('type', resource_labels) + self.assertEqual(resource.get_labels(), resource_labels) + self.assertIsNot(resource.get_labels(), resource_labels) + got_labels = resource.get_labels() + got_labels[label_key] = Mock() + self.assertNotEqual(resource.get_labels(), got_labels) + self.assertEqual(resource.get_labels()[label_key], label_value) + + def test_merge(self): + r1 = Resource('t1', {'lk1': 'lv1'}) + r2 = Resource('t2', {'lk2': 'lv2'}) + + r1m2 = r1.merge(r2) + self.assertIsNot(r1m2, r1) + self.assertIsNot(r1m2, r2) + self.assertEqual(r1m2.type, r1.type) + self.assertNotEqual(r1m2.type, r2.type) + self.assertDictEqual(r1m2.labels, {'lk1': 'lv1', 'lk2': 'lv2'}) + + r2m1 = r2.merge(r1) + self.assertIsNot(r2m1, r1) + self.assertIsNot(r2m1, r2) + self.assertEqual(r2m1.type, r2.type) + self.assertNotEqual(r2m1.type, r1.type) + self.assertDictEqual(r2m1.labels, {'lk1': 'lv1', 'lk2': 'lv2'}) + + def test_merge_self(self): + resource = Resource('type', {'key': 'value'}) + merged = resource.merge(resource) + self.assertEqual(merged.type, resource.type) + self.assertDictEqual(merged.labels, resource.labels) + + def test_merge_overwrite(self): + r1 = Resource('t1', {'lk1': 'lv11'}) + r2 = Resource('t2', {'lk1': 'lv12', 'lk2': 'lv22'}) + self.assertEqual(r1.merge(r2).labels, {'lk1': 'lv11', 'lk2': 'lv22'}) + self.assertEqual(r2.merge(r1).labels, {'lk1': 'lv12', 'lk2': 'lv22'}) + + +class TestResourceModule(unittest.TestCase): + + def test_check_ascii_256(self): + self.assertIsNone(resource_module.check_ascii_256(None)) + + # Accept both str and unicode in python 2, reject bytes (i.e. encoded + # ascii) in python 3. + self.assertIsNone(resource_module.check_ascii_256('')) + self.assertIsNone(resource_module.check_ascii_256(u'')) + self.assertIsNone(resource_module.check_ascii_256('abc')) + self.assertIsNone(resource_module.check_ascii_256(u'abc')) + + with self.assertRaises(ValueError) as ve: + resource_module.check_ascii_256(u'长猫') + self.assertIn(u'长', ve.exception.args[0]) + self.assertNotIn(u'猫', ve.exception.args[0]) + + with self.assertRaises(ValueError): + resource_module.check_ascii_256('abc' + chr(31)) + with self.assertRaises(ValueError): + resource_module.check_ascii_256(u'abc' + chr(31)) diff --git a/tests/unit/stats/exporters/test_stackdriver_stats.py b/tests/unit/stats/exporters/test_stackdriver_stats.py index a5a88a76c..7b52744cd 100644 --- a/tests/unit/stats/exporters/test_stackdriver_stats.py +++ b/tests/unit/stats/exporters/test_stackdriver_stats.py @@ -224,7 +224,7 @@ def test_on_register_view(self): self.assertTrue(client.create_metric_descriptor.called) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_emit(self, monitor_resource_mock): client = mock.Mock() @@ -270,7 +270,7 @@ def test_handle_upload_no_data(self): self.assertFalse(client.create_time_series.called) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_handle_upload_with_data(self, monitor_resource_mock): client = mock.Mock() @@ -295,7 +295,7 @@ def assertCorrectLabels(self, actual_labels, expected_labels, self.assertDictEqual(actual_labels, expected_labels) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_batched_time_series(self, monitor_resource_mock): client = mock.Mock() @@ -322,7 +322,7 @@ def test_create_batched_time_series(self, monitor_resource_mock): include_opencensus=True) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_batched_time_series_with_many(self, monitor_resource_mock): client = mock.Mock() @@ -382,7 +382,7 @@ def test_stackdriver_register_exporter(self): self.assertEqual(registered_exporters, 1) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries(self, monitor_resource_mock): view_manager, stats_recorder, exporter = \ @@ -437,7 +437,7 @@ def test_create_timeseries(self, monitor_resource_mock): self.assertEqual(value.distribution_value.mean, 25 * MiB) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance') + 'monitored_resource.get_instance') def test_create_timeseries_with_resource(self, monitor_resource_mock): view_manager, stats_recorder, exporter = \ self.setup_create_timeseries_test() @@ -464,10 +464,10 @@ def test_create_timeseries_with_resource(self, monitor_resource_mock): 'namespace_id': 'namespace' } - monitor_resource_mock.return_value = mock.Mock() - monitor_resource_mock.return_value.resource_type = 'gce_instance' - monitor_resource_mock.return_value.get_resource_labels.return_value =\ - mocked_labels + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'gce_instance' + mock_resource.get_labels.return_value = mocked_labels + monitor_resource_mock.return_value = mock_resource time_series_list = exporter.create_time_series_list(v_data, "", "") self.assertEqual(len(time_series_list), 1) @@ -503,10 +503,10 @@ def test_create_timeseries_with_resource(self, monitor_resource_mock): 'namespace_id': 'namespace' } - monitor_resource_mock.return_value = mock.Mock() - monitor_resource_mock.return_value.resource_type = 'gke_container' - monitor_resource_mock.return_value.get_resource_labels.return_value =\ - mocked_labels + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'gke_container' + mock_resource.get_labels.return_value = mocked_labels + monitor_resource_mock.return_value = mock_resource time_series_list = exporter.create_time_series_list(v_data, "", "") self.assertEqual(len(time_series_list), 1) @@ -531,10 +531,10 @@ def test_create_timeseries_with_resource(self, monitor_resource_mock): 'region': 'us-east1', } - monitor_resource_mock.return_value = mock.Mock() - monitor_resource_mock.return_value.resource_type = 'aws_ec2_instance' - monitor_resource_mock.return_value.get_resource_labels.return_value =\ - mocked_labels + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'aws_ec2_instance' + mock_resource.get_labels.return_value = mocked_labels + monitor_resource_mock.return_value = mock_resource time_series_list = exporter.create_time_series_list(v_data, "", "") self.assertEqual(len(time_series_list), 1) @@ -551,10 +551,10 @@ def test_create_timeseries_with_resource(self, monitor_resource_mock): self.assertIsNotNone(time_series) # check for out of box monitored resource - monitor_resource_mock.return_value = mock.Mock() - monitor_resource_mock.return_value.resource_type = '' - monitor_resource_mock.return_value.get_resource_labels.return_value =\ - mock.Mock() + mock_resource = mock.Mock() + mock_resource.get_type.return_value = '' + mock_resource.get_labels.return_value = mock.Mock() + monitor_resource_mock.return_value = mock_resource time_series_list = exporter.create_time_series_list(v_data, "", "") self.assertEqual(len(time_series_list), 1) @@ -567,7 +567,7 @@ def test_create_timeseries_with_resource(self, monitor_resource_mock): self.assertIsNotNone(time_series) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_str_tagvalue(self, monitor_resource_mock): view_manager, stats_recorder, exporter = \ @@ -608,7 +608,7 @@ def test_create_timeseries_str_tagvalue(self, monitor_resource_mock): self.assertEqual(time_series.points[0].value, expected_value) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_str_tagvalue_count_aggregtation( self, monitor_resource_mock): @@ -650,7 +650,7 @@ def test_create_timeseries_str_tagvalue_count_aggregtation( self.assertEqual(time_series.points[0].value, expected_value) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_last_value_float_tagvalue( self, monitor_resource_mock): @@ -692,7 +692,7 @@ def test_create_timeseries_last_value_float_tagvalue( self.assertEqual(time_series.points[0].value, expected_value) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_float_tagvalue(self, monitor_resource_mock): client = mock.Mock() @@ -747,7 +747,7 @@ def test_create_timeseries_float_tagvalue(self, monitor_resource_mock): self.assertEqual(time_series.points[0].value, expected_value) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_multiple_tag_values(self, monitoring_resoure_mock): @@ -807,7 +807,7 @@ def test_create_timeseries_multiple_tag_values(self, self.assertEqual(value2.distribution_value.mean, 12 * MiB) @mock.patch('opencensus.stats.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_create_timeseries_disjoint_tags(self, monitoring_resoure_mock): view_manager, stats_recorder, exporter = \ diff --git a/tests/unit/trace/exporters/test_stackdriver_exporter.py b/tests/unit/trace/exporters/test_stackdriver_exporter.py index 4a2bc2b51..2b76b5c84 100644 --- a/tests/unit/trace/exporters/test_stackdriver_exporter.py +++ b/tests/unit/trace/exporters/test_stackdriver_exporter.py @@ -65,7 +65,7 @@ def test_export(self): self.assertTrue(exporter.transport.export_called) @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_emit(self, mr_mock): trace_id = '6e0c63257de34c92bf9efcd03927272e' @@ -144,7 +144,7 @@ def test_emit(self, mr_mock): self.assertTrue(client.batch_write_spans.called) @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_translate_to_stackdriver(self, mr_mock): project_id = 'PROJECT' @@ -524,7 +524,7 @@ def test_translate_common_attributes_to_stackdriver(self): class Test_set_attributes_gae(unittest.TestCase): @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance', + 'monitored_resource.get_instance', return_value=None) def test_set_attributes_gae(self, mr_mock): import os @@ -586,7 +586,7 @@ def test_set_attributes_gae(self, mr_mock): class TestMonitoredResourceAttributes(unittest.TestCase): @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance') + 'monitored_resource.get_instance') def test_monitored_resource_attributes_gke(self, gmr_mock): import os @@ -666,10 +666,9 @@ def test_monitored_resource_attributes_gke(self, gmr_mock): } } - gmr_mock.return_value = mock.Mock() - - gmr_mock.return_value.resource_type = 'gke_container' - gmr_mock.return_value.get_resource_labels.return_value = { + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'gke_container' + mock_resource.get_labels.return_value = { 'pod_id': 'pod', 'cluster_name': 'cluster', 'namespace_id': 'namespace', @@ -678,6 +677,7 @@ def test_monitored_resource_attributes_gke(self, gmr_mock): 'instance_id': 'instance', 'zone': 'zone1' } + gmr_mock.return_value = mock_resource with mock.patch.dict( os.environ, { stackdriver_exporter._APPENGINE_FLEXIBLE_ENV_VM: 'vm', @@ -693,7 +693,7 @@ def test_monitored_resource_attributes_gke(self, gmr_mock): self.assertEqual(span, expected) @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance') + 'monitored_resource.get_instance') def test_monitored_resource_attributes_gce(self, gmr_mock): trace = {'spans': [{'attributes': {}}]} @@ -729,19 +729,20 @@ def test_monitored_resource_attributes_gce(self, gmr_mock): } } - gmr_mock.return_value = mock.Mock() - gmr_mock.return_value.resource_type = 'gce_instance' - gmr_mock.return_value.get_resource_labels.return_value = { + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'gce_instance' + mock_resource.get_labels.return_value = { 'project_id': 'my_project', 'instance_id': '12345', 'zone': 'zone1' } + gmr_mock.return_value = mock_resource stackdriver_exporter.set_attributes(trace) span = trace.get('spans')[0] self.assertEqual(span, expected) @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance') + 'monitored_resource.get_instance') def test_monitored_resource_attributes_aws(self, amr_mock): trace = {'spans': [{'attributes': {}}]} @@ -771,19 +772,20 @@ def test_monitored_resource_attributes_aws(self, amr_mock): } } - amr_mock.return_value = mock.Mock() - - amr_mock.return_value.resource_type = 'aws_ec2_instance' - amr_mock.return_value.get_resource_labels.return_value = { + mock_resource = mock.Mock() + mock_resource.get_type.return_value = 'aws_ec2_instance' + mock_resource.get_labels.return_value = { 'aws_account': '123456789012', 'region': 'us-west-2' } + amr_mock.return_value = mock_resource + stackdriver_exporter.set_attributes(trace) span = trace.get('spans')[0] self.assertEqual(span, expected) @mock.patch('opencensus.trace.exporters.stackdriver_exporter.' - 'MonitoredResourceUtil.get_instance') + 'monitored_resource.get_instance') def test_monitored_resource_attributes_None(self, mr_mock): trace = {'spans': [{'attributes': {}}]} @@ -806,10 +808,11 @@ def test_monitored_resource_attributes_None(self, mr_mock): span = trace.get('spans')[0] self.assertEqual(span, expected) - mr_mock.return_value = mock.Mock() - mr_mock.return_value.resource_type = mock.Mock() - mr_mock.return_value.get_resource_labels.return_value = mock.Mock( - ) + mock_resource = mock.Mock() + mock_resource.get_type.return_value = mock.Mock() + mock_resource.get_labels.return_value = mock.Mock() + mr_mock.return_value = mock_resource + stackdriver_exporter.set_attributes(trace) span = trace.get('spans')[0] self.assertEqual(span, expected)