diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample
index 1ff5b09746..31063978df 100644
--- a/etc/ironic/ironic.conf.sample
+++ b/etc/ironic/ironic.conf.sample
@@ -135,6 +135,7 @@
 # is set in the configuration file and other logging
 # configuration options are ignored (for example,
 # logging_context_format_string). (string value)
+# Note: This option can be changed without restarting.
 # Deprecated group/name - [DEFAULT]/log_config
 #log_config_append = <None>
 
@@ -713,8 +714,12 @@
 # From oslo.db
 #
 
-# The file name to use with SQLite. (string value)
+# DEPRECATED: The file name to use with SQLite. (string value)
 # Deprecated group/name - [DEFAULT]/sqlite_db
+# This option is deprecated for removal.
+# Its value may be silently ignored in the future.
+# Reason: Should use config option connection or
+# slave_connection to connect the database.
 #sqlite_db = oslo.sqlite
 
 # If True, SQLite uses synchronous mode. (boolean value)
@@ -902,17 +907,18 @@
 
 # Size of EFI system partition in MiB when configuring UEFI
 # systems for local boot. (integer value)
-# Deprecated group/name - [deploy]/efi_system_partition_size
 #efi_system_partition_size = 200
 
+# Size of BIOS Boot partition in MiB when configuring GPT
+# partitioned systems for local boot in BIOS. (integer value)
+#bios_boot_partition_size = 1
+
 # Block size to use when writing to the nodes disk. (string
 # value)
-# Deprecated group/name - [deploy]/dd_block_size
 #dd_block_size = 1M
 
 # Maximum attempts to verify an iSCSI connection is active,
 # sleeping 1 second between attempts. (integer value)
-# Deprecated group/name - [deploy]/iscsi_verify_attempts
 #iscsi_verify_attempts = 3
 
 
@@ -1271,7 +1277,16 @@
 # From keystonemiddleware.auth_token
 #
 
-# Complete public Identity API endpoint. (string value)
+# Complete "public" Identity API endpoint. This endpoint
+# should not be an "admin" endpoint, as it should be
+# accessible by all end users. Unauthenticated clients are
+# redirected to this endpoint to authenticate. Although this
+# endpoint should  ideally be unversioned, client support in
+# the wild varies.  If you're using a versioned v2 endpoint
+# here, then this  should *not* be the same endpoint the
+# service user utilizes  for validating tokens, because normal
+# end users may not be  able to reach that endpoint. (string
+# value)
 #auth_uri = <None>
 
 # API version of the admin Identity API endpoint. (string
@@ -1470,6 +1485,49 @@
 #socket_timeout = 10000
 
 
+[metrics]
+
+#
+# From ironic_lib.metrics
+#
+
+# Backend to use for the metrics system. (string value)
+# Allowed values: noop, statsd
+#backend = noop
+
+# Prepend the hostname to all metric names. The format of
+# metric names is
+# [global_prefix.][host_name.]prefix.metric_name. (boolean
+# value)
+#prepend_host = false
+
+# Split the prepended host value by "." and reverse it (to
+# better match the reverse hierarchical form of domain names).
+# (boolean value)
+#prepend_host_reverse = true
+
+# 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. (string
+# value)
+#global_prefix = <None>
+
+
+[metrics_statsd]
+
+#
+# From ironic_lib.metrics_statsd
+#
+
+# Host for use with the statsd backend. (string value)
+#statsd_host = localhost
+
+# Port to use with the statsd backend. (port value)
+# Minimum value: 0
+# Maximum value: 65535
+#statsd_port = 8125
+
+
 [neutron]
 
 #
diff --git a/ironic/api/controllers/v1/chassis.py b/ironic/api/controllers/v1/chassis.py
index 4c5ffe5e3a..e43841b5f1 100644
--- a/ironic/api/controllers/v1/chassis.py
+++ b/ironic/api/controllers/v1/chassis.py
@@ -15,6 +15,7 @@
 
 import datetime
 
+from ironic_lib import metrics_utils
 import pecan
 from pecan import rest
 from six.moves import http_client
@@ -32,6 +33,8 @@ from ironic.common import exception
 from ironic.common.i18n import _
 from ironic import objects
 
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
 
 _DEFAULT_RETURN_FIELDS = ('uuid', 'description')
 
