summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrwe <rafaelweingartner@gmail.com>2019-11-19 15:41:26 +0100
committerRafael Weingärtner <rafael@apache.org>2020-02-20 09:58:38 -0300
commit4e3c12968d53a28fb9fa016c1eb2377d796eed76 (patch)
treedae3a773c32348e2f5f221860a15677cafd02436
parentd0e8f95fe4e0a46ad5864cc690309facdc368652 (diff)
downloadceilometer-4e3c12968d53a28fb9fa016c1eb2377d796eed76.tar.gz
Multi metric dynamic pollsters (handling attribute values with list of objects)
The initial idea for this feature comes from the `categories` fields that we can find in the `summary` object of the RadosGW API. Each user has a `categories` attribute in the response; in the `categories` list, we can find the object that presents ain a granular fashion the consumption of different RadosGW API operations such as GET, PUT, POST, and may others. In that context, and having in mind that we have APIs with similar data structures, we developed an extension for the dynamic pollster that enables multi-metric processing for a single pollster. It works as follows. The pollster name will contain a placeholder for the variable that identifies the "submetric". E.g. `dynamic.radosgw.api.request.{category}`. The placeholder `{category}` indicates the object attribute that is in the list of objects that we use to load the sub metric name. Then, we must use a special notation in the `value_attribute` configuration to indicate that we are dealing with a list of objects. This is achieved via `[]` (brackets); for instance, in the `dynamic.radosgw.api.request.{category}`, we can use `[categories].ops` as the `value_attribute`. This indicates that the value we retrieve is a list of objects, and when the dynamic pollster processes it, we want it (the pollster) to load the `ops` value for the sub metrics being generated Depends-On: https://review.opendev.org/#/c/694519/ Change-Id: I6ed4632f209ac51a07687476ca316212659d72bb Signed-off-by: Rafael Weingärtner <rafael@apache.org>
-rw-r--r--ceilometer/declarative.py9
-rw-r--r--ceilometer/polling/dynamic_pollster.py728
-rw-r--r--ceilometer/polling/manager.py11
-rw-r--r--ceilometer/polling/non_openstack_dynamic_pollster.py144
-rw-r--r--ceilometer/tests/unit/polling/test_dynamic_pollster.py275
-rw-r--r--ceilometer/tests/unit/polling/test_manager.py17
-rw-r--r--ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py220
-rw-r--r--doc/source/admin/telemetry-dynamic-pollster.rst207
8 files changed, 1205 insertions, 406 deletions
diff --git a/ceilometer/declarative.py b/ceilometer/declarative.py
index 11480b6f..9fe367e9 100644
--- a/ceilometer/declarative.py
+++ b/ceilometer/declarative.py
@@ -42,11 +42,16 @@ class ResourceDefinitionException(DefinitionException):
pass
-class DynamicPollsterDefinitionException(DefinitionException):
+class DynamicPollsterException(DefinitionException):
pass
-class NonOpenStackApisDynamicPollsterException(DefinitionException):
+class DynamicPollsterDefinitionException(DynamicPollsterException):
+ pass
+
+
+class NonOpenStackApisDynamicPollsterException\
+ (DynamicPollsterDefinitionException):
pass
diff --git a/ceilometer/polling/dynamic_pollster.py b/ceilometer/polling/dynamic_pollster.py
index dc7e76b1..913e30cd 100644
--- a/ceilometer/polling/dynamic_pollster.py
+++ b/ceilometer/polling/dynamic_pollster.py
@@ -17,6 +17,9 @@
'/etc/ceilometer/pollsters.d/'. The pollster are defined in YAML files
similar to the idea used for handling notifications.
"""
+import copy
+import re
+
from oslo_log import log
from oslo_utils import timeutils
@@ -24,7 +27,7 @@ from requests import RequestException
from ceilometer import declarative
from ceilometer.polling import plugin_base
-from ceilometer import sample
+from ceilometer import sample as ceilometer_sample
from functools import reduce
import operator
@@ -35,203 +38,447 @@ from six.moves.urllib import parse as url_parse
LOG = log.getLogger(__name__)
-class DynamicPollster(plugin_base.PollsterBase):
+def validate_sample_type(sample_type):
+ if sample_type not in ceilometer_sample.TYPES:
+ raise declarative.DynamicPollsterDefinitionException(
+ "Invalid sample type [%s]. Valid ones are [%s]."
+ % (sample_type, ceilometer_sample.TYPES))
- OPTIONAL_POLLSTER_FIELDS = ['metadata_fields', 'skip_sample_values',
- 'value_mapping', 'default_value',
- 'metadata_mapping',
- 'preserve_mapped_metadata',
- 'response_entries_key']
- REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit',
- 'value_attribute', 'endpoint_type',
- 'url_path']
+class PollsterDefinitionBuilder(object):
- # Mandatory name field
- name = ""
+ def __init__(self, definitions):
+ self.definitions = definitions
- def __init__(self, pollster_definitions, conf=None):
- super(DynamicPollster, self).__init__(conf)
+ def build_definitions(self, configurations):
+ supported_definitions = []
+ for definition in self.definitions:
+ if definition.is_field_applicable_to_definition(configurations):
+ supported_definitions.append(definition)
- self.ALL_POLLSTER_FIELDS =\
- self.OPTIONAL_POLLSTER_FIELDS + self.REQUIRED_POLLSTER_FIELDS
+ if not supported_definitions:
+ raise declarative.DynamicPollsterDefinitionException(
+ "Your configurations do not fit any type of DynamicPollsters, "
+ "please recheck them. Used configurations = [%s]." %
+ configurations)
- LOG.debug("%s instantiated with [%s]", __name__,
- pollster_definitions)
+ definition_name = self.join_supported_definitions_names(
+ supported_definitions)
- self.pollster_definitions = pollster_definitions
- self.validate_pollster_definition()
+ definition_parents = tuple(supported_definitions)
+ definition_attribs = {'extra_definitions': reduce(
+ lambda d1, d2: d1 + d2, map(lambda df: df.extra_definitions,
+ supported_definitions))}
+ definition_type = type(definition_name, definition_parents,
+ definition_attribs)
+ return definition_type(configurations)
- if 'metadata_fields' in self.pollster_definitions:
- LOG.debug("Metadata fields configured to [%s].",
- self.pollster_definitions['metadata_fields'])
+ @staticmethod
+ def join_supported_definitions_names(supported_definitions):
+ return ''.join(map(lambda df: df.__name__,
+ supported_definitions))
- self.set_default_values()
- self.name = self.pollster_definitions['name']
- self.obj = self
+class PollsterSampleExtractor(object):
- def set_default_values(self):
- if 'skip_sample_values' not in self.pollster_definitions:
- self.pollster_definitions['skip_sample_values'] = []
+ def __init__(self, definitions):
+ self.definitions = definitions
- if 'value_mapping' not in self.pollster_definitions:
- self.pollster_definitions['value_mapping'] = {}
+ def generate_new_metadata_fields(self, metadata=None,
+ pollster_definitions=None):
+ pollster_definitions =\
+ pollster_definitions or self.definitions.configurations
+ metadata_mapping = pollster_definitions['metadata_mapping']
+ if not metadata_mapping or not metadata:
+ return
- if 'default_value' not in self.pollster_definitions:
- self.pollster_definitions['default_value'] = -1
+ metadata_keys = list(metadata.keys())
+ for k in metadata_keys:
+ if k not in metadata_mapping:
+ continue
- if 'preserve_mapped_metadata' not in self.pollster_definitions:
- self.pollster_definitions['preserve_mapped_metadata'] = True
+ new_key = metadata_mapping[k]
+ metadata[new_key] = metadata[k]
+ LOG.debug("Generating new key [%s] with content [%s] of key [%s]",
+ new_key, metadata[k], k)
+ if pollster_definitions['preserve_mapped_metadata']:
+ continue
- if 'metadata_mapping' not in self.pollster_definitions:
- self.pollster_definitions['metadata_mapping'] = {}
+ k_value = metadata.pop(k)
+ LOG.debug("Removed key [%s] with value [%s] from "
+ "metadata set that is sent to Gnocchi.", k, k_value)
- if 'response_entries_key' not in self.pollster_definitions:
- self.pollster_definitions['response_entries_key'] = None
+ def generate_sample(self, pollster_sample, pollster_definitons=None):
+ pollster_definitions =\
+ pollster_definitons or self.definitions.configurations
+ metadata = []
+ if 'metadata_fields' in pollster_definitions:
+ metadata = dict((k, pollster_sample.get(k))
+ for k in pollster_definitions['metadata_fields'])
+
+ self.generate_new_metadata_fields(
+ metadata=metadata, pollster_definitions=pollster_definitions)
+ return ceilometer_sample.Sample(
+ timestamp=timeutils.isotime(),
+ name=pollster_definitions['name'],
+ type=pollster_definitions['sample_type'],
+ unit=pollster_definitions['unit'],
+ volume=pollster_sample['value'],
+ user_id=pollster_sample.get("user_id"),
+ project_id=pollster_sample.get("project_id"),
+ resource_id=pollster_sample.get("id"),
+ resource_metadata=metadata)
+
+ def retrieve_attribute_nested_value(self, json_object,
+ value_attribute=None):
+
+ attribute_key = value_attribute or self.definitions.\
+ extract_attribute_key()
+ LOG.debug("Retrieving the nested keys [%s] from [%s].",
+ attribute_key, json_object)
+ keys_and_operations = attribute_key.split("|")
+ attribute_key = keys_and_operations[0].strip()
+ nested_keys = attribute_key.split(".")
+ value = reduce(operator.getitem, nested_keys, json_object)
- def validate_pollster_definition(self):
- missing_required_fields = \
- [field for field in self.REQUIRED_POLLSTER_FIELDS
- if field not in self.pollster_definitions]
+ return self.operate_value(keys_and_operations, value)
+
+ def operate_value(self, keys_and_operations, value):
+ # We do not have operations to be executed against the value extracted
+ if len(keys_and_operations) < 2:
+ return value
+ for operation in keys_and_operations[1::]:
+ # The operation must be performed onto the 'value' variable
+ if 'value' not in operation:
+ raise declarative.DynamicPollsterDefinitionException(
+ "The attribute field operation [%s] must use the ["
+ "value] variable." % operation,
+ self.definitions.configurations)
+ LOG.debug("Executing operation [%s] against value [%s].",
+ operation, value)
+ value = eval(operation.strip())
+ LOG.debug("Result [%s] of operation [%s].",
+ value, operation)
+ return value
- if missing_required_fields:
- raise declarative.DynamicPollsterDefinitionException(
- "Required fields %s not specified."
- % missing_required_fields, self.pollster_definitions)
- sample_type = self.pollster_definitions['sample_type']
- if sample_type not in sample.TYPES:
- raise declarative.DynamicPollsterDefinitionException(
- "Invalid sample type [%s]. Valid ones are [%s]."
- % (sample_type, sample.TYPES), self.pollster_definitions)
+class SimplePollsterSampleExtractor(PollsterSampleExtractor):
- for definition_key in self.pollster_definitions:
- if definition_key not in self.ALL_POLLSTER_FIELDS:
- LOG.warning(
- "Field [%s] defined in [%s] is unknown "
- "and will be ignored. Valid fields are [%s].",
- definition_key, self.pollster_definitions,
- self.ALL_POLLSTER_FIELDS)
+ def generate_single_sample(self, pollster_sample):
+ value = self.retrieve_attribute_nested_value(pollster_sample)
+ value = self.definitions.value_mapper.map_or_skip_value(
+ value, pollster_sample)
- def get_samples(self, manager, cache, resources):
- if not resources:
- LOG.debug("No resources received for processing.")
- yield None
+ if isinstance(value, SkippedSample):
+ return value
- for r in resources:
- LOG.debug("Executing get sample for resource [%s].", r)
+ pollster_sample['value'] = value
- samples = list([])
- try:
- samples = self.execute_request_get_samples(
- keystone_client=manager._keystone, resource=r)
- except RequestException as e:
- LOG.warning("Error [%s] while loading samples for [%s] "
- "for dynamic pollster [%s].",
- e, r, self.name)
+ return self.generate_sample(pollster_sample)
+
+ def extract_sample(self, pollster_sample):
+ sample = self.generate_single_sample(pollster_sample)
+ if isinstance(sample, SkippedSample):
+ return sample
+ yield sample
- for pollster_sample in samples:
- response_value_attribute_name = self.pollster_definitions[
- 'value_attribute']
- value = self.retrieve_attribute_nested_value(
- pollster_sample, response_value_attribute_name)
-
- skip_sample_values = \
- self.pollster_definitions['skip_sample_values']
- if skip_sample_values and value in skip_sample_values:
- LOG.debug("Skipping sample [%s] because value [%s] "
- "is configured to be skipped in skip list [%s].",
- pollster_sample, value, skip_sample_values)
- continue
- value = self.execute_value_mapping(value)
+class MultiMetricPollsterSampleExtractor(PollsterSampleExtractor):
- user_id = None
- if 'user_id' in pollster_sample:
- user_id = pollster_sample["user_id"]
+ def extract_sample(self, pollster_sample):
+ pollster_definitions = self.definitions.configurations
+ value = self.retrieve_attribute_nested_value(pollster_sample)
+ LOG.debug("We are dealing with a multi metric pollster. The "
+ "value we are processing is the following: [%s].",
+ value)
- project_id = None
- if 'project_id' in pollster_sample:
- project_id = pollster_sample["project_id"]
+ self.validate_sample_is_list(value)
+ sub_metric_placeholder, pollster_name, sub_metric_attribute_name = \
+ self.extract_names_attrs()
- resource_id = None
- if 'id' in pollster_sample:
- resource_id = pollster_sample["id"]
+ value_attribute = \
+ self.extract_field_name_from_value_attribute_configuration()
+ LOG.debug("Using attribute [%s] to look for values in the "
+ "multi metric pollster [%s] with sample [%s]",
+ value_attribute, pollster_definitions, value)
- metadata = []
- if 'metadata_fields' in self.pollster_definitions:
- metadata = dict((k, pollster_sample.get(k))
- for k in self.pollster_definitions[
- 'metadata_fields'])
- self.generate_new_metadata_fields(metadata=metadata)
- yield sample.Sample(
- timestamp=timeutils.isotime(),
+ pollster_definitions = copy.deepcopy(pollster_definitions)
+ yield from self.extract_sub_samples(value, sub_metric_attribute_name,
+ pollster_name, value_attribute,
+ sub_metric_placeholder,
+ pollster_definitions,
+ pollster_sample)
- name=self.pollster_definitions['name'],
- type=self.pollster_definitions['sample_type'],
- unit=self.pollster_definitions['unit'],
- volume=value,
+ def extract_sub_samples(self, value, sub_metric_attribute_name,
+ pollster_name, value_attribute,
+ sub_metric_placeholder, pollster_definitions,
+ pollster_sample):
- user_id=user_id,
- project_id=project_id,
- resource_id=resource_id,
+ for sub_sample in value:
+ sub_metric_name = sub_sample[sub_metric_attribute_name]
+ new_metric_name = pollster_name.replace(
+ sub_metric_placeholder, sub_metric_name)
+ pollster_definitions['name'] = new_metric_name
- resource_metadata=metadata
- )
+ actual_value = self.retrieve_attribute_nested_value(
+ sub_sample, value_attribute)
+
+ pollster_sample['value'] = actual_value
+
+ if self.should_skip_generate_sample(actual_value, sub_sample,
+ sub_metric_name):
+ continue
+
+ yield self.generate_sample(pollster_sample, pollster_definitions)
+
+ def extract_field_name_from_value_attribute_configuration(self):
+ value_attribute = self.definitions.configurations['value_attribute']
+ return self.definitions.pattern_pollster_value_attribute.match(
+ value_attribute).group(3)[1::]
+
+ def extract_names_attrs(self):
+ pollster_name = self.definitions.configurations['name']
+ sub_metric_placeholder = pollster_name.split(".").pop()
+ return (sub_metric_placeholder,
+ pollster_name,
+ self.definitions.pattern_pollster_name.match(
+ "." + sub_metric_placeholder).group(2))
+
+ def validate_sample_is_list(self, value):
+ pollster_definitions = self.definitions.configurations
+ if not isinstance(value, list):
+ raise declarative.DynamicPollsterException(
+ "Multi metric pollster defined, but the value [%s]"
+ " obtained with [%s] attribute is not a list"
+ " of objects."
+ % (value,
+ pollster_definitions['value_attribute']),
+ pollster_definitions)
+
+ def should_skip_generate_sample(self, actual_value, sub_sample,
+ sub_metric_name):
+ skip_sample_values = \
+ self.definitions.configurations['skip_sample_values']
+ if actual_value in skip_sample_values:
+ LOG.debug(
+ "Skipping multi metric sample [%s] because "
+ "value [%s] is configured to be skipped in "
+ "skip list [%s].", sub_sample, actual_value,
+ skip_sample_values)
+ return True
+ if sub_metric_name in skip_sample_values:
+ LOG.debug(
+ "Skipping sample [%s] because its sub-metric "
+ "name [%s] is configured to be skipped in "
+ "skip list [%s].", sub_sample, sub_metric_name,
+ skip_sample_values)
+ return True
+ return False
+
+
+class PollsterValueMapper(object):
+
+ def __init__(self, definitions):
+ self.definitions = definitions
+
+ def map_or_skip_value(self, value, pollster_sample):
+ skip_sample_values = \
+ self.definitions.configurations['skip_sample_values']
+
+ if value in skip_sample_values:
+ LOG.debug("Skipping sample [%s] because value [%s] "
+ "is configured to be skipped in skip list [%s].",
+ pollster_sample, value, skip_sample_values)
+ return SkippedSample()
+
+ return self.execute_value_mapping(value)
def execute_value_mapping(self, value):
- value_mapping = self.pollster_definitions['value_mapping']
- if value_mapping:
- if value in value_mapping:
- old_value = value
- value = value_mapping[value]
- LOG.debug("Value mapped from [%s] to [%s]",
- old_value, value)
- else:
- default_value = \
- self.pollster_definitions['default_value']
- LOG.warning(
- "Value [%s] was not found in value_mapping [%s]; "
- "therefore, we will use the default [%s].",
- value, value_mapping, default_value)
- value = default_value
+ value_mapping = self.definitions.configurations['value_mapping']
+ if not value_mapping:
+ return value
+
+ if value in value_mapping:
+ old_value = value
+ value = value_mapping[value]
+ LOG.debug("Value mapped from [%s] to [%s]",
+ old_value, value)
+ else:
+ default_value = \
+ self.definitions.configurations['default_value']
+ LOG.warning(
+ "Value [%s] was not found in value_mapping [%s]; "
+ "therefore, we will use the default [%s].",
+ value, value_mapping, default_value)
+ value = default_value
return value
- def generate_new_metadata_fields(self, metadata=None):
- metadata_mapping = self.pollster_definitions['metadata_mapping']
- if not metadata_mapping or not metadata:
- return
- metadata_keys = list(metadata.keys())
- for k in metadata_keys:
- if k not in metadata_mapping:
- continue
+class PollsterDefinition(object):
+
+ def __init__(self, name, required=False, on_missing=lambda df: df.default,
+ default=None, validation_regex=None, creatable=True,
+ validator=None):
+ self.name = name
+ self.required = required
+ self.on_missing = on_missing
+ self.validation_regex = validation_regex
+ self.creatable = creatable
+ self.default = default
+ if self.validation_regex:
+ self.validation_pattern = re.compile(self.validation_regex)
+ self.validator = validator
+
+ def validate(self, val):
+ if val is None:
+ return self.on_missing(self)
+ if self.validation_regex and not self.validation_pattern.match(val):
+ raise declarative.DynamicPollsterDefinitionException(
+ "Pollster %s [%s] does not match [%s]."
+ % (self.name, val, self.validation_regex))
+
+ if self.validator:
+ self.validator(val)
+
+ return val
+
+
+class PollsterDefinitions(object):
+
+ POLLSTER_VALID_NAMES_REGEXP = "^([\w-]+)(\.[\w-]+)*(\.{[\w-]+})?$"
+
+ standard_definitions = [
+ PollsterDefinition(name='name', required=True,
+ validation_regex=POLLSTER_VALID_NAMES_REGEXP),
+ PollsterDefinition(name='sample_type', required=True,
+ validator=validate_sample_type),
+ PollsterDefinition(name='unit', required=True),
+ PollsterDefinition(name='endpoint_type', required=True),
+ PollsterDefinition(name='url_path', required=True),
+ PollsterDefinition(name='metadata_fields', creatable=False),
+ PollsterDefinition(name='skip_sample_values', default=[]),
+ PollsterDefinition(name='value_mapping', default={}),
+ PollsterDefinition(name='default_value', default=-1),
+ PollsterDefinition(name='metadata_mapping', default={}),
+ PollsterDefinition(name='preserve_mapped_metadata', default=True),
+ PollsterDefinition(name='response_entries_key')]
+
+ extra_definitions = []
+
+ def __init__(self, configurations):
+ self.configurations = configurations
+ self.value_mapper = PollsterValueMapper(self)
+ self.definitions = self.map_definitions()
+ self.validate_configurations(configurations)
+ self.validate_missing()
+ self.sample_gatherer = PollsterSampleGatherer(self)
+ self.sample_extractor = SimplePollsterSampleExtractor(self)
+
+ def validate_configurations(self, configurations):
+ for k, v in self.definitions.items():
+ if configurations.get(k) is not None:
+ self.configurations[k] = self.definitions[k].validate(
+ self.configurations[k])
+ elif self.definitions[k].creatable:
+ self.configurations[k] = self.definitions[k].default
+
+ @staticmethod
+ def is_field_applicable_to_definition(configurations):
+ return True
+
+ def map_definitions(self):
+ definitions = dict(
+ map(lambda df: (df.name, df), self.standard_definitions))
+ extra_definitions = dict(
+ map(lambda df: (df.name, df), self.extra_definitions))
+ definitions.update(extra_definitions)
+ return definitions
+
+ def extract_attribute_key(self):
+ pass
+
+ def validate_missing(self):
+ required_configurations = map(lambda fdf: fdf.name,
+ filter(lambda df: df.required,
+ self.definitions.values()))
+
+ missing = list(filter(
+ lambda rf: rf not in map(lambda f: f[0],
+ filter(lambda f: f[1],
+ self.configurations.items())),
+ required_configurations))
+
+ if missing:
+ raise declarative.DynamicPollsterDefinitionException(
+ "Required fields %s not specified."
+ % missing, self.configurations)
- new_key = metadata_mapping[k]
- metadata[new_key] = metadata[k]
- LOG.debug("Generating new key [%s] with content [%s] of key [%s]",
- new_key, metadata[k], k)
- if self.pollster_definitions['preserve_mapped_metadata']:
- continue
- k_value = metadata.pop(k)
- LOG.debug("Removed key [%s] with value [%s] from "
- "metadata set that is sent to Gnocchi.", k, k_value)
+class MultiMetricPollsterDefinitions(PollsterDefinitions):
+
+ MULTI_METRIC_POLLSTER_NAME_REGEXP = ".*(\.{(\w+)})$"
+ pattern_pollster_name = re.compile(
+ MULTI_METRIC_POLLSTER_NAME_REGEXP)
+ MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP = "^(\[(\w+)\])((\.\w+)+)$"
+ pattern_pollster_value_attribute = re.compile(
+ MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP)
+
+ extra_definitions = [
+ PollsterDefinition(
+ name='value_attribute', required=True,
+ validation_regex=MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP),
+ ]
+
+ def __init__(self, configurations):
+ super(MultiMetricPollsterDefinitions, self).__init__(configurations)
+ self.sample_extractor = MultiMetricPollsterSampleExtractor(self)
+
+ @staticmethod
+ def is_field_applicable_to_definition(configurations):
+ return configurations.get(
+ 'name') and MultiMetricPollsterDefinitions.\
+ pattern_pollster_name.match(configurations['name'])
+
+ def extract_attribute_key(self):
+ return self.pattern_pollster_value_attribute.match(
+ self.configurations['value_attribute']).group(2)
+
+
+class SingleMetricPollsterDefinitions(PollsterDefinitions):
+
+ extra_definitions = [
+ PollsterDefinition(name='value_attribute', required=True)]
+
+ def __init__(self, configurations):
+ super(SingleMetricPollsterDefinitions, self).__init__(configurations)
+
+ def extract_attribute_key(self):
+ return self.configurations['value_attribute']
+
+ @staticmethod
+ def is_field_applicable_to_definition(configurations):
+ return not MultiMetricPollsterDefinitions. \
+ is_field_applicable_to_definition(configurations)
+
+
+class PollsterSampleGatherer(object):
+
+ def __init__(self, definitions):
+ self.definitions = definitions
@property
def default_discovery(self):
- return 'endpoint:' + self.pollster_definitions['endpoint_type']
+ return 'endpoint:' + self.definitions.configurations['endpoint_type']
def execute_request_get_samples(self, **kwargs):
- resp, url = self.internal_execute_request_get_samples(kwargs)
+ resp, url = self.definitions.sample_gatherer. \
+ internal_execute_request_get_samples(kwargs)
response_json = resp.json()
-
entry_size = len(response_json)
LOG.debug("Entries [%s] in the JSON for request [%s] "
"for dynamic pollster [%s].",
- response_json, url, self.name)
+ response_json, url, self.definitions.configurations['name'])
if entry_size > 0:
return self.retrieve_entries_from_response(response_json)
@@ -241,7 +488,7 @@ class DynamicPollster(plugin_base.PollsterBase):
keystone_client = kwargs['keystone_client']
endpoint = kwargs['resource']
url = url_parse.urljoin(
- endpoint, self.pollster_definitions['url_path'])
+ endpoint, self.definitions.configurations['url_path'])
resp = keystone_client.session.get(url, authenticated=True)
if resp.status_code != requests.codes.ok:
resp.raise_for_status()
@@ -251,41 +498,172 @@ class DynamicPollster(plugin_base.PollsterBase):
if isinstance(response_json, list):
return response_json
- first_entry_name = self.pollster_definitions['response_entries_key']
+ first_entry_name = \
+ self.definitions.configurations['response_entries_key']
if not first_entry_name:
try:
first_entry_name = next(iter(response_json))
except RuntimeError as e:
LOG.debug("Generator threw a StopIteration "
"and we need to catch it [%s].", e)
- return self.retrieve_attribute_nested_value(response_json,
- first_entry_name)
+ return self.definitions.sample_extractor. \
+ retrieve_attribute_nested_value(response_json, first_entry_name)
- def retrieve_attribute_nested_value(self, json_object, attribute_key):
- LOG.debug("Retrieving the nested keys [%s] from [%s].",
- attribute_key, json_object)
- keys_and_operations = attribute_key.split("|")
- attribute_key = keys_and_operations[0].strip()
+class NonOpenStackApisPollsterDefinition(PollsterDefinitions):
- nested_keys = attribute_key.split(".")
- value = reduce(operator.getitem, nested_keys, json_object)
+ extra_definitions = [
+ PollsterDefinition(name='value_attribute', required=True),
+ PollsterDefinition(name='module', required=True),
+ PollsterDefinition(name='authentication_object', required=True),
+ PollsterDefinition(name='user_id_attribute'),
+ PollsterDefinition(name='resource_id_attribute'),
+ PollsterDefinition(name='barbican_secret_id', default=""),
+ PollsterDefinition(name='authentication_parameters', default=""),
+ PollsterDefinition(name='project_id_attribute'),
+ PollsterDefinition(name='endpoint_type')]
- # We have operations to be executed against the value extracted
- if len(keys_and_operations) > 1:
- for operation in keys_and_operations[1::]:
- # The operation must be performed onto the 'value' variable
- if 'value' not in operation:
- raise declarative.DynamicPollsterDefinitionException(
- "The attribute field operation [%s] must use the ["
- "value] variable." % operation,
- self.pollster_definitions)
+ def __init__(self, configurations):
+ super(NonOpenStackApisPollsterDefinition, self).__init__(
+ configurations)
+ self.sample_gatherer = NonOpenStackApisSamplesGatherer(self)
- LOG.debug("Executing operation [%s] against value[%s].",
- operation, value)
+ @staticmethod
+ def is_field_applicable_to_definition(configurations):
+ return configurations.get('module')
- value = eval(operation.strip())
- LOG.debug("Result [%s] of operation [%s].",
- value, operation)
- return value
+class NonOpenStackApisSamplesGatherer(PollsterSampleGatherer):
+
+ @property
+ def default_discovery(self):
+ return 'barbican:' + \
+ self.definitions.configurations['barbican_secret_id']
+
+ def internal_execute_request_get_samples(self, kwargs):
+ credentials = kwargs['resource']
+
+ override_credentials = self.definitions.configurations[
+ 'authentication_parameters']
+ if override_credentials:
+ credentials = override_credentials
+
+ url = self.definitions.configurations['url_path']
+
+ authenticator_module_name = self.definitions.configurations['module']
+ authenticator_class_name = \
+ self.definitions.configurations['authentication_object']
+
+ imported_module = __import__(authenticator_module_name)
+ authenticator_class = getattr(imported_module,
+ authenticator_class_name)
+
+ authenticator_arguments = list(map(str.strip, credentials.split(",")))
+ authenticator_instance = authenticator_class(*authenticator_arguments)
+
+ resp = requests.get(
+ url,
+ auth=authenticator_instance)
+
+ if resp.status_code != requests.codes.ok:
+ raise declarative.NonOpenStackApisDynamicPollsterException(
+ "Error while executing request[%s]."
+ " Status[%s] and reason [%s]."
+ % (url, resp.status_code, resp.reason))
+
+ return resp, url
+
+ def execute_request_get_samples(self, **kwargs):
+ samples = super(NonOpenStackApisSamplesGatherer,
+ self).execute_request_get_samples(**kwargs)
+
+ if samples:
+ user_id_attribute = self.definitions.configurations[
+ 'user_id_attribute']
+ project_id_attribute = self.definitions.configurations[
+ 'project_id_attribute']
+ resource_id_attribute = self.definitions.configurations[
+ 'resource_id_attribute']
+
+ for request_sample in samples:
+ self.generate_new_attributes_in_sample(
+ request_sample, user_id_attribute, 'user_id')
+ self.generate_new_attributes_in_sample(
+ request_sample, project_id_attribute, 'project_id')
+ self.generate_new_attributes_in_sample(
+ request_sample, resource_id_attribute, 'id')
+
+ return samples
+
+ def generate_new_attributes_in_sample(
+ self, sample, attribute_key, new_attribute_key):
+ if attribute_key:
+ attribute_value = self.definitions.sample_extractor. \
+ retrieve_attribute_nested_value(sample, attribute_key)
+
+ LOG.debug("Mapped attribute [%s] to value [%s] in sample [%s].",
+ attribute_key, attribute_value, sample)
+
+ sample[new_attribute_key] = attribute_value
+
+
+class SkippedSample(object):
+ pass
+
+
+class DynamicPollster(plugin_base.PollsterBase):
+ # Mandatory name field
+ name = ""
+
+ def __init__(self, pollster_definitions={}, conf=None,
+ supported_definitions=[NonOpenStackApisPollsterDefinition,
+ MultiMetricPollsterDefinitions,
+ SingleMetricPollsterDefinitions]):
+ super(DynamicPollster, self).__init__(conf)
+ self.supported_definitions = supported_definitions
+ LOG.debug("%s instantiated with [%s]", __name__,
+ pollster_definitions)
+
+ self.definitions = PollsterDefinitionBuilder(
+ self.supported_definitions).build_definitions(pollster_definitions)
+ self.pollster_definitions = self.definitions.configurations
+ if 'metadata_fields' in self.pollster_definitions:
+ LOG.debug("Metadata fields configured to [%s].",
+ self.pollster_definitions['metadata_fields'])
+
+ self.name = self.pollster_definitions['name']
+ self.obj = self
+
+ @property
+ def default_discovery(self):
+ return self.definitions.sample_gatherer.default_discovery()
+
+ def load_samples(self, resource, manager):
+ try:
+ return self.definitions.sample_gatherer.\
+ execute_request_get_samples(keystone_client=manager._keystone,
+ resource=resource)
+ except RequestException as e:
+ LOG.warning("Error [%s] while loading samples for [%s] "
+ "for dynamic pollster [%s].",
+ e, resource, self.name)
+
+ return list([])
+
+ def get_samples(self, manager, cache, resources):
+ if not resources:
+ LOG.debug("No resources received for processing.")
+ yield None
+
+ for r in resources:
+ LOG.debug("Executing get sample for resource [%s].", r)
+ samples = self.load_samples(r, manager)
+ for pollster_sample in samples:
+ sample = self.extract_sample(pollster_sample)
+ if isinstance(sample, SkippedSample):
+ continue
+ yield from sample
+
+ def extract_sample(self, pollster_sample):
+ return self.definitions.sample_extractor.extract_sample(
+ pollster_sample)
diff --git a/ceilometer/polling/manager.py b/ceilometer/polling/manager.py
index 3329d16e..fbdce8b5 100644
--- a/ceilometer/polling/manager.py
+++ b/ceilometer/polling/manager.py
@@ -40,7 +40,6 @@ from ceilometer import declarative
from ceilometer import keystone_client
from ceilometer import messaging
from ceilometer.polling import dynamic_pollster
-from ceilometer.polling import non_openstack_dynamic_pollster
from ceilometer.polling import plugin_base
from ceilometer.publisher import utils as publisher_utils
from ceilometer import utils
@@ -340,7 +339,8 @@ class AgentManager(cotyledon.Service):
pollster_name, pollsters_definitions_file)
try:
pollsters_definitions[pollster_name] =\
- self.instantiate_dynamic_pollster(pollster_cfg)
+ dynamic_pollster.DynamicPollster(
+ pollster_cfg, self.conf)
except Exception as e:
LOG.error(
"Error [%s] while loading dynamic pollster [%s].",
@@ -355,13 +355,6 @@ class AgentManager(cotyledon.Service):
len(pollsters_definitions))
return pollsters_definitions.values()
- def instantiate_dynamic_pollster(self, pollster_cfg):
- if 'module' in pollster_cfg:
- return non_openstack_dynamic_pollster\
- .NonOpenStackApisDynamicPollster(pollster_cfg, self.conf)
- else:
- return dynamic_pollster.DynamicPollster(pollster_cfg, self.conf)
-
@staticmethod
def _get_ext_mgr(namespace, *args, **kwargs):
def _catch_extension_load_error(mgr, ep, exc):
diff --git a/ceilometer/polling/non_openstack_dynamic_pollster.py b/ceilometer/polling/non_openstack_dynamic_pollster.py
deleted file mode 100644
index 2854b921..00000000
--- a/ceilometer/polling/non_openstack_dynamic_pollster.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#
-# 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.
-
-
-"""Non-OpenStack Dynamic pollster component
- This component enables operators to create pollsters on the fly
- via configuration for non-OpenStack APIs. This appraoch is quite
- useful when adding metrics from APIs such as RadosGW into the Cloud
- rating and billing modules.
-"""
-import copy
-import requests
-
-from ceilometer.declarative import NonOpenStackApisDynamicPollsterException
-from ceilometer.polling.dynamic_pollster import DynamicPollster
-from oslo_log import log
-
-LOG = log.getLogger(__name__)
-
-
-class NonOpenStackApisDynamicPollster(DynamicPollster):
-
- POLLSTER_REQUIRED_POLLSTER_FIELDS = ['module', 'authentication_object']
-
- POLLSTER_OPTIONAL_POLLSTER_FIELDS = ['user_id_attribute',
- 'project_id_attribute',
- 'resource_id_attribute',
- 'barbican_secret_id',
- 'authentication_parameters'
- ]
-
- def __init__(self, pollster_definitions, conf=None):
- # Making sure that we do not change anything in parent classes
- self.REQUIRED_POLLSTER_FIELDS = copy.deepcopy(
- DynamicPollster.REQUIRED_POLLSTER_FIELDS)
- self.OPTIONAL_POLLSTER_FIELDS = copy.deepcopy(
- DynamicPollster.OPTIONAL_POLLSTER_FIELDS)
-
- # Non-OpenStack dynamic pollster do not need the 'endpoint_type'.
- self.REQUIRED_POLLSTER_FIELDS.remove('endpoint_type')
-
- self.REQUIRED_POLLSTER_FIELDS += self.POLLSTER_REQUIRED_POLLSTER_FIELDS
- self.OPTIONAL_POLLSTER_FIELDS += self.POLLSTER_OPTIONAL_POLLSTER_FIELDS
-
- super(NonOpenStackApisDynamicPollster, self).__init__(
- pollster_definitions, conf)
-
- def set_default_values(self):
- super(NonOpenStackApisDynamicPollster, self).set_default_values()
-
- if 'user_id_attribute' not in self.pollster_definitions:
- self.pollster_definitions['user_id_attribute'] = None
-
- if 'project_id_attribute' not in self.pollster_definitions:
- self.pollster_definitions['project_id_attribute'] = None
-
- if 'resource_id_attribute' not in self.pollster_definitions:
- self.pollster_definitions['resource_id_attribute'] = None
-
- if 'barbican_secret_id' not in self.pollster_definitions:
- self.pollster_definitions['barbican_secret_id'] = ""
-
- if 'authentication_parameters' not in self.pollster_definitions:
- self.pollster_definitions['authentication_parameters'] = ""
-
- @property
- def default_discovery(self):
- return 'barbican:' + self.pollster_definitions['barbican_secret_id']
-
- def internal_execute_request_get_samples(self, kwargs):
- credentials = kwargs['resource']
-
- override_credentials = self.pollster_definitions[
- 'authentication_parameters']
- if override_credentials:
- credentials = override_credentials
-
- url = self.pollster_definitions['url_path']
-
- authenticator_module_name = self.pollster_definitions['module']
- authenticator_class_name = \
- self.pollster_definitions['authentication_object']
-
- imported_module = __import__(authenticator_module_name)
- authenticator_class = getattr(imported_module,
- authenticator_class_name)
-
- authenticator_arguments = list(map(str.strip, credentials.split(",")))
- authenticator_instance = authenticator_class(*authenticator_arguments)
-
- resp = requests.get(
- url,
- auth=authenticator_instance)
-
- if resp.status_code != requests.codes.ok:
- raise NonOpenStackApisDynamicPollsterException(
- "Error while executing request[%s]."
- " Status[%s] and reason [%s]."
- % (url, resp.status_code, resp.reason))
-
- return resp, url
-
- def execute_request_get_samples(self, **kwargs):
- samples = super(NonOpenStackApisDynamicPollster,
- self).execute_request_get_samples(**kwargs)
-
- if samples:
- user_id_attribute = self.pollster_definitions[
- 'user_id_attribute']
- project_id_attribute = self.pollster_definitions[
- 'project_id_attribute']
- resource_id_attribute = self.pollster_definitions[
- 'resource_id_attribute']
-
- for sample in samples:
- self.generate_new_attributes_in_sample(
- sample, user_id_attribute, 'user_id')
- self.generate_new_attributes_in_sample(
- sample, project_id_attribute, 'project_id')
- self.generate_new_attributes_in_sample(
- sample, resource_id_attribute, 'id')
-
- return samples
-
- def generate_new_attributes_in_sample(
- self, sample, attribute_key, new_attribute_key):
- if attribute_key:
- attribute_value = self.retrieve_attribute_nested_value(
- sample, attribute_key)
-
- LOG.debug("Mapped attribute [%s] to value [%s] in sample [%s].",
- attribute_key, attribute_value, sample)
-
- sample[new_attribute_key] = attribute_value
diff --git a/ceilometer/tests/unit/polling/test_dynamic_pollster.py b/ceilometer/tests/unit/polling/test_dynamic_pollster.py
index 52a2b916..912f7b8f 100644
--- a/ceilometer/tests/unit/polling/test_dynamic_pollster.py
+++ b/ceilometer/tests/unit/polling/test_dynamic_pollster.py
@@ -14,6 +14,7 @@
"""Tests for ceilometer/polling/dynamic_pollster.py
"""
+from oslotest import base
from ceilometer.declarative import DynamicPollsterDefinitionException
from ceilometer.polling import dynamic_pollster
@@ -23,13 +24,87 @@ import copy
import logging
import mock
-from oslotest import base
-
import requests
LOG = logging.getLogger(__name__)
+REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit',
+ 'value_attribute', 'endpoint_type',
+ 'url_path']
+
+
+class SampleGenerator(object):
+
+ def __init__(self, samples_dict, turn_to_list=False):
+ self.turn_to_list = turn_to_list
+ self.samples_dict = {}
+ for k, v in samples_dict.items():
+ if isinstance(v, list):
+ self.samples_dict[k] = [0, v]
+ else:
+ self.samples_dict[k] = [0, [v]]
+
+ def get_next_sample_dict(self):
+ _dict = {}
+ for key in self.samples_dict.keys():
+ _dict[key] = self.get_next_sample(key)
+
+ if self.turn_to_list:
+ _dict = [_dict]
+ return _dict
+
+ def get_next_sample(self, key):
+ samples = self.samples_dict[key][1]
+ samples_next_iteration = self.samples_dict[key][0] % len(samples)
+ self.samples_dict[key][0] += 1
+ _sample = samples[samples_next_iteration]
+ if isinstance(_sample, SampleGenerator):
+ return _sample.get_next_sample_dict()
+ return _sample
+
+
+class PagedSamplesGenerator(SampleGenerator):
+
+ def __init__(self, samples_dict, dict_name, page_link_name):
+ super(PagedSamplesGenerator, self).__init__(samples_dict)
+ self.dict_name = dict_name
+ self.page_link_name = page_link_name
+ self.response = {}
+
+ def generate_samples(self, page_base_link, page_links, last_page_size):
+ self.response.clear()
+ current_page_link = page_base_link
+ for page_link, page_size in page_links.items():
+ page_link = page_base_link + "/" + page_link
+ self.response[current_page_link] = {
+ self.page_link_name: page_link,
+ self.dict_name: self.populate_page(page_size)
+ }
+ current_page_link = page_link
+
+ self.response[current_page_link] = {
+ self.dict_name: self.populate_page(last_page_size)
+ }
+
+ def populate_page(self, page_size):
+ page = []
+ for item_number in range(0, page_size):
+ page.append(self.get_next_sample_dict())
+
+ return page
+
+
+class PagedSamplesGeneratorHttpRequestMock(PagedSamplesGenerator):
+
+ def mock_request(self, url, **kwargs):
+ return_value = TestDynamicPollster.FakeResponse()
+ return_value.status_code = requests.codes.ok
+ return_value.json_object = self.response[url]
+
+ return return_value
+
+
class TestDynamicPollster(base.BaseTestCase):
class FakeResponse(object):
status_code = None
@@ -42,7 +117,8 @@ class TestDynamicPollster(base.BaseTestCase):
raise requests.HTTPError("Mock HTTP error.", response=self)
class FakeManager(object):
- _keystone = None
+ def __init__(self, keystone=None):
+ self._keystone = keystone
def setUp(self):
super(TestDynamicPollster, self).setUp()
@@ -62,19 +138,61 @@ class TestDynamicPollster(base.BaseTestCase):
'old-metadata-name': "new-metadata-name"
},
'preserve_mapped_metadata': False}
+
self.pollster_definition_all_fields.update(
self.pollster_definition_only_required_fields)
+ self.multi_metric_pollster_definition = {
+ 'name': "test-pollster.{category}", 'sample_type': "gauge",
+ 'unit': "test", 'value_attribute': "[categories].ops",
+ 'endpoint_type': "test", 'url_path': "v1/test/endpoint/fake"}
+
def execute_basic_asserts(self, pollster, pollster_definition):
self.assertEqual(pollster, pollster.obj)
self.assertEqual(pollster_definition['name'], pollster.name)
- for key in pollster.REQUIRED_POLLSTER_FIELDS:
+ for key in REQUIRED_POLLSTER_FIELDS:
self.assertEqual(pollster_definition[key],
pollster.pollster_definitions[key])
self.assertEqual(pollster_definition, pollster.pollster_definitions)
+ @mock.patch('keystoneclient.v2_0.client.Client')
+ def test_skip_samples(self, keystone_mock):
+ generator = PagedSamplesGeneratorHttpRequestMock(samples_dict={
+ 'volume': SampleGenerator(samples_dict={
+ 'name': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
+ 'tmp': ['ra', 'rb', 'rc', 'rd', 're', 'rf', 'rg', 'rh']},
+ turn_to_list=True),
+ 'id': [1, 2, 3, 4, 5, 6, 7, 8],
+ 'name': ['a1', 'b2', 'c3', 'd4', 'e5', 'f6', 'g7', 'h8']
+ }, dict_name='servers', page_link_name='server_link')
+
+ generator.generate_samples('http://test.com/v1/test-volumes', {
+ 'marker=c3': 3,
+ 'marker=f6': 3
+ }, 2)
+
+ keystone_mock.session.get.side_effect = generator.mock_request
+ fake_manager = self.FakeManager(keystone=keystone_mock)
+
+ pollster_definition = dict(self.multi_metric_pollster_definition)
+ pollster_definition['name'] = 'test-pollster.{name}'
+ pollster_definition['value_attribute'] = '[volume].tmp'
+ pollster_definition['skip_sample_values'] = ['rb']
+ pollster_definition['url_path'] = 'v1/test-volumes'
+ pollster_definition['response_entries_key'] = 'servers'
+ pollster = dynamic_pollster.DynamicPollster(pollster_definition)
+ samples = pollster.get_samples(fake_manager, None, ['http://test.com'])
+ self.assertEqual(['ra', 'rc'], list(map(lambda s: s.volume, samples)))
+
+ pollster_definition['name'] = 'test-pollster'
+ pollster_definition['value_attribute'] = 'name'
+ pollster_definition['skip_sample_values'] = ['b2']
+ pollster = dynamic_pollster.DynamicPollster(pollster_definition)
+ samples = pollster.get_samples(fake_manager, None, ['http://test.com'])
+ self.assertEqual(['a1', 'c3'], list(map(lambda s: s.volume, samples)))
+
def test_all_required_fields_ok(self):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
@@ -112,8 +230,7 @@ class TestDynamicPollster(base.BaseTestCase):
False, pollster.pollster_definitions['preserve_mapped_metadata'])
def test_all_required_fields_exceptions(self):
- for key in dynamic_pollster.\
- DynamicPollster.REQUIRED_POLLSTER_FIELDS:
+ for key in REQUIRED_POLLSTER_FIELDS:
pollster_definition = copy.deepcopy(
self.pollster_definition_only_required_fields)
pollster_definition.pop(key)
@@ -148,7 +265,8 @@ class TestDynamicPollster(base.BaseTestCase):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- self.assertEqual("endpoint:test", pollster.default_discovery)
+ self.assertEqual("endpoint:test", pollster.definitions.sample_gatherer
+ .default_discovery)
@mock.patch('keystoneclient.v2_0.client.Client')
def test_execute_request_get_samples_empty_response(self, client_mock):
@@ -161,9 +279,10 @@ class TestDynamicPollster(base.BaseTestCase):
client_mock.session.get.return_value = return_value
- samples = pollster.execute_request_get_samples(
- keystone_client=client_mock,
- resource="https://endpoint.server.name/")
+ samples = pollster.definitions.sample_gatherer. \
+ execute_request_get_samples(
+ keystone_client=client_mock,
+ resource="https://endpoint.server.name/")
self.assertEqual(0, len(samples))
@@ -179,9 +298,10 @@ class TestDynamicPollster(base.BaseTestCase):
client_mock.session.get.return_value = return_value
- samples = pollster.execute_request_get_samples(
- keystone_client=client_mock,
- resource="https://endpoint.server.name/")
+ samples = pollster.definitions.sample_gatherer. \
+ execute_request_get_samples(
+ keystone_client=client_mock,
+ resource="https://endpoint.server.name/")
self.assertEqual(3, len(samples))
@@ -197,7 +317,8 @@ class TestDynamicPollster(base.BaseTestCase):
client_mock.session.get.return_value = return_value
exception = self.assertRaises(requests.HTTPError,
- pollster.execute_request_get_samples,
+ pollster.definitions.sample_gatherer.
+ execute_request_get_samples,
keystone_client=client_mock,
resource="https://endpoint.server.name/")
self.assertEqual("Mock HTTP error.", str(exception))
@@ -211,7 +332,8 @@ class TestDynamicPollster(base.BaseTestCase):
self.pollster_definition_only_required_fields['metadata_mapping'] = {}
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- pollster.generate_new_metadata_fields(metadata)
+ pollster.definitions.sample_extractor.generate_new_metadata_fields(
+ metadata, self.pollster_definition_only_required_fields)
self.assertEqual(metadata_before_call, metadata)
@@ -227,7 +349,8 @@ class TestDynamicPollster(base.BaseTestCase):
'preserve_mapped_metadata'] = True
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- pollster.generate_new_metadata_fields(metadata)
+ pollster.definitions.sample_extractor.generate_new_metadata_fields(
+ metadata, self.pollster_definition_only_required_fields)
self.assertEqual(expected_metadata, metadata)
@@ -244,7 +367,8 @@ class TestDynamicPollster(base.BaseTestCase):
'preserve_mapped_metadata'] = False
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- pollster.generate_new_metadata_fields(metadata)
+ pollster.definitions.sample_extractor.generate_new_metadata_fields(
+ metadata, self.pollster_definition_only_required_fields)
self.assertEqual(expected_clean_metadata, metadata)
@@ -255,7 +379,8 @@ class TestDynamicPollster(base.BaseTestCase):
value_to_be_mapped = "test"
expected_value = value_to_be_mapped
- value = pollster.execute_value_mapping(value_to_be_mapped)
+ value = pollster.definitions.value_mapper. \
+ execute_value_mapping(value_to_be_mapped)
self.assertEqual(expected_value, value)
@@ -267,7 +392,8 @@ class TestDynamicPollster(base.BaseTestCase):
value_to_be_mapped = "test"
expected_value = -1
- value = pollster.execute_value_mapping(value_to_be_mapped)
+ value = pollster.definitions.value_mapper. \
+ execute_value_mapping(value_to_be_mapped)
self.assertEqual(expected_value, value)
@@ -282,7 +408,8 @@ class TestDynamicPollster(base.BaseTestCase):
value_to_be_mapped = "test"
expected_value = 0
- value = pollster.execute_value_mapping(value_to_be_mapped)
+ value = pollster.definitions.value_mapper. \
+ execute_value_mapping(value_to_be_mapped)
self.assertEqual(expected_value, value)
@@ -294,7 +421,8 @@ class TestDynamicPollster(base.BaseTestCase):
value_to_be_mapped = "test"
expected_value = 'new-value'
- value = pollster.execute_value_mapping(value_to_be_mapped)
+ value = pollster.definitions.value_mapper. \
+ execute_value_mapping(value_to_be_mapped)
self.assertEqual(expected_value, value)
@@ -306,7 +434,7 @@ class TestDynamicPollster(base.BaseTestCase):
self.assertEqual(None, next(samples))
@mock.patch('ceilometer.polling.dynamic_pollster.'
- 'DynamicPollster.execute_request_get_samples')
+ 'PollsterSampleGatherer.execute_request_get_samples')
def test_get_samples_empty_samples(self, execute_request_get_samples_mock):
execute_request_get_samples_mock.side_effect = []
@@ -346,7 +474,7 @@ class TestDynamicPollster(base.BaseTestCase):
return samples_list
@mock.patch.object(
- dynamic_pollster.DynamicPollster,
+ dynamic_pollster.PollsterSampleGatherer,
'execute_request_get_samples',
fake_sample_list)
def test_get_samples(self):
@@ -383,7 +511,8 @@ class TestDynamicPollster(base.BaseTestCase):
self.pollster_definition_only_required_fields)
response = [{"object1-attr1": 1}, {"object1-attr2": 2}]
- entries = pollster.retrieve_entries_from_response(response)
+ entries = pollster.definitions.sample_gatherer. \
+ retrieve_entries_from_response(response)
self.assertEqual(response, entries)
@@ -401,7 +530,8 @@ class TestDynamicPollster(base.BaseTestCase):
response = {"first": first_entries_from_response,
"second": second_entries_from_response}
- entries = pollster.retrieve_entries_from_response(response)
+ entries = pollster.definitions.sample_gatherer. \
+ retrieve_entries_from_response(response)
self.assertEqual(first_entries_from_response, entries)
@@ -418,7 +548,8 @@ class TestDynamicPollster(base.BaseTestCase):
response = {"first": first_entries_from_response,
"second": second_entries_from_response}
- entries = pollster.retrieve_entries_from_response(response)
+ entries = pollster.definitions.sample_gatherer. \
+ retrieve_entries_from_response(response)
self.assertEqual(second_entries_from_response, entries)
@@ -431,8 +562,8 @@ class TestDynamicPollster(base.BaseTestCase):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- returned_value = pollster.retrieve_attribute_nested_value(
- json_object, key)
+ returned_value = pollster.definitions.sample_extractor.\
+ retrieve_attribute_nested_value(json_object, key)
self.assertEqual(value, returned_value)
@@ -447,8 +578,8 @@ class TestDynamicPollster(base.BaseTestCase):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- returned_value = pollster.retrieve_attribute_nested_value(
- json_object, key)
+ returned_value = pollster.definitions.sample_extractor. \
+ retrieve_attribute_nested_value(json_object, key)
self.assertEqual(sub_value, returned_value)
@@ -465,8 +596,8 @@ class TestDynamicPollster(base.BaseTestCase):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- returned_value = pollster.retrieve_attribute_nested_value(
- json_object, key)
+ returned_value = pollster.definitions.sample_extractor.\
+ retrieve_attribute_nested_value(json_object, key)
self.assertEqual(expected_value_after_operations, returned_value)
@@ -496,7 +627,83 @@ class TestDynamicPollster(base.BaseTestCase):
pollster = dynamic_pollster.DynamicPollster(
self.pollster_definition_only_required_fields)
- returned_value = pollster.retrieve_attribute_nested_value(json_object,
- key)
+ returned_value = pollster.definitions.sample_extractor.\
+ retrieve_attribute_nested_value(json_object, key)
self.assertEqual(expected_value_after_operations, returned_value)
+
+ def fake_sample_multi_metric(self, keystone_client=None, resource=None):
+ multi_metric_sample_list = [
+ {"categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "create_bucket",
+ "ops": 2,
+ "successful_ops": 2
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 2120428,
+ "category": "get_obj",
+ "ops": 46,
+ "successful_ops": 46
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 21484,
+ "category": "list_bucket",
+ "ops": 8,
+ "successful_ops": 8
+ },
+ {
+ "bytes_received": 6889056,
+ "bytes_sent": 0,
+ "category": "put_obj",
+ "ops": 46,
+ "successful_ops": 6
+ }],
+ "total": {
+ "bytes_received": 6889056,
+ "bytes_sent": 2141912,
+ "ops": 102,
+ "successful_ops": 106
+ },
+ "user": "test-user"}]
+ return multi_metric_sample_list
+
+ @mock.patch.object(
+ dynamic_pollster.PollsterSampleGatherer,
+ 'execute_request_get_samples',
+ fake_sample_multi_metric)
+ def test_get_samples_multi_metric_pollster(self):
+ pollster = dynamic_pollster.DynamicPollster(
+ self.multi_metric_pollster_definition)
+
+ fake_manager = self.FakeManager()
+ samples = pollster.get_samples(
+ fake_manager, None, ["https://endpoint.server.name.com/"])
+
+ samples_list = list(samples)
+ self.assertEqual(4, len(samples_list))
+
+ create_bucket_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.create_bucket"][0]
+
+ get_obj_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.get_obj"][0]
+
+ list_bucket_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.list_bucket"][0]
+
+ put_obj_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.put_obj"][0]
+
+ self.assertEqual(2, create_bucket_sample.volume)
+ self.assertEqual(46, get_obj_sample.volume)
+ self.assertEqual(8, list_bucket_sample.volume)
+ self.assertEqual(46, put_obj_sample.volume)
diff --git a/ceilometer/tests/unit/polling/test_manager.py b/ceilometer/tests/unit/polling/test_manager.py
index 3ac7af7c..322bb3ad 100644
--- a/ceilometer/tests/unit/polling/test_manager.py
+++ b/ceilometer/tests/unit/polling/test_manager.py
@@ -27,9 +27,10 @@ from stevedore import extension
from ceilometer.compute import discovery as nova_discover
from ceilometer.hardware import discovery
from ceilometer.polling.dynamic_pollster import DynamicPollster
+from ceilometer.polling.dynamic_pollster import \
+ NonOpenStackApisPollsterDefinition
+from ceilometer.polling.dynamic_pollster import SingleMetricPollsterDefinitions
from ceilometer.polling import manager
-from ceilometer.polling.non_openstack_dynamic_pollster import \
- NonOpenStackApisDynamicPollster
from ceilometer.polling import plugin_base
from ceilometer import sample
from ceilometer import service
@@ -900,10 +901,10 @@ class TestPollingAgentPartitioned(BaseAgent):
'name': "test-pollster", 'sample_type': "gauge", 'unit': "test",
'value_attribute': "volume", 'endpoint_type': "test",
'url_path': "v1/test/endpoint/fake"}
- pollster = self.mgr.instantiate_dynamic_pollster(
- pollster_definition_only_required_fields)
+ pollster = DynamicPollster(pollster_definition_only_required_fields)
- self.assertIsInstance(pollster, DynamicPollster)
+ self.assertIsInstance(pollster.definitions,
+ SingleMetricPollsterDefinitions)
def test_instantiate_dynamic_pollster_non_openstack_api(self):
pollster_definition_only_required_fields = {
@@ -911,7 +912,7 @@ class TestPollingAgentPartitioned(BaseAgent):
'value_attribute': "volume",
'url_path': "v1/test/endpoint/fake", 'module': "module-name",
'authentication_object': "authentication_object"}
- pollster = self.mgr.instantiate_dynamic_pollster(
- pollster_definition_only_required_fields)
+ pollster = DynamicPollster(pollster_definition_only_required_fields)
- self.assertIsInstance(pollster, NonOpenStackApisDynamicPollster)
+ self.assertIsInstance(pollster.definitions,
+ NonOpenStackApisPollsterDefinition)
diff --git a/ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py b/ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py
index f0a35b64..2a4d269e 100644
--- a/ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py
+++ b/ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py
@@ -22,13 +22,76 @@ import requests
from ceilometer.declarative import DynamicPollsterDefinitionException
from ceilometer.declarative import NonOpenStackApisDynamicPollsterException
from ceilometer.polling.dynamic_pollster import DynamicPollster
-from ceilometer.polling.non_openstack_dynamic_pollster\
- import NonOpenStackApisDynamicPollster
+from ceilometer.polling.dynamic_pollster import MultiMetricPollsterDefinitions
+from ceilometer.polling.dynamic_pollster import \
+ NonOpenStackApisPollsterDefinition
+from ceilometer.polling.dynamic_pollster import PollsterSampleGatherer
+from ceilometer.polling.dynamic_pollster import SingleMetricPollsterDefinitions
+
from oslotest import base
+REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit', 'value_attribute',
+ 'url_path', 'module', 'authentication_object']
+
+OPTIONAL_POLLSTER_FIELDS = ['metadata_fields', 'skip_sample_values',
+ 'value_mapping', 'default_value',
+ 'metadata_mapping', 'preserve_mapped_metadata',
+ 'response_entries_key', 'user_id_attribute',
+ 'resource_id_attribute', 'barbican_secret_id',
+ 'authentication_parameters',
+ 'project_id_attribute']
+
+ALL_POLLSTER_FIELDS = REQUIRED_POLLSTER_FIELDS + OPTIONAL_POLLSTER_FIELDS
+
+
+def fake_sample_multi_metric(self, keystone_client=None, resource=None):
+ multi_metric_sample_list = [
+ {"user_id": "UID-U007",
+ "project_id": "UID-P007",
+ "id": "UID-007",
+ "categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "create_bucket",
+ "ops": 2,
+ "successful_ops": 2
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 2120428,
+ "category": "get_obj",
+ "ops": 46,
+ "successful_ops": 46
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 21484,
+ "category": "list_bucket",
+ "ops": 8,
+ "successful_ops": 8
+ },
+ {
+ "bytes_received": 6889056,
+ "bytes_sent": 0,
+ "category": "put_obj",
+ "ops": 46,
+ "successful_ops": 6
+ }],
+ "total": {
+ "bytes_received": 6889056,
+ "bytes_sent": 2141912,
+ "ops": 102,
+ "successful_ops": 106
+ },
+ "user": "test-user"}]
+ return multi_metric_sample_list
+
class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
+ class FakeManager(object):
+ _keystone = None
class FakeResponse(object):
status_code = None
@@ -42,6 +105,16 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
def setUp(self):
super(TestNonOpenStackApisDynamicPollster, self).setUp()
+ self.pollster_definition_only_openstack_required_single_metric = {
+ 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test",
+ 'value_attribute': "volume", "endpoint_type": "type",
+ 'url_path': "v1/test/endpoint/fake"}
+
+ self.pollster_definition_only_openstack_required_multi_metric = {
+ 'name': "test-pollster.{category}", 'sample_type': "gauge",
+ 'unit': "test", 'value_attribute': "[categories].ops",
+ 'url_path': "v1/test/endpoint/fake", "endpoint_type": "type"}
+
self.pollster_definition_only_required_fields = {
'name': "test-pollster", 'sample_type': "gauge", 'unit': "test",
'value_attribute': "volume",
@@ -58,10 +131,17 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
'resource_id_attribute': 'id', 'barbican_secret_id': 'barbican_id',
'authentication_parameters': 'parameters'}
- def test_all_fields(self):
- pollster = NonOpenStackApisDynamicPollster(
- self.pollster_definition_only_required_fields)
+ self.pollster_definition_all_fields_multi_metrics = {
+ 'name': "test-pollster.{category}", 'sample_type': "gauge",
+ 'unit': "test", 'value_attribute': "[categories].ops",
+ 'url_path': "v1/test/endpoint/fake", 'module': "module-name",
+ 'authentication_object': "authentication_object",
+ 'user_id_attribute': 'user_id',
+ 'project_id_attribute': 'project_id',
+ 'resource_id_attribute': 'id', 'barbican_secret_id': 'barbican_id',
+ 'authentication_parameters': 'parameters'}
+ def test_all_fields(self):
all_required = ['module', 'authentication_object', 'name',
'sample_type', 'unit', 'value_attribute',
'url_path']
@@ -74,27 +154,27 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
'response_entries_key'] + all_required
for field in all_required:
- self.assertIn(field, pollster.REQUIRED_POLLSTER_FIELDS)
+ self.assertIn(field, REQUIRED_POLLSTER_FIELDS)
for field in all_optional:
- self.assertIn(field, pollster.ALL_POLLSTER_FIELDS)
+ self.assertIn(field, ALL_POLLSTER_FIELDS)
def test_all_required_fields_exceptions(self):
- pollster = NonOpenStackApisDynamicPollster(
- self.pollster_definition_only_required_fields)
-
- for key in pollster.REQUIRED_POLLSTER_FIELDS:
+ for key in REQUIRED_POLLSTER_FIELDS:
+ if key == 'module':
+ continue
pollster_definition = copy.deepcopy(
self.pollster_definition_only_required_fields)
pollster_definition.pop(key)
- exception = self.assertRaises(DynamicPollsterDefinitionException,
- NonOpenStackApisDynamicPollster,
- pollster_definition)
+ exception = self.assertRaises(
+ DynamicPollsterDefinitionException, DynamicPollster,
+ pollster_definition, None,
+ [NonOpenStackApisPollsterDefinition])
self.assertEqual("Required fields ['%s'] not specified."
% key, exception.brief_message)
def test_set_default_values(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
pollster_definitions = pollster.pollster_definitions
@@ -106,7 +186,7 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
self.assertEqual('', pollster_definitions['authentication_parameters'])
def test_user_set_optional_parameters(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_all_fields)
pollster_definitions = pollster.pollster_definitions
@@ -122,23 +202,25 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
pollster_definitions['authentication_parameters'])
def test_default_discovery_empty_secret_id(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
- self.assertEqual("barbican:", pollster.default_discovery)
+ self.assertEqual("barbican:", pollster.definitions.sample_gatherer.
+ default_discovery)
def test_default_discovery_not_empty_secret_id(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_all_fields)
- self.assertEqual("barbican:barbican_id", pollster.default_discovery)
+ self.assertEqual("barbican:barbican_id", pollster.definitions.
+ sample_gatherer.default_discovery)
@mock.patch('requests.get')
def test_internal_execute_request_get_samples_status_code_ok(
self, get_mock):
sys.modules['module-name'] = mock.MagicMock()
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
return_value = self.FakeResponse()
@@ -150,7 +232,8 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
kwargs = {'resource': "credentials"}
- resp, url = pollster.internal_execute_request_get_samples(kwargs)
+ resp, url = pollster.definitions.sample_gatherer. \
+ internal_execute_request_get_samples(kwargs)
self.assertEqual(
self.pollster_definition_only_required_fields['url_path'], url)
@@ -161,7 +244,7 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
self, get_mock):
sys.modules['module-name'] = mock.MagicMock()
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
for http_status_code in requests.status_codes._codes.keys():
@@ -177,7 +260,8 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
kwargs = {'resource': "credentials"}
exception = self.assertRaises(
NonOpenStackApisDynamicPollsterException,
- pollster.internal_execute_request_get_samples, kwargs)
+ pollster.definitions.sample_gatherer.
+ internal_execute_request_get_samples, kwargs)
self.assertEqual(
"NonOpenStackApisDynamicPollsterException"
@@ -188,25 +272,28 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
http_status_code, return_value.reason), str(exception))
def test_generate_new_attributes_in_sample_attribute_key_none(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
sample = {"test": "2"}
new_key = "new-key"
- pollster.generate_new_attributes_in_sample(sample, None, new_key)
- pollster.generate_new_attributes_in_sample(sample, "", new_key)
+ pollster.definitions.sample_gatherer. \
+ generate_new_attributes_in_sample(sample, None, new_key)
+ pollster.definitions.sample_gatherer. \
+ generate_new_attributes_in_sample(sample, "", new_key)
self.assertNotIn(new_key, sample)
def test_generate_new_attributes_in_sample(self):
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_only_required_fields)
sample = {"test": "2"}
new_key = "new-key"
- pollster.generate_new_attributes_in_sample(sample, "test", new_key)
+ pollster.definitions.sample_gatherer. \
+ generate_new_attributes_in_sample(sample, "test", new_key)
self.assertIn(new_key, sample)
self.assertEqual(sample["test"], sample[new_key])
@@ -220,7 +307,7 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
samples = [sample]
return samples
- DynamicPollster.execute_request_get_samples =\
+ PollsterSampleGatherer.execute_request_get_samples = \
execute_request_get_samples_mock
self.pollster_definition_all_fields[
@@ -230,11 +317,12 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
self.pollster_definition_all_fields[
'resource_id_attribute'] = 'resource_id_attribute'
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_all_fields)
params = {"d": "d"}
- response = pollster.execute_request_get_samples(**params)
+ response = pollster.definitions.sample_gatherer. \
+ execute_request_get_samples(**params)
self.assertEqual(sample['user_id_attribute'],
response[0]['user_id'])
@@ -252,7 +340,7 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
samples = [sample]
return samples
- DynamicPollster.execute_request_get_samples =\
+ DynamicPollster.execute_request_get_samples = \
execute_request_get_samples_mock
self.pollster_definition_all_fields[
@@ -262,7 +350,7 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
self.pollster_definition_all_fields[
'resource_id_attribute'] = None
- pollster = NonOpenStackApisDynamicPollster(
+ pollster = DynamicPollster(
self.pollster_definition_all_fields)
params = {"d": "d"}
@@ -271,3 +359,67 @@ class TestNonOpenStackApisDynamicPollster(base.BaseTestCase):
self.assertNotIn('user_id', response[0])
self.assertNotIn('project_id', response[0])
self.assertNotIn('id', response[0])
+
+ def test_pollster_defintions_instantiation(self):
+ def validate_definitions_instance(instance, isNonOpenstack,
+ isMultiMetric, isSingleMetric):
+ self.assertIs(
+ isinstance(instance, NonOpenStackApisPollsterDefinition),
+ isNonOpenstack)
+ self.assertIs(isinstance(instance, MultiMetricPollsterDefinitions),
+ isMultiMetric)
+ self.assertIs(
+ isinstance(instance, SingleMetricPollsterDefinitions),
+ isSingleMetric)
+
+ pollster = DynamicPollster(
+ self.pollster_definition_all_fields_multi_metrics)
+ validate_definitions_instance(pollster.definitions, True, True, False)
+
+ pollster = DynamicPollster(
+ self.pollster_definition_all_fields)
+ validate_definitions_instance(pollster.definitions, True, False, True)
+
+ pollster = DynamicPollster(
+ self.pollster_definition_only_openstack_required_multi_metric)
+ validate_definitions_instance(pollster.definitions, False, True, False)
+
+ pollster = DynamicPollster(
+ self.pollster_definition_only_openstack_required_single_metric)
+ validate_definitions_instance(pollster.definitions, False, False, True)
+
+ @mock.patch.object(
+ PollsterSampleGatherer,
+ 'execute_request_get_samples',
+ fake_sample_multi_metric)
+ def test_get_samples_multi_metric_pollster(self):
+ pollster = DynamicPollster(
+ self.pollster_definition_all_fields_multi_metrics)
+
+ fake_manager = self.FakeManager()
+ samples = pollster.get_samples(
+ fake_manager, None, ["https://endpoint.server.name.com/"])
+
+ samples_list = list(samples)
+ self.assertEqual(4, len(samples_list))
+
+ create_bucket_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.create_bucket"][0]
+
+ get_obj_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.get_obj"][0]
+
+ list_bucket_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.list_bucket"][0]
+
+ put_obj_sample = [
+ s for s in samples_list
+ if s.name == "test-pollster.put_obj"][0]
+
+ self.assertEqual(2, create_bucket_sample.volume)
+ self.assertEqual(46, get_obj_sample.volume)
+ self.assertEqual(8, list_bucket_sample.volume)
+ self.assertEqual(46, put_obj_sample.volume)
diff --git a/doc/source/admin/telemetry-dynamic-pollster.rst b/doc/source/admin/telemetry-dynamic-pollster.rst
index 2c829836..0ea50ace 100644
--- a/doc/source/admin/telemetry-dynamic-pollster.rst
+++ b/doc/source/admin/telemetry-dynamic-pollster.rst
@@ -376,3 +376,210 @@ following:
* user_id_attribute
* project_id_attribute
* resource_id_attribute
+
+Multi metric dynamic pollsters (handling attribute values with list of objects)
+-------------------------------------------------------------------------------
+
+The initial idea for this feature comes from the `categories` fields that we
+can find in the `summary` object of the RadosGW API. Each user has a
+`categories` attribute in the response; in the `categories` list, we can find
+the object that presents in a granular fashion the consumption of different
+RadosGW API operations such as GET, PUT, POST, and may others.
+
+As follows we present an example of such a JSON response.
+
+.. code-block:: json
+
+ {
+ "entries": [
+ {
+ "buckets": [
+ {
+ "bucket": "",
+ "categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 40,
+ "category": "list_buckets",
+ "ops": 2,
+ "successful_ops": 2
+ }
+ ],
+ "epoch": 1572969600,
+ "owner": "user",
+ "time": "2019-11-21 00:00:00.000000Z"
+ },
+ {
+ "bucket": "-",
+ "categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "get_obj",
+ "ops": 1,
+ "successful_ops": 0
+ }
+ ],
+ "epoch": 1572969600,
+ "owner": "someOtherUser",
+ "time": "2019-11-21 00:00:00.000000Z"
+ }
+ ]
+ }
+ ]
+ "summary": [
+ {
+ "categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "create_bucket",
+ "ops": 2,
+ "successful_ops": 2
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 2120428,
+ "category": "get_obj",
+ "ops": 46,
+ "successful_ops": 46
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 21484,
+ "category": "list_bucket",
+ "ops": 8,
+ "successful_ops": 8
+ },
+ {
+ "bytes_received": 6889056,
+ "bytes_sent": 0,
+ "category": "put_obj",
+ "ops": 46,
+ "successful_ops": 46
+ }
+ ],
+ "total": {
+ "bytes_received": 6889056,
+ "bytes_sent": 2141912,
+ "ops": 102,
+ "successful_ops": 102
+ },
+ "user": "user"
+ },
+ {
+ "categories": [
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "create_bucket",
+ "ops": 1,
+ "successful_ops": 1
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "category": "delete_obj",
+ "ops": 23,
+ "successful_ops": 23
+ },
+ {
+ "bytes_received": 0,
+ "bytes_sent": 5371,
+ "category": "list_bucket",
+ "ops": 2,
+ "successful_ops": 2
+ },
+ {
+ "bytes_received": 3444350,
+ "bytes_sent": 0,
+ "category": "put_obj",
+ "ops": 23,
+ "successful_ops": 23
+ }
+ ],
+ "total": {
+ "bytes_received": 3444350,
+ "bytes_sent": 5371,
+ "ops": 49,
+ "successful_ops": 49
+ },
+ "user": "someOtherUser"
+ }
+ ]
+ }
+
+In that context, and having in mind that we have APIs with similar data
+structures, we developed an extension for the dynamic pollster that enables
+multi-metric processing for a single pollster. It works as follows.
+
+The pollster name will contain a placeholder for the variable that
+identifies the "submetric". E.g. `dynamic.radosgw.api.request.{category}`.
+The placeholder `{category}` indicates the object's attribute that is in the
+list of objects that we use to load the sub metric name. Then, we must use a
+special notation in the `value_attribute` configuration to indicate that we are
+dealing with a list of objects. This is achieved via `[]` (brackets); for
+instance, in the `dynamic.radosgw.api.request.{category}`, we can use
+`[categories].ops` as the `value_attribute`. This indicates that the value we
+retrieve is a list of objects, and when the dynamic pollster processes it, we
+want it (the pollster) to load the `ops` value for the sub metrics being
+generated.
+
+Examples on how to create multi-metric pollster to handle data from RadosGW API
+are presented as follows:
+
+.. code-block:: yaml
+
+ ---
+
+ - name: "dynamic.radosgw.api.request.{category}"
+ sample_type: "gauge"
+ unit: "request"
+ value_attribute: "[categories].ops"
+ url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage"
+ module: "awsauth"
+ authentication_object: "S3Auth"
+ authentication_parameters: "<access_key>, <secret_key>,<rados_gateway_server>"
+ user_id_attribute: "user | value.split('$')[0]"
+ project_id_attribute: "user | value.split('$') | value[0]"
+ resource_id_attribute: "user | value.split('$') | value[0]"
+ response_entries_key: "summary"
+
+ - name: "dynamic.radosgw.api.request.successful_ops.{category}"
+ sample_type: "gauge"
+ unit: "request"
+ value_attribute: "[categories].successful_ops"
+ url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage"
+ module: "awsauth"
+ authentication_object: "S3Auth"
+ authentication_parameters: "<access_key>, <secret_key>,<rados_gateway_server>"
+ user_id_attribute: "user | value.split('$')[0]"
+ project_id_attribute: "user | value.split('$') | value[0]"
+ resource_id_attribute: "user | value.split('$') | value[0]"
+ response_entries_key: "summary"
+
+ - name: "dynamic.radosgw.api.bytes_sent.{category}"
+ sample_type: "gauge"
+ unit: "request"
+ value_attribute: "[categories].bytes_sent"
+ url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage"
+ module: "awsauth"
+ authentication_object: "S3Auth"
+ authentication_parameters: "<access_key>, <secret_key>,<rados_gateway_server>"
+ user_id_attribute: "user | value.split('$')[0]"
+ project_id_attribute: "user | value.split('$') | value[0]"
+ resource_id_attribute: "user | value.split('$') | value[0]"
+ response_entries_key: "summary"
+
+ - name: "dynamic.radosgw.api.bytes_received.{category}"
+ sample_type: "gauge"
+ unit: "request"
+ value_attribute: "[categories].bytes_received"
+ url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage"
+ module: "awsauth"
+ authentication_object: "S3Auth"
+ authentication_parameters: "<access_key>, <secret_key>,<rados_gateway_server>"
+ user_id_attribute: "user | value.split('$')[0]"
+ project_id_attribute: "user | value.split('$') | value[0]"
+ resource_id_attribute: "user | value.split('$') | value[0]"
+ response_entries_key: "summary"