Merge "Ceilometer to export Prometheus metrics"

This commit is contained in:
Zuul 2024-09-30 17:19:33 +00:00 committed by Gerrit Code Review
commit f6ccfcad15
4 changed files with 415 additions and 9 deletions

View File

@ -44,6 +44,7 @@ from ceilometer import keystone_client
from ceilometer import messaging
from ceilometer.polling import dynamic_pollster
from ceilometer.polling import plugin_base
from ceilometer.polling import prom_exporter
from ceilometer.publisher import utils as publisher_utils
from ceilometer import utils
@ -84,6 +85,19 @@ POLLING_OPTS = [
'recommended that ceilometer be configured with a '
'caching backend to reduce the number of calls '
'made to keystone.'),
cfg.BoolOpt('enable_notifications',
default=True,
help='Whether the polling service should be sending '
'notifications to RabbitMQ after polling cycles.'),
cfg.BoolOpt('enable_prometheus_exporter',
default=False,
help='Allow this ceilometer polling instance to '
'expose directly the retrieved metrics in Prometheus '
'format.'),
cfg.ListOpt('prometheus_listen_addresses',
default=["127.0.0.1:9101"],
help='A list of ipaddr:port combinations on which '
'the exported metrics will be exposed.')
]
@ -296,11 +310,14 @@ class PollingTask(object):
exc_info=True)
def _send_notification(self, samples):
self.manager.notifier.sample(
{},
'telemetry.polling',
{'samples': samples}
)
if self.manager.conf.polling.enable_notifications:
self.manager.notifier.sample(
{},
'telemetry.polling',
{'samples': samples}
)
if self.manager.conf.polling.enable_prometheus_exporter:
prom_exporter.collect_metrics(samples)
class AgentHeartBeatManager(cotyledon.Service):
@ -441,10 +458,17 @@ class AgentManager(cotyledon.Service):
self.group_prefix = ('%s-%s' % (namespace_prefix, group_prefix)
if group_prefix else namespace_prefix)
self.notifier = oslo_messaging.Notifier(
messaging.get_transport(self.conf),
driver=self.conf.publisher_notifier.telemetry_driver,
publisher_id="ceilometer.polling")
if self.conf.polling.enable_notifications:
self.notifier = oslo_messaging.Notifier(
messaging.get_transport(self.conf),
driver=self.conf.publisher_notifier.telemetry_driver,
publisher_id="ceilometer.polling")
if self.conf.polling.enable_prometheus_exporter:
for addr in self.conf.polling.prometheus_listen_addresses:
address = addr.split(":")
if len(address) == 2:
prom_exporter.export(address[0], address[1])
self._keystone = None
self._keystone_last_exception = None

View File

@ -0,0 +1,143 @@
#
# Copyright 2024 Juan Larriba
# Copyright 2024 Red Hat, 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 prometheus_client as prom
CEILOMETER_REGISTRY = prom.CollectorRegistry()
def export(prometheus_iface, prometheus_port):
prom.start_http_server(port=int(prometheus_port),
addr=prometheus_iface,
registry=CEILOMETER_REGISTRY)
def collect_metrics(samples):
for sample in samples:
name = "ceilometer_" + sample['counter_name'].replace('.', '_')
type = sample['counter_type']
value = sample['counter_volume']
labels = _gen_labels(sample)
metric = CEILOMETER_REGISTRY._names_to_collectors.get(name, None)
if metric is None:
if type == "cumulative":
metric = prom.Counter(name=name, documentation="",
labelnames=labels['keys'],
registry=CEILOMETER_REGISTRY)
metric.labels(*labels['values']).inc(value)
if type == "gauge" or type == "delta":
metric = prom.Gauge(name=name, documentation="",
labelnames=labels['keys'],
registry=CEILOMETER_REGISTRY)
metric.labels(*labels['values']).set(value)
else:
if type == 'cumulative':
metric.labels(*labels['values']).inc(value)
elif type == 'gauge' or type == 'delta':
metric.labels(*labels['values']).set(value)
def _gen_labels(sample):
labels = dict(keys=[], values=[])
cNameShards = sample['counter_name'].split(".")
ctype = ''
plugin = cNameShards[0]
pluginVal = sample['resource_id']
if len(cNameShards) > 2:
pluginVal = cNameShards[2]
if len(cNameShards) > 1:
ctype = cNameShards[1]
else:
ctype = cNameShards[0]
labels['keys'].append(plugin)
labels['values'].append(pluginVal)
labels['keys'].append("publisher")
labels['values'].append("ceilometer")
labels['keys'].append("type")
labels['values'].append(ctype)
index = 3
if (sample.get('counter_name', '') != '' and
sample.get('counter_name') is not None):
labels['keys'].append("counter")
labels['values'].append(sample['counter_name'])
index += 1
if (sample.get('project_id', '') != '' and
sample.get('project_id') is not None):
labels['keys'].append("project")
labels['values'].append(sample['project_id'])
index += 1
if (sample.get('project_name', '') != '' and
sample.get('project_name') is not None):
labels['keys'].append("project_name")
labels['values'].append(sample['project_name'])
index += 1
if (sample.get('user_id', '') != '' and
sample.get('user_id') is not None):
labels['keys'].append("user")
labels['values'].append(sample['user_id'])
index += 1
if (sample.get('user_name', '') != '' and
sample.get('user_name') is not None):
labels['keys'].append("user_name")
labels['values'].append(sample['user_name'])
index += 1
if (sample.get('counter_unit', '') != '' and
sample.get('counter_unit') is not None):
labels['keys'].append("unit")
labels['values'].append(sample['counter_unit'])
index += 1
if (sample.get('resource_id', '') != '' and
sample.get('resource_id') is not None):
labels['keys'].append("resource")
labels['values'].append(sample['resource_id'])
index += 1
if (sample.get('resource_metadata', '') != '' and
sample.get('resource_metadata') is not None):
if (sample['resource_metadata'].get('host', '') != ''):
labels['keys'].append("vm_instance")
labels['values'].append(sample['resource_metadata']['host'])
index += 1
if (sample['resource_metadata'].get('display_name', '') != ''):
labels['keys'].append("resource_name")
labels['values'].append(sample['resource_metadata']
['display_name'])
if (sample['resource_metadata'].get('name', '') != ''):
labels['keys'].append("resource_name")
if (labels['values'][index] if index < len(labels['values'])
else '' != ''):
labels['values'].append(labels['values'][index] + ":" +
sample['resource_metadata']['name'])
else:
labels['values'].append(sample['resource_metadata']['name'])
return labels