@@ -190,6 +193,7 @@ class ChassisController(rest.RestController):
                                                     sort_key=sort_key,
                                                     sort_dir=sort_dir)
 
+    @METRICS.timer('ChassisController.get_all')
     @expose.expose(ChassisCollection, types.uuid, int,
                    wtypes.text, wtypes.text, types.listtype)
     def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc',
@@ -209,6 +213,7 @@ class ChassisController(rest.RestController):
         return self._get_chassis_collection(marker, limit, sort_key, sort_dir,
                                             fields=fields)
 
+    @METRICS.timer('ChassisController.detail')
     @expose.expose(ChassisCollection, types.uuid, int,
                    wtypes.text, wtypes.text)
     def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
@@ -228,6 +233,7 @@ class ChassisController(rest.RestController):
         return self._get_chassis_collection(marker, limit, sort_key, sort_dir,
                                             resource_url)
 
+    @METRICS.timer('ChassisController.get_one')
     @expose.expose(Chassis, types.uuid, types.listtype)
     def get_one(self, chassis_uuid, fields=None):
         """Retrieve information about the given chassis.
@@ -241,6 +247,7 @@ class ChassisController(rest.RestController):
                                                   chassis_uuid)
         return Chassis.convert_with_links(rpc_chassis, fields=fields)
 
+    @METRICS.timer('ChassisController.post')
     @expose.expose(Chassis, body=Chassis, status_code=http_client.CREATED)
     def post(self, chassis):
         """Create a new chassis.
@@ -254,6 +261,7 @@ class ChassisController(rest.RestController):
         pecan.response.location = link.build_url('chassis', new_chassis.uuid)
         return Chassis.convert_with_links(new_chassis)
 
+    @METRICS.timer('ChassisController.patch')
     @wsme.validate(types.uuid, [ChassisPatchType])
     @expose.expose(Chassis, types.uuid, body=[ChassisPatchType])
     def patch(self, chassis_uuid, patch):
@@ -286,6 +294,7 @@ class ChassisController(rest.RestController):
         rpc_chassis.save()
         return Chassis.convert_with_links(rpc_chassis)
 
+    @METRICS.timer('ChassisController.delete')
     @expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
     def delete(self, chassis_uuid):
         """Delete a chassis.
diff --git a/ironic/api/controllers/v1/driver.py b/ironic/api/controllers/v1/driver.py
index f84c1aad29..4d327e9b21 100644
--- a/ironic/api/controllers/v1/driver.py
+++ b/ironic/api/controllers/v1/driver.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from ironic_lib import metrics_utils
 import pecan
 from pecan import rest
 from six.moves import http_client
@@ -27,6 +28,8 @@ from ironic.api import expose
 from ironic.common import exception
 
 
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
 # Property information for drivers:
 #   key = driver name;
 #   value = dictionary of properties of that driver:
@@ -139,6 +142,7 @@ class DriverPassthruController(rest.RestController):
         'methods': ['GET']
     }
 
+    @METRICS.timer('DriverPassthruController.methods')
     @expose.expose(wtypes.text, wtypes.text)
     def methods(self, driver_name):
         """Retrieve information about vendor methods of the given driver.
@@ -157,6 +161,7 @@ class DriverPassthruController(rest.RestController):
 
         return _VENDOR_METHODS[driver_name]
 
+    @METRICS.timer('DriverPassthruController._default')
     @expose.expose(wtypes.text, wtypes.text, wtypes.text,
                    body=wtypes.text)
     def _default(self, driver_name, method, data=None):
@@ -178,6 +183,7 @@ class DriverRaidController(rest.RestController):
         'logical_disk_properties': ['GET']
     }
 
+    @METRICS.timer('DriverRaidController.logical_disk_properties')
     @expose.expose(types.jsontype, wtypes.text)
     def logical_disk_properties(self, driver_name):
         """Returns the logical disk properties for the driver.
@@ -222,6 +228,7 @@ class DriversController(rest.RestController):
         'properties': ['GET'],
     }
 
+    @METRICS.timer('DriversController.get_all')
     @expose.expose(DriverList)
     def get_all(self):
         """Retrieve a list of drivers."""
@@ -232,6 +239,7 @@ class DriversController(rest.RestController):
         driver_list = pecan.request.dbapi.get_active_driver_dict()
         return DriverList.convert_with_links(driver_list)
 
+    @METRICS.timer('DriversController.get_one')
     @expose.expose(Driver, wtypes.text)
     def get_one(self, driver_name):
         """Retrieve a single driver."""
@@ -247,6 +255,7 @@ class DriversController(rest.RestController):
 
         raise exception.DriverNotFound(driver_name=driver_name)
 
+    @METRICS.timer('DriversController.properties')
     @expose.expose(wtypes.text, wtypes.text)
     def properties(self, driver_name):
         """Retrieve property information of the given driver.
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 303bbd3ca0..bd09789871 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -16,6 +16,7 @@
 import ast
 import datetime
 
