diff --git a/ceilometer/event/trait_plugins.py b/ceilometer/event/trait_plugins.py index 5d94f6c5d5..9c0a4412d7 100644 --- a/ceilometer/event/trait_plugins.py +++ b/ceilometer/event/trait_plugins.py @@ -217,3 +217,58 @@ class TimedeltaPlugin(TraitPluginBase): ) return [None] return [abs((end_time - start_time).total_seconds())] + + +class MapTraitPlugin(TraitPluginBase): + """A trait plugin for mapping one set of values to another.""" + + def __init__(self, values=None, default=None, case_sensitive=True, **kw): + """Setup map trait. + + :param values: (dict[Any, Any]) Mapping of values to their + desired target values. + :param default: (Any) Value to set if no mapping for a value is found. + :param case_sensitive: (bool) Perform case-sensitive string lookups. + """ + if not values: + raise ValueError(("The 'values' parameter is required " + "for the map trait plugin")) + if not isinstance(values, dict): + raise ValueError(("The 'values' parameter needs to be a dict " + "for the map trait plugin")) + self.case_sensitive = case_sensitive + if not self.case_sensitive: + self.values = {(k.casefold() + if isinstance(k, str) + else k): v + for k, v in values.items()} + else: + self.values = dict(values) + self.default = default + super(MapTraitPlugin, self).__init__(**kw) + + def trait_values(self, match_list): + mapped_values = [] + for match in match_list: + key = match[1] + folded_key = ( + key.casefold() + if not self.case_sensitive and isinstance(key, str) + else key) + try: + value = self.values[folded_key] + except KeyError: + LOG.warning( + ('Unknown value %s found when mapping %s, ' + 'mapping to default value of %s'), + repr(key), + match[0], + repr(self.default)) + value = self.default + else: + LOG.debug('Value %s for %s mapped to value %s', + repr(key), + match[0], + repr(value)) + mapped_values.append(value) + return mapped_values diff --git a/ceilometer/tests/unit/event/test_trait_plugins.py b/ceilometer/tests/unit/event/test_trait_plugins.py index 80b5ae7827..9552a83be8 100644 --- a/ceilometer/tests/unit/event/test_trait_plugins.py +++ b/ceilometer/tests/unit/event/test_trait_plugins.py @@ -113,3 +113,47 @@ class TestBitfieldPlugin(base.BaseTestCase): plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual(0x412, value[0]) + + +class TestMapTraitPlugin(base.BaseTestCase): + + def setUp(self): + super(TestMapTraitPlugin, self).setUp() + self.pclass = trait_plugins.MapTraitPlugin + self.params = dict(values={'ACTIVE': 1, 'ERROR': 2, 3: 4}, + default=-1) + + def test_map(self): + match_list = [('payload.foo', 'ACTIVE'), + ('payload.bar', 'ERROR'), + ('thingy.boink', 3), + ('thingy.invalid', 999)] + plugin = self.pclass(**self.params) + value = plugin.trait_values(match_list) + self.assertEqual([1, 2, 4, -1], value) + + def test_case_sensitive(self): + match_list = [('payload.foo', 'ACTIVE'), + ('payload.bar', 'error'), + ('thingy.boink', 3), + ('thingy.invalid', 999)] + plugin = self.pclass(case_sensitive=True, **self.params) + value = plugin.trait_values(match_list) + self.assertEqual([1, -1, 4, -1], value) + + def test_case_insensitive(self): + match_list = [('payload.foo', 'active'), + ('payload.bar', 'ErRoR'), + ('thingy.boink', 3), + ('thingy.invalid', 999)] + plugin = self.pclass(case_sensitive=False, **self.params) + value = plugin.trait_values(match_list) + self.assertEqual([1, 2, 4, -1], value) + + def test_values_undefined(self): + self.assertRaises(ValueError, self.pclass) + + def test_values_invalid(self): + self.assertRaises( + ValueError, + lambda: self.pclass(values=[('ACTIVE', 1), ('ERROR', 2), (3, 4)])) diff --git a/releasenotes/notes/add-map-trait-plugin-0d969f5cc7b18175.yaml b/releasenotes/notes/add-map-trait-plugin-0d969f5cc7b18175.yaml new file mode 100644 index 0000000000..8b13f3457e --- /dev/null +++ b/releasenotes/notes/add-map-trait-plugin-0d969f5cc7b18175.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A ``map`` event trait plugin has been added. + This allows notification meter attributes to be created + by mapping one set of values from an attribute to another + set of values defined in the meter definition. + Additional options are also available for controlling + how to handle edge cases, such as unknown values and + case sensitivity. diff --git a/setup.cfg b/setup.cfg index 1f84d13c65..0fda69f911 100644 --- a/setup.cfg +++ b/setup.cfg @@ -165,6 +165,7 @@ ceilometer.event.trait_plugin = split = ceilometer.event.trait_plugins:SplitterTraitPlugin bitfield = ceilometer.event.trait_plugins:BitfieldTraitPlugin timedelta = ceilometer.event.trait_plugins:TimedeltaPlugin + map = ceilometer.event.trait_plugins:MapTraitPlugin console_scripts = ceilometer-polling = ceilometer.cmd.polling:main