View File

@ -0,0 +1,238 @@
#
# Copyright 2022 Red Hat, 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.
"""Tests for ceilometer/polling/prom_exporter.py"""
from oslotest import base
from unittest import mock
from unittest.mock import call
from ceilometer.polling import manager
from ceilometer.polling import prom_exporter
from ceilometer import service
COUNTER_SOURCE = 'testsource'
class TestPromExporter(base.BaseTestCase):
test_data = [
{
'source': 'openstack',
'counter_name': 'disk.device.read.latency',
'counter_type': 'cumulative',
'counter_unit': 'ns',
'counter_volume': 132128682,
'user_id': '6e7d71415cd5401cbe103829c9c5dec2',
'user_name': None,
'project_id': 'd965489b7f894cbda89cd2e25bfd85a0',
'project_name': None,
'resource_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda',
'timestamp': '2024-06-20T09:32:36.521082',
'resource_metadata': {
'display_name': 'myserver',
'name': 'instance-00000002',
'instance_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63',
'instance_type': 'tiny',
'host': 'e0d297f5df3b62ec73c8d42b',
'instance_host': 'devstack',
'flavor': {
'id': '4af9ac72-5787-4f86-8644-0faa87ce7c83',
'name': 'tiny',
'vcpus': 1,
'ram': 512,
'disk': 1,
'ephemeral': 0,
'swap': 0
},
'status': 'active',
'state': 'running',
'task_state': '',
'image': {
'id': '71860ed5-f66d-43e0-9514-f1d188106284'
},
'image_ref': '71860ed5-f66d-43e0-9514-f1d188106284',
'image_ref_url': None,
'architecture': 'x86_64',
'os_type': 'hvm',
'vcpus': 1,
'memory_mb': 512,
'disk_gb': 1,
'ephemeral_gb': 0,
'root_gb': 1,
'disk_name': 'vda'
},
'message_id': '078029c7-2ee8-11ef-a915-bd45e2085de3',
'monotonic_time': 1819980.112406547,
'message_signature': 'f8d9a411b0cd0cb0d34e83'
},
{
'source': 'openstack',
'counter_name': 'memory.usage',
'counter_type': 'gauge',
'counter_unit': 'MB',
'counter_volume': 37.98046875,
'user_id': '6e7d71415cd5401cbe103829c9c5dec2',
'user_name': None,
'project_id': 'd965489b7f894cbda89cd2e25bfd85a0',
'project_name': None,
'resource_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63',
'timestamp': '2024-06-20T09:32:36.515823',
'resource_metadata': {
'display_name': 'myserver',
'name': 'instance-00000002',
'instance_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63',
'instance_type': 'tiny',
'host': 'e0d297f5df3b62ec73c8d42b',
'instance_host': 'devstack',
'flavor': {
'id': '4af9ac72-5787-4f86-8644-0faa87ce7c83',
'name': 'tiny',
'vcpus': 1,
'ram': 512,
'disk': 1,
'ephemeral': 0,
'swap': 0
},
'status': 'active',
'state': 'running',
'task_state': '',
'image': {
'id': '71860ed5-f66d-43e0-9514-f1d188106284'
},
'image_ref': '71860ed5-f66d-43e0-9514-f1d188106284',
'image_ref_url': None,
'architecture': 'x86_64',
'os_type': 'hvm',
'vcpus': 1,
'memory_mb': 512,
'disk_gb': 1,
'ephemeral_gb': 0,
'root_gb': 1
},
'message_id': '078029bf-2ee8-11ef-a915-bd45e2085de3',
'monotonic_time': 1819980.131767362,
'message_signature': 'f8d9a411b0cd0cb0d34e83'
},
{
'source': 'openstack',
'counter_name': 'image.size',
'counter_type': 'gauge',
'counter_unit': 'B',
'counter_volume': 16344576,
'user_id': None,
'user_name': None,
'project_id': 'd965489b7f894cbda89cd2e25bfd85a0',
'project_name': None,
'resource_id': 'f9276c96-8a12-432b-96a1-559d70715f97',
'timestamp': '2024-06-20T09:40:17.118871',
'resource_metadata': {
'status': 'active',
'visibility': 'public',
'name': 'cirros2',
'container_format': 'bare',
'created_at': '2024-05-30T11:38:52Z',
'disk_format': 'qcow2',
'updated_at': '2024-05-30T11:38:52Z',
'min_disk': 0,
'protected': False,
'checksum': '7734eb3945297adc90ddc6cebe8bb082',
'min_ram': 0,
'tags': [],
'virtual_size': 117440512
},
'message_id': '19f8f78a-2ee9-11ef-a95f-bd45e2085de3',
'monotonic_time': None,
'message_signature': 'f8d9a411b0cd0cb0d34e83'
}
]
@mock.patch('ceilometer.polling.prom_exporter.export')
def test_prom_disabled(self, export):
CONF = service.prepare_service([], [])
manager.AgentManager(0, CONF)
export.assert_not_called()
@mock.patch('ceilometer.polling.prom_exporter.export')
def test_export_called(self, export):
CONF = service.prepare_service([], [])
CONF.polling.enable_prometheus_exporter = True
CONF.polling.prometheus_listen_addresses = ['127.0.0.1:9101',
'127.0.0.1:9102']
manager.AgentManager(0, CONF)
export.assert_has_calls([
call('127.0.0.1', '9101'),
call('127.0.0.1', '9102')
])
def test_collect_metrics(self):
prom_exporter.collect_metrics(self.test_data)
sample_dict_1 = {'counter': 'image.size',
'image': 'f9276c96-8a12-432b-96a1-559d70715f97',
'project': 'd965489b7f894cbda89cd2e25bfd85a0',
'publisher': 'ceilometer',
'resource': 'f9276c96-8a12-432b-96a1-559d70715f97',
'resource_name': 'cirros2',
'type': 'size',
'unit': 'B'}
sample_dict_2 = {'counter': 'memory.usage',
'memory': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63',
'project': 'd965489b7f894cbda89cd2e25bfd85a0',
'publisher': 'ceilometer',
'resource': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63',
'resource_name': 'myserver:instance-00000002',
'type': 'usage',
'unit': 'MB',
'user': '6e7d71415cd5401cbe103829c9c5dec2',
'vm_instance': 'e0d297f5df3b62ec73c8d42b'}
self.assertEqual(16344576,
prom_exporter.CEILOMETER_REGISTRY.
get_sample_value('ceilometer_image_size',
sample_dict_1))
self.assertEqual(37.98046875,
prom_exporter.CEILOMETER_REGISTRY.
get_sample_value('ceilometer_memory_usage',
sample_dict_2))
def test_gen_labels(self):
slabels1 = dict(keys=[], values=[])
slabels1['keys'] = ['disk', 'publisher', 'type', 'counter',
'project', 'user', 'unit', 'resource',
'vm_instance', 'resource_name',
'resource_name']
slabels1['values'] = ['read', 'ceilometer', 'device',
'disk.device.read.latency',
'd965489b7f894cbda89cd2e25bfd85a0',
'6e7d71415cd5401cbe103829c9c5dec2',
'ns',
'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda',
'e0d297f5df3b62ec73c8d42b', 'myserver',
'myserver:instance-00000002']
label1 = prom_exporter._gen_labels(self.test_data[0])
self.assertDictEqual(label1, slabels1)
slabels2 = dict(keys=[], values=[])
slabels2['keys'] = ['image', 'publisher', 'type', 'counter',
'project', 'unit', 'resource',
'resource_name']
slabels2['values'] = ['f9276c96-8a12-432b-96a1-559d70715f97',
'ceilometer', 'size', 'image.size',
'd965489b7f894cbda89cd2e25bfd85a0', 'B',
'f9276c96-8a12-432b-96a1-559d70715f97',
'cirros2']
label2 = prom_exporter._gen_labels(self.test_data[2])
self.assertDictEqual(label2, slabels2)

View File

@ -35,3 +35,4 @@ tooz>=1.47.0 # Apache-2.0
oslo.cache>=1.26.0 # Apache-2.0
gnocchiclient>=7.0.0 # Apache-2.0
python-zaqarclient>=1.3.0 # Apache-2.0
prometheus_client>=0.20.0 # Apache-2.0