+from ironic_lib import metrics_utils
 import jsonschema
 from oslo_config import cfg
 from oslo_log import log
@@ -78,6 +79,8 @@ _CLEAN_STEPS_SCHEMA = {
     }
 }
 
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
 # Vendor information for node's driver:
 #   key = driver name;
 #   value = dictionary of node vendor methods of that driver:
@@ -167,6 +170,7 @@ class BootDeviceController(rest.RestController):
             return pecan.request.rpcapi.get_boot_device(pecan.request.context,
                                                         rpc_node.uuid, topic)
 
+    @METRICS.timer('BootDeviceController.put')
     @expose.expose(None, types.uuid_or_name, wtypes.text, types.boolean,
                    status_code=http_client.NO_CONTENT)
     def put(self, node_ident, boot_device, persistent=False):
@@ -190,6 +194,7 @@ class BootDeviceController(rest.RestController):
                                              persistent=persistent,
                                              topic=topic)
 
+    @METRICS.timer('BootDeviceController.get')
     @expose.expose(wtypes.text, types.uuid_or_name)
     def get(self, node_ident):
         """Get the current boot device for a node.
@@ -205,6 +210,7 @@ class BootDeviceController(rest.RestController):
         """
         return self._get_boot_device(node_ident)
 
+    @METRICS.timer('BootDeviceController.supported')
     @expose.expose(wtypes.text, types.uuid_or_name)
     def supported(self, node_ident):
         """Get a list of the supported boot devices.
@@ -242,6 +248,7 @@ class ConsoleInfo(base.APIBase):
 
 class NodeConsoleController(rest.RestController):
 
+    @METRICS.timer('NodeConsoleController.get')
     @expose.expose(ConsoleInfo, types.uuid_or_name)
     def get(self, node_ident):
         """Get connection information about the console.
@@ -260,6 +267,7 @@ class NodeConsoleController(rest.RestController):
 
         return ConsoleInfo(console_enabled=console_state, console_info=console)
 
+    @METRICS.timer('NodeConsoleController.put')
     @expose.expose(None, types.uuid_or_name, types.boolean,
                    status_code=http_client.ACCEPTED)
     def put(self, node_ident, enabled):
@@ -350,6 +358,7 @@ class NodeStatesController(rest.RestController):
     console = NodeConsoleController()
     """Expose console as a sub-element of states"""
 
+    @METRICS.timer('NodeStatesController.get')
     @expose.expose(NodeStates, types.uuid_or_name)
     def get(self, node_ident):
         """List the states of the node.
@@ -362,6 +371,7 @@ class NodeStatesController(rest.RestController):
         rpc_node = api_utils.get_rpc_node(node_ident)
         return NodeStates.convert(rpc_node)
 
+    @METRICS.timer('NodeStatesController.raid')
     @expose.expose(None, types.uuid_or_name, body=types.jsontype)
     def raid(self, node_ident, target_raid_config):
         """Set the target raid config of the node.
@@ -390,6 +400,7 @@ class NodeStatesController(rest.RestController):
             e.code = http_client.NOT_FOUND
             raise
 
+    @METRICS.timer('NodeStatesController.power')
     @expose.expose(None, types.uuid_or_name, wtypes.text,
                    status_code=http_client.ACCEPTED)
     def power(self, node_ident, target):
@@ -429,6 +440,7 @@ class NodeStatesController(rest.RestController):
         url_args = '/'.join([node_ident, 'states'])
         pecan.response.location = link.build_url('nodes', url_args)
 
+    @METRICS.timer('NodeStatesController.provision')
     @expose.expose(None, types.uuid_or_name, wtypes.text,
                    wtypes.text, types.jsontype,
                    status_code=http_client.ACCEPTED)
@@ -860,6 +872,7 @@ class NodeVendorPassthruController(rest.RestController):
         'methods': ['GET']
     }
 
+    @METRICS.timer('NodeVendorPassthruController.methods')
     @expose.expose(wtypes.text, types.uuid_or_name)
     def methods(self, node_ident):
         """Retrieve information about vendor methods of the given node.
@@ -880,6 +893,7 @@ class NodeVendorPassthruController(rest.RestController):
 
         return _VENDOR_METHODS[rpc_node.driver]
 
+    @METRICS.timer('NodeVendorPassthruController._default')
     @expose.expose(wtypes.text, types.uuid_or_name, wtypes.text,
                    body=wtypes.text)
     def _default(self, node_ident, method, data=None):
@@ -911,6 +925,7 @@ class NodeMaintenanceController(rest.RestController):
         pecan.request.rpcapi.update_node(pecan.request.context,
                                          rpc_node, topic=topic)
 
+    @METRICS.timer('NodeMaintenanceController.put')
     @expose.expose(None, types.uuid_or_name, wtypes.text,
                    status_code=http_client.ACCEPTED)
     def put(self, node_ident, reason=None):
@@ -922,6 +937,7 @@ class NodeMaintenanceController(rest.RestController):
         """
         self._set_maintenance(node_ident, True, reason=reason)
 
+    @METRICS.timer('NodeMaintenanceController.delete')
     @expose.expose(None, types.uuid_or_name, status_code=http_client.ACCEPTED)
     def delete(self, node_ident):
         """Remove the node from maintenance mode.
@@ -1096,6 +1112,7 @@ class NodesController(rest.RestController):
                   "enabled. Please stop the console first.") % node_ident,
                 status_code=http_client.CONFLICT)
 
+    @METRICS.timer('NodesController.get_all')
     @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
                    types.boolean, wtypes.text, types.uuid, int, wtypes.text,
                    wtypes.text, wtypes.text, types.listtype)
@@ -1137,6 +1154,7 @@ class NodesController(rest.RestController):
                                           limit, sort_key, sort_dir,
                                           driver, fields=fields)
 
+    @METRICS.timer('NodesController.detail')
     @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
                    types.boolean, wtypes.text, types.uuid, int, wtypes.text,
                    wtypes.text, wtypes.text)
@@ -1178,6 +1196,7 @@ class NodesController(rest.RestController):
                                           limit, sort_key, sort_dir,
                                           driver, resource_url)
 
+    @METRICS.timer('NodesController.validate')
     @expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
     def validate(self, node=None, node_uuid=None):
         """Validate the driver interfaces, using the node's UUID or name.
@@ -1201,6 +1220,7 @@ class NodesController(rest.RestController):
         return pecan.request.rpcapi.validate_driver_interfaces(
             pecan.request.context, rpc_node.uuid, topic)
 
+    @METRICS.timer('NodesController.get_one')
     @expose.expose(Node, types.uuid_or_name, types.listtype)
     def get_one(self, node_ident, fields=None):
         """Retrieve information about the given node.
@@ -1217,6 +1237,7 @@ class NodesController(rest.RestController):
         rpc_node = api_utils.get_rpc_node(node_ident)
         return Node.convert_with_links(rpc_node, fields=fields)
 
+    @METRICS.timer('NodesController.post')
     @expose.expose(Node, body=Node, status_code=http_client.CREATED)
     def post(self, node):
         """Create a new node.
@@ -1254,6 +1275,7 @@ class NodesController(rest.RestController):
         pecan.response.location = link.build_url('nodes', new_node.uuid)
         return Node.convert_with_links(new_node)
 
+    @METRICS.timer('NodesController.patch')
     @wsme.validate(types.uuid, [NodePatchType])
     @expose.expose(Node, types.uuid_or_name, body=[NodePatchType])
     def patch(self, node_ident, patch):
@@ -1316,6 +1338,7 @@ class NodesController(rest.RestController):
 
         return Node.convert_with_links(new_node)
 
+    @METRICS.timer('NodesController.delete')
     @expose.expose(None, types.uuid_or_name,
                    status_code=http_client.NO_CONTENT)
     def delete(self, node_ident):
diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py
index b5f332c2c9..b764def959 100644
--- a/ironic/api/controllers/v1/port.py
+++ b/ironic/api/controllers/v1/port.py
@@ -15,6 +15,7 @@
 
 import datetime
 
+from ironic_lib import metrics_utils
 from oslo_utils import uuidutils
 import pecan
 from pecan import rest
@@ -32,6 +33,8 @@ from ironic.common import exception
 from ironic.common.i18n import _
 from ironic import objects
 
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
 
 _DEFAULT_RETURN_FIELDS = ('uuid', 'address')
 
@@ -263,6 +266,7 @@ class PortsController(rest.RestController):
         except exception.PortNotFound:
             return []
 
+    @METRICS.timer('PortsController.get_all')
     @expose.expose(PortCollection, types.uuid_or_name, types.uuid,
                    types.macaddress, types.uuid, int, wtypes.text,
                    wtypes.text, types.listtype)
@@ -302,6 +306,7 @@ class PortsController(rest.RestController):
                                           limit, sort_key, sort_dir,
                                           fields=fields)
 
+    @METRICS.timer('PortsController.detail')
     @expose.expose(PortCollection, types.uuid_or_name, types.uuid,
                    types.macaddress, types.uuid, int, wtypes.text,
                    wtypes.text)
@@ -341,6 +346,7 @@ class PortsController(rest.RestController):
                                           limit, sort_key, sort_dir,
                                           resource_url)
 
+    @METRICS.timer('PortsController.get_one')
     @expose.expose(Port, types.uuid, types.listtype)
     def get_one(self, port_uuid, fields=None):
         """Retrieve information about the given port.
@@ -357,6 +363,7 @@ class PortsController(rest.RestController):
         rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid)
         return Port.convert_with_links(rpc_port, fields=fields)
 
+    @METRICS.timer('PortsController.post')
     @expose.expose(Port, body=Port, status_code=http_client.CREATED)
     def post(self, port):
         """Create a new port.
@@ -373,6 +380,7 @@ class PortsController(rest.RestController):
         pecan.response.location = link.build_url('ports', new_port.uuid)
         return Port.convert_with_links(new_port)
 
+    @METRICS.timer('PortsController.patch')
     @wsme.validate(types.uuid, [PortPatchType])
     @expose.expose(Port, types.uuid, body=[PortPatchType])
     def patch(self, port_uuid, patch):
@@ -417,6 +425,7 @@ class PortsController(rest.RestController):
 
         return Port.convert_with_links(new_port)
 
+    @METRICS.timer('PortsController.delete')
     @expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
     def delete(self, port_uuid):
         """Delete a port.
diff --git a/releasenotes/notes/emit-metrics-for-api-calls-69f18fd1b9d54b05.yaml b/releasenotes/notes/emit-metrics-for-api-calls-69f18fd1b9d54b05.yaml
new file mode 100644
index 0000000000..2397950fd1
--- /dev/null
+++ b/releasenotes/notes/emit-metrics-for-api-calls-69f18fd1b9d54b05.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - With this change, ironic now emits timing metrics
+    for all API methods to statsd, if enabled by config
+    in the [metrics] and [metrics_statsd] sections.
diff --git a/requirements.txt b/requirements.txt
index 3264502f87..61ea5200a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,7 @@ paramiko>=2.0 # LGPLv2.1+
 python-neutronclient>=4.2.0 # Apache-2.0
 python-glanceclient>=2.0.0 # Apache-2.0
 python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
-ironic-lib>=1.3.0 # Apache-2.0
+ironic-lib>=2.0.0 # Apache-2.0
 python-swiftclient>=2.2.0 # Apache-2.0
 pytz>=2013.6 # MIT
 stevedore>=1.10.0 # Apache-2.0
diff --git a/tools/config/ironic-config-generator.conf b/tools/config/ironic-config-generator.conf
index 4196cda88a..088c234bf6 100644
--- a/tools/config/ironic-config-generator.conf
+++ b/tools/config/ironic-config-generator.conf
@@ -4,6 +4,8 @@ wrap_width = 62
 namespace = ironic
 namespace = ironic_lib.disk_utils
 namespace = ironic_lib.disk_partitioner
+namespace = ironic_lib.metrics
+namespace = ironic_lib.metrics_statsd
 namespace = ironic_lib.utils
 namespace = oslo.db
 namespace = oslo.messaging