diff --git a/ironic_lib/exception.py b/ironic_lib/exception.py
index 78326077..e3f65a1b 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 00000000..20c85321
--- /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:
+        _prefix<delim>name
+    where:
+        _prefix: [global_prefix<delim>][uuid<delim>][host_name<delim>]prefix
+        name: the name of this metric
+        <delim>: 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:
+           _prefix<delim>name
+        where:
+           _prefix: [global_prefix<delim>][uuid<delim>][host_name<delim>]prefix
+           name: the name of this metric
+           <delim>: 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 00000000..f863ceac
--- /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 00000000..0b479448
--- /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<delim>][host_name<delim>]prefix
+    where <delim> 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 00000000..92ccd083
--- /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 00000000..d328cd60
--- /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 00000000..a479d196
--- /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 3d1e6b4a..b48475ee 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