From 15c366580a18c77963d75cbc37e0805c96bc2a50 Mon Sep 17 00:00:00 2001 From: Jay Faulkner <jay@jvf.cc> Date: Tue, 5 Apr 2016 21:10:49 +0000 Subject: [PATCH] Metric chassis, driver, node, and port API calls This change adds initial metrics for Ironic based on new support in ironic-lib. Emits timing metrics for basic Ironic API calls. Bumps ironic-lib to 2.0.0 in requirements to add metrics support, as well as adding ironic_lib.metrics and ironic_lib.metrics_statds to ironic-config-generator.conf to get them in the sample config, which is also regenerated. Change-Id: Ic35802e4cd11763ebbedb8ddc28f7e8dc535cc2f Partial-bug: #1526219 --- etc/ironic/ironic.conf.sample | 68 +++++++++++++++++-- ironic/api/controllers/v1/chassis.py | 9 +++ ironic/api/controllers/v1/driver.py | 9 +++ ironic/api/controllers/v1/node.py | 23 +++++++ ironic/api/controllers/v1/port.py | 9 +++ ...etrics-for-api-calls-69f18fd1b9d54b05.yaml | 5 ++ requirements.txt | 2 +- tools/config/ironic-config-generator.conf | 2 + 8 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/emit-metrics-for-api-calls-69f18fd1b9d54b05.yaml 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