diff --git a/ironic_lib/exception.py b/ironic_lib/exception.py index 7832607..e3f65a1 100644 --- a/ironic_lib/exception.py +++ b/ironic_lib/exception.py @@ -99,3 +99,7 @@ class InstanceDeployFailure(IronicException): class FileSystemNotSupported(IronicException): message = _("Failed to create a file system. " "File system %(fs)s is not supported.") + + +class InvalidMetricConfig(IronicException): + message = _("Invalid value for metrics config option: %(reason)s") diff --git a/ironic_lib/metrics.py b/ironic_lib/metrics.py new file mode 100644 index 0000000..20c8532 --- /dev/null +++ b/ironic_lib/metrics.py @@ -0,0 +1,300 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved. +# +# 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 abc +import functools +import random +import time + +import six + +from ironic_lib.common.i18n import _ + + +class Timer(object): + """A timer decorator and context manager. + + It is bound to this MetricLogger. For example: + + from ironic_lib import metrics + + METRICS = metrics.get_metrics_logger() + + @METRICS.timer('foo') + def foo(bar, baz): + print bar, baz + + with METRICS.timer('foo'): + do_something() + """ + def __init__(self, metrics, name): + """Init the decorator / context manager. + + :param metrics: The metric logger + :param name: The metric name + """ + if not isinstance(name, six.string_types): + raise TypeError(_("The metric name is expected to be a string. " + "Value is %s") % name) + self.metrics = metrics + self.name = name + self._start = None + + def __call__(self, f): + @functools.wraps(f) + def wrapped(*args, **kwargs): + start = _time() + result = f(*args, **kwargs) + duration = _time() - start + + # Log the timing data (in ms) + self.metrics.send_timer(self.metrics.get_metric_name(self.name), + duration * 1000) + return result + return wrapped + + def __enter__(self): + self._start = _time() + + def __exit__(self, exc_type, exc_val, exc_tb): + duration = _time() - self._start + # Log the timing data (in ms) + self.metrics.send_timer(self.metrics.get_metric_name(self.name), + duration * 1000) + + +class Counter(object): + """A counter decorator and context manager. + + It is bound to this MetricLogger. For example: + + from ironic_lib import metrics + + METRICS = metrics.get_metrics_logger() + + @METRICS.counter('foo') + def foo(bar, baz): + print bar, baz + + with METRICS.counter('foo'): + do_something() + """ + def __init__(self, metrics, name, sample_rate): + """Init the decorator / context manager. + + :param metrics: The metric logger + :param name: The metric name + :param sample_rate: Probabilistic rate at which the values will be sent + """ + if not isinstance(name, six.string_types): + raise TypeError(_("The metric name is expected to be a string. " + "Value is %s") % name) + + if (sample_rate is not None and + (sample_rate < 0.0 or sample_rate > 1.0)): + msg = _("sample_rate is set to %s. Value must be None " + "or in the interval [0.0, 1.0]") % sample_rate + raise ValueError(msg) + + self.metrics = metrics + self.name = name + self.sample_rate = sample_rate + + def __call__(self, f): + @functools.wraps(f) + def wrapped(*args, **kwargs): + self.metrics.send_counter( + self.metrics.get_metric_name(self.name), + 1, sample_rate=self.sample_rate) + + result = f(*args, **kwargs) + + return result + return wrapped + + def __enter__(self): + self.metrics.send_counter(self.metrics.get_metric_name(self.name), + 1, sample_rate=self.sample_rate) + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +class Gauge(object): + """A gauge decorator. + + It is bound to this MetricLogger. For example: + + from ironic_lib import metrics + + METRICS = metrics.get_metrics_logger() + + @METRICS.gauge('foo') + def foo(bar, baz): + print bar, baz + + with METRICS.gauge('foo'): + do_something() + """ + def __init__(self, metrics, name): + """Init the decorator / context manager. + + :param metrics: The metric logger + :param name: The metric name + """ + if not isinstance(name, six.string_types): + raise TypeError(_("The metric name is expected to be a string. " + "Value is %s") % name) + self.metrics = metrics + self.name = name + + def __call__(self, f): + @functools.wraps(f) + def wrapped(*args, **kwargs): + result = f(*args, **kwargs) + self.metrics.send_gauge(self.metrics.get_metric_name(self.name), + result) + + return result + return wrapped + + +def _time(): + """Wraps time.time() for simpler testing.""" + return time.time() + + +@six.add_metaclass(abc.ABCMeta) +class MetricLogger(object): + """Abstract class representing a metrics logger. + + A MetricLogger sends data to a backend (noop or statsd). + The data can be a gauge, a counter, or a timer. + + The data sent to the backend is composed of: + - a full metric name + - a numeric value + + The format of the full metric name is: + _prefixname + where: + _prefix: [global_prefix][uuid][host_name]prefix + name: the name of this metric + : the delimiter. Default is '.' + """ + + def __init__(self, prefix='', delimiter='.'): + """Init a MetricLogger. + + :param prefix: Prefix for this metric logger. This string will prefix + all metric names. + :param delimiter: Delimiter used to generate the full metric name. + """ + self._prefix = prefix + self._delimiter = delimiter + + def get_metric_name(self, name): + """Get the full metric name. + + The format of the full metric name is: + _prefixname + where: + _prefix: [global_prefix][uuid][host_name]prefix + name: the name of this metric + : the delimiter. Default is '.' + + :param name: The metric name. + :return: The full metric name, with logger prefix, as a string. + """ + if not self._prefix: + return name + return self._delimiter.join([self._prefix, name]) + + def send_gauge(self, name, value): + """Send gauge metric data. + + Gauges are simple values. + The backend will set the value of gauge 'name' to 'value'. + + :param name: Metric name + :param value: Metric numeric value that will be sent to the backend + """ + self._gauge(name, value) + + def send_counter(self, name, value, sample_rate=None): + """Send counter metric data. + + Counters are used to count how many times an event occurred. + The backend will increment the counter 'name' by the value 'value'. + + Optionally, specify sample_rate in the interval [0.0, 1.0] to + sample data probabilistically where: + + P(send metric data) = sample_rate + + If sample_rate is None, then always send metric data, but do not + have the backend send sample rate information (if supported). + + :param name: Metric name + :param value: Metric numeric value that will be sent to the backend + :param sample_rate: Probabilistic rate at which the values will be + sent. Value must be None or in the interval [0.0, 1.0]. + """ + if (sample_rate is None or random.random() < sample_rate): + return self._counter(name, value, + sample_rate=sample_rate) + + def send_timer(self, name, value): + """Send timer data. + + Timers are used to measure how long it took to do something. + + :param m_name: Metric name + :param m_value: Metric numeric value that will be sent to the backend + """ + self._timer(name, value) + + def timer(self, name): + return Timer(self, name) + + def counter(self, name, sample_rate=None): + return Counter(self, name, sample_rate) + + def gauge(self, name): + return Gauge(self, name) + + @abc.abstractmethod + def _gauge(self, name, value): + """Abstract method for backends to implement gauge behavior.""" + + @abc.abstractmethod + def _counter(self, name, value, sample_rate=None): + """Abstract method for backends to implement counter behavior.""" + + @abc.abstractmethod + def _timer(self, name, value): + """Abstract method for backends to implement timer behavior.""" + + +class NoopMetricLogger(MetricLogger): + """Noop metric logger that throws away all metric data.""" + def _gauge(self, name, value): + pass + + def _counter(self, name, value, sample_rate=None): + pass + + def _timer(self, m_name, value): + pass diff --git a/ironic_lib/metrics_statsd.py b/ironic_lib/metrics_statsd.py new file mode 100644 index 0000000..f863cea --- /dev/null +++ b/ironic_lib/metrics_statsd.py @@ -0,0 +1,108 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved. +# +# 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 contextlib +import socket + +from oslo_config import cfg +from oslo_log import log + +from ironic_lib.common.i18n import _LW +from ironic_lib import metrics + +statsd_opts = [ + cfg.StrOpt('statsd_host', + default='localhost', + help='Host for use with the statsd backend.'), + cfg.PortOpt('statsd_port', + default=8125, + help='Port to use with the statsd backend.') +] + +CONF = cfg.CONF +CONF.register_opts(statsd_opts, group='metrics_statsd') + +LOG = log.getLogger(__name__) + + +class StatsdMetricLogger(metrics.MetricLogger): + """Metric logger that reports data via the statsd protocol.""" + + GAUGE_TYPE = 'g' + COUNTER_TYPE = 'c' + TIMER_TYPE = 'ms' + + def __init__(self, prefix, delimiter='.', host=None, port=None): + """Initialize a StatsdMetricLogger + + The logger uses the given prefix list, delimiter, host, and port. + + :param prefix: Prefix for this metric logger. + :param delimiter: Delimiter used to generate the full metric name. + :param host: The statsd host + :param port: The statsd port + """ + super(StatsdMetricLogger, self).__init__(prefix, + delimiter=delimiter) + + self._host = host or CONF.metrics_statsd.statsd_host + self._port = port or CONF.metrics_statsd.statsd_port + + self._target = (self._host, self._port) + + def _send(self, name, value, metric_type, sample_rate=None): + """Send metrics to the statsd backend + + :param name: Metric name + :param value: Metric value + :param metric_type: Metric type (GAUGE_TYPE, COUNTER_TYPE, + or TIMER_TYPE) + :param sample_rate: Probabilistic rate at which the values will be sent + """ + if sample_rate is None: + metric = '%s:%s|%s' % (name, value, metric_type) + else: + metric = '%s:%s|%s@%s' % (name, value, metric_type, sample_rate) + + # Ideally, we'd cache a sending socket in self, but that + # results in a socket getting shared by multiple green threads. + with contextlib.closing(self._open_socket()) as sock: + try: + sock.settimeout(0.0) + sock.sendto(metric, self._target) + except socket.error as e: + LOG.warning(_LW("Failed to send the metric value to " + "host %(host)s port %(port)s. " + "Error: %(error)s"), + {'host': self._host, 'port': self._port, + 'error': e}) + + def _open_socket(self): + return socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def _gauge(self, name, value): + return self._send(name, value, self.GAUGE_TYPE) + + def _counter(self, name, value, sample_rate=None): + return self._send(name, value, self.COUNTER_TYPE, + sample_rate=sample_rate) + + def _timer(self, name, value): + return self._send(name, value, self.TIMER_TYPE) + + +def list_opts(): + """Entry point for oslo-config-generator.""" + return [('metrics_statsd', statsd_opts)] diff --git a/ironic_lib/metrics_utils.py b/ironic_lib/metrics_utils.py new file mode 100644 index 0000000..0b47944 --- /dev/null +++ b/ironic_lib/metrics_utils.py @@ -0,0 +1,100 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +import six + +from ironic_lib.common.i18n import _ +from ironic_lib import exception +from ironic_lib import metrics +from ironic_lib import metrics_statsd + +metrics_opts = [ + cfg.StrOpt('backend', + default='noop', + choices=['noop', 'statsd'], + help='Backend to use for the metrics system.'), + cfg.BoolOpt('prepend_host', + default=False, + help='Prepend the hostname to all metric names. ' + 'The format of metric names is ' + '[global_prefix.][host_name.]prefix.metric_name.'), + cfg.BoolOpt('prepend_host_reverse', + default=True, + help='Split the prepended host value by "." and reverse it ' + '(to better match the reverse hierarchical form of ' + 'domain names).'), + cfg.StrOpt('global_prefix', + help='Prefix all metric names with this value. ' + 'By default, there is no global prefix. ' + 'The format of metric names is ' + '[global_prefix.][host_name.]prefix.metric_name.') +] + +CONF = cfg.CONF +CONF.register_opts(metrics_opts, group='metrics') + + +def get_metrics_logger(prefix='', backend=None, host=None, delimiter='.'): + """Return a metric logger with the specified prefix. + + The format of the prefix is: + [global_prefix][host_name]prefix + where is the delimiter (default is '.') + + :param prefix: Prefix for this metric logger. + Value should be a string or None. + :param backend: Backend to use for the metrics system. + Possible values are 'noop' and 'statsd'. + :param host: Name of this node. + :param delimiter: Delimiter to use for the metrics name. + :return: The new MetricLogger. + """ + if not isinstance(prefix, six.string_types): + msg = (_("This metric prefix (%s) is of unsupported type. " + "Value should be a string or None") + % str(prefix)) + raise exception.InvalidMetricConfig(msg) + + if CONF.metrics.prepend_host and host: + if CONF.metrics.prepend_host_reverse: + host = '.'.join(reversed(host.split('.'))) + + if prefix: + prefix = delimiter.join([host, prefix]) + else: + prefix = host + + if CONF.metrics.global_prefix: + if prefix: + prefix = delimiter.join([CONF.metrics.global_prefix, prefix]) + else: + prefix = CONF.metrics.global_prefix + + backend = backend or CONF.metrics.backend + if backend == 'statsd': + return metrics_statsd.StatsdMetricLogger(prefix, delimiter=delimiter) + elif backend == 'noop': + return metrics.NoopMetricLogger(prefix, delimiter=delimiter) + else: + msg = (_("The backend is set to an unsupported type: " + "%s. Value should be 'noop' or 'statsd'.") + % backend) + raise exception.InvalidMetricConfig(msg) + + +def list_opts(): + """Entry point for oslo-config-generator.""" + return [('metrics', metrics_opts)] diff --git a/ironic_lib/tests/test_metrics.py b/ironic_lib/tests/test_metrics.py new file mode 100644 index 0000000..92ccd08 --- /dev/null +++ b/ironic_lib/tests/test_metrics.py @@ -0,0 +1,161 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved +# +# 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 types + +import mock +from oslo_config import cfg +from oslotest import base as test_base + +from ironic_lib import metrics as metricslib + +CONF = cfg.CONF + + +class MockedMetricLogger(metricslib.MetricLogger): + _gauge = mock.Mock(spec_set=types.FunctionType) + _counter = mock.Mock(spec_set=types.FunctionType) + _timer = mock.Mock(spec_set=types.FunctionType) + + +class TestMetricLogger(test_base.BaseTestCase): + def setUp(self): + super(TestMetricLogger, self).setUp() + self.ml = MockedMetricLogger('prefix', '.') + self.ml_no_prefix = MockedMetricLogger('', '.') + self.ml_other_delim = MockedMetricLogger('prefix', '*') + self.ml_default = MockedMetricLogger() + + def test_init(self): + self.assertEqual(self.ml._prefix, 'prefix') + self.assertEqual(self.ml._delimiter, '.') + + self.assertEqual(self.ml_no_prefix._prefix, '') + self.assertEqual(self.ml_other_delim._delimiter, '*') + self.assertEqual(self.ml_default._prefix, '') + + def test_get_metric_name(self): + self.assertEqual( + self.ml.get_metric_name('metric'), + 'prefix.metric') + + self.assertEqual( + self.ml_no_prefix.get_metric_name('metric'), + 'metric') + + self.assertEqual( + self.ml_other_delim.get_metric_name('metric'), + 'prefix*metric') + + def test_send_gauge(self): + self.ml.send_gauge('prefix.metric', 10) + self.ml._gauge.assert_called_once_with('prefix.metric', 10) + + def test_send_counter(self): + self.ml.send_counter('prefix.metric', 10) + self.ml._counter.assert_called_once_with( + 'prefix.metric', 10, + sample_rate=None) + self.ml._counter.reset_mock() + + self.ml.send_counter('prefix.metric', 10, sample_rate=1.0) + self.ml._counter.assert_called_once_with( + 'prefix.metric', 10, + sample_rate=1.0) + self.ml._counter.reset_mock() + + self.ml.send_counter('prefix.metric', 10, sample_rate=0.0) + self.assertFalse(self.ml._counter.called) + + def test_send_timer(self): + self.ml.send_timer('prefix.metric', 10) + self.ml._timer.assert_called_once_with('prefix.metric', 10) + + @mock.patch('ironic_lib.metrics._time', autospec=True) + @mock.patch('ironic_lib.metrics.MetricLogger.send_timer', autospec=True) + def test_decorator_timer(self, mock_timer, mock_time): + mock_time.side_effect = [1, 43] + + @self.ml.timer('foo.bar.baz') + def func(x): + return x * x + + func(10) + + mock_timer.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', + 42 * 1000) + + @mock.patch('ironic_lib.metrics.MetricLogger.send_counter', autospec=True) + def test_decorator_counter(self, mock_counter): + + @self.ml.counter('foo.bar.baz') + def func(x): + return x * x + + func(10) + + mock_counter.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', 1, + sample_rate=None) + + @mock.patch('ironic_lib.metrics.MetricLogger.send_counter', autospec=True) + def test_decorator_counter_sample_rate(self, mock_counter): + + @self.ml.counter('foo.bar.baz', sample_rate=0.5) + def func(x): + return x * x + + func(10) + + mock_counter.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', 1, + sample_rate=0.5) + + @mock.patch('ironic_lib.metrics.MetricLogger.send_gauge', autospec=True) + def test_decorator_gauge(self, mock_gauge): + @self.ml.gauge('foo.bar.baz') + def func(x): + return x + + func(10) + + mock_gauge.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', 10) + + @mock.patch('ironic_lib.metrics._time', autospec=True) + @mock.patch('ironic_lib.metrics.MetricLogger.send_timer', autospec=True) + def test_context_mgr_timer(self, mock_timer, mock_time): + mock_time.side_effect = [1, 43] + + with self.ml.timer('foo.bar.baz'): + pass + + mock_timer.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', + 42 * 1000) + + @mock.patch('ironic_lib.metrics.MetricLogger.send_counter', autospec=True) + def test_context_mgr_counter(self, mock_counter): + + with self.ml.counter('foo.bar.baz'): + pass + + mock_counter.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', 1, + sample_rate=None) + + @mock.patch('ironic_lib.metrics.MetricLogger.send_counter', autospec=True) + def test_context_mgr_counter_sample_rate(self, mock_counter): + + with self.ml.counter('foo.bar.baz', sample_rate=0.5): + pass + + mock_counter.assert_called_once_with(self.ml, 'prefix.foo.bar.baz', 1, + sample_rate=0.5) diff --git a/ironic_lib/tests/test_metrics_statsd.py b/ironic_lib/tests/test_metrics_statsd.py new file mode 100644 index 0000000..d328cd6 --- /dev/null +++ b/ironic_lib/tests/test_metrics_statsd.py @@ -0,0 +1,96 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved +# +# 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 socket + +import mock +from oslotest import base as test_base + +from ironic_lib import metrics_statsd + + +class TestStatsdMetricLogger(test_base.BaseTestCase): + def setUp(self): + super(TestStatsdMetricLogger, self).setUp() + self.ml = metrics_statsd.StatsdMetricLogger('prefix', '.', 'test-host', + 4321) + + def test_init(self): + self.assertEqual(self.ml._host, 'test-host') + self.assertEqual(self.ml._port, 4321) + self.assertEqual(self.ml._target, ('test-host', 4321)) + + @mock.patch('ironic_lib.metrics_statsd.StatsdMetricLogger._send', + autospec=True) + def test_gauge(self, mock_send): + self.ml._gauge('metric', 10) + mock_send.assert_called_once_with(self.ml, 'metric', 10, 'g') + + @mock.patch('ironic_lib.metrics_statsd.StatsdMetricLogger._send', + autospec=True) + def test_counter(self, mock_send): + self.ml._counter('metric', 10) + mock_send.assert_called_once_with(self.ml, 'metric', 10, 'c', + sample_rate=None) + mock_send.reset_mock() + + self.ml._counter('metric', 10, sample_rate=1.0) + mock_send.assert_called_once_with(self.ml, 'metric', 10, 'c', + sample_rate=1.0) + + @mock.patch('ironic_lib.metrics_statsd.StatsdMetricLogger._send', + autospec=True) + def test_timer(self, mock_send): + self.ml._timer('metric', 10) + mock_send.assert_called_once_with(self.ml, 'metric', 10, 'ms') + + @mock.patch('socket.socket') + def test_open_socket(self, mock_socket_constructor): + self.ml._open_socket() + mock_socket_constructor.assert_called_once_with( + socket.AF_INET, + socket.SOCK_DGRAM) + + @mock.patch('socket.socket') + def test_send(self, mock_socket_constructor): + mock_socket = mock.Mock() + mock_socket_constructor.return_value = mock_socket + + self.ml._send('part1.part2', 2, 'type') + mock_socket.sendto.assert_called_once_with( + 'part1.part2:2|type', + ('test-host', 4321)) + mock_socket.close.assert_called_once_with() + mock_socket.reset_mock() + + self.ml._send('part1.part2', 3.14159, 'type') + mock_socket.sendto.assert_called_once_with( + 'part1.part2:3.14159|type', + ('test-host', 4321)) + mock_socket.close.assert_called_once_with() + mock_socket.reset_mock() + + self.ml._send('part1.part2', 5, 'type') + mock_socket.sendto.assert_called_once_with( + 'part1.part2:5|type', + ('test-host', 4321)) + mock_socket.close.assert_called_once_with() + mock_socket.reset_mock() + + self.ml._send('part1.part2', 5, 'type', sample_rate=0.5) + mock_socket.sendto.assert_called_once_with( + 'part1.part2:5|type@0.5', + ('test-host', 4321)) + mock_socket.close.assert_called_once_with() diff --git a/ironic_lib/tests/test_metrics_utils.py b/ironic_lib/tests/test_metrics_utils.py new file mode 100644 index 0000000..a479d19 --- /dev/null +++ b/ironic_lib/tests/test_metrics_utils.py @@ -0,0 +1,108 @@ +# Copyright 2016 Rackspace Hosting +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base as test_base + +from oslo_config import cfg + +from ironic_lib import exception +from ironic_lib import metrics as metricslib +from ironic_lib import metrics_statsd +from ironic_lib import metrics_utils + +CONF = cfg.CONF + + +class TestGetLogger(test_base.BaseTestCase): + def setUp(self): + super(TestGetLogger, self).setUp() + + def test_default_backend(self): + metrics = metrics_utils.get_metrics_logger('foo') + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + + def test_statsd_backend(self): + CONF.set_override('backend', 'statsd', group='metrics') + + metrics = metrics_utils.get_metrics_logger('foo') + self.assertIsInstance(metrics, metrics_statsd.StatsdMetricLogger) + CONF.clear_override('backend', group='metrics') + + def test_nonexisting_backend(self): + CONF.set_override('backend', 'none', group='metrics') + + self.assertRaises(exception.InvalidMetricConfig, + metrics_utils.get_metrics_logger, 'foo') + CONF.clear_override('backend', group='metrics') + + def test_numeric_prefix(self): + self.assertRaises(exception.InvalidMetricConfig, + metrics_utils.get_metrics_logger, 1) + + def test_numeric_list_prefix(self): + self.assertRaises(exception.InvalidMetricConfig, + metrics_utils.get_metrics_logger, (1, 2)) + + def test_default_prefix(self): + metrics = metrics_utils.get_metrics_logger() + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + self.assertEqual(metrics.get_metric_name("bar"), "bar") + + def test_prepend_host_backend(self): + CONF.set_override('prepend_host', True, group='metrics') + CONF.set_override('prepend_host_reverse', False, group='metrics') + + metrics = metrics_utils.get_metrics_logger(prefix='foo', + host="host.example.com") + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + self.assertEqual(metrics.get_metric_name("bar"), + "host.example.com.foo.bar") + + CONF.clear_override('prepend_host', group='metrics') + CONF.clear_override('prepend_host_reverse', group='metrics') + + def test_prepend_global_prefix_host_backend(self): + CONF.set_override('prepend_host', True, group='metrics') + CONF.set_override('prepend_host_reverse', False, group='metrics') + CONF.set_override('global_prefix', 'global_pre', group='metrics') + + metrics = metrics_utils.get_metrics_logger(prefix='foo', + host="host.example.com") + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + self.assertEqual(metrics.get_metric_name("bar"), + "global_pre.host.example.com.foo.bar") + + CONF.clear_override('prepend_host', group='metrics') + CONF.clear_override('prepend_host_reverse', group='metrics') + CONF.clear_override('global_prefix', group='metrics') + + def test_prepend_other_delim(self): + metrics = metrics_utils.get_metrics_logger('foo', delimiter='*') + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + self.assertEqual(metrics.get_metric_name("bar"), + "foo*bar") + + def test_prepend_host_reverse_backend(self): + CONF.set_override('prepend_host', True, group='metrics') + CONF.set_override('prepend_host_reverse', True, group='metrics') + + metrics = metrics_utils.get_metrics_logger('foo', + host="host.example.com") + self.assertIsInstance(metrics, metricslib.NoopMetricLogger) + self.assertEqual(metrics.get_metric_name("bar"), + "com.example.host.foo.bar") + + CONF.clear_override('prepend_host', group='metrics') + CONF.clear_override('prepend_host_reverse', group='metrics') diff --git a/setup.cfg b/setup.cfg index 3d1e6b4..b48475e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,3 +25,5 @@ oslo.config.opts = ironic_lib.disk_partitioner = ironic_lib.disk_partitioner:list_opts ironic_lib.disk_utils = ironic_lib.disk_utils:list_opts ironic_lib.utils = ironic_lib.utils:list_opts + ironic_lib.metrics = ironic_lib.metrics_utils:list_opts + ironic_lib.metrics_statsd = ironic_lib.metrics_statsd:list_opts