Add a map event trait plugin

Add a new map event trait plugin for mapping one set of values
to another.

This is useful, for example, when you wish to express the state
of a resource as the volume for the resulting samples:

metric:
  - name: 'instance'
    event_type: *instance_events
    type: 'gauge'
    unit: 'instance'
    volume:
      fields: [$.payload.state]
      plugin:
        name: map
        parameters:
          values:
            BUILDING: 0  # VM only exists in DB
            ACTIVE: 1  # VM is running
            PAUSED: 2  # VM is suspended to RAM
            SUSPENDED: 3  # VM is suspended to disk
            STOPPED: 4  # VM is temporarily powered off
            RESCUED: 5  # A rescue image is running
            RESIZED: 6  # VM active with a new size is active
            SOFT_DELETED: 7  # VM deleted but not permanently removed
            DELETED: 8  # VM is permanently deleted
            ERROR: 9  # VM is in error state
            SHELVED: 10  # VM is powered off, still on hypervisor
            SHELVED_OFFLOADED: 11  # VM is powered off, off hypervisor
          default: -1
          case_sensitive: false
    user_id: $.payload.user_id
    project_id: $.payload.tenant_id
    resource_id: $.payload.instance_id
    user_metadata: $.payload.metadata
    metadata:
      <<: *instance_meta

Change-Id: I9469cbf8baeec543ffe152b8b5bf07595fa6905a
This commit is contained in:
Callum Dickinson 2025-02-25 10:31:07 +13:00
parent 3a2e49c8c1
commit d1ba90b3c3
4 changed files with 110 additions and 0 deletions

View File

@ -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

View File

@ -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)]))

View File

@ -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.

View File

@ -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