├── .gitignore ├── LICENSE ├── README.md ├── collectd_iostat_python.py └── iostat_types.db /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # idea 56 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Denis Zhdanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | collectd-iostat-python 2 | ====================== 3 | 4 | `collectd-iostat-python` is an `iostat` plugin for Collectd that allows you to 5 | graph Linux `iostat` metrics in Graphite or other output formats that are 6 | supported by Collectd. 7 | 8 | This plugin (and mostly this `README`) is rewrite of the 9 | [collectd-iostat](https://github.com/keirans/collectd-iostat) from Ruby to Python 10 | using the 11 | [collectd-python](http://collectd.org/documentation/manpages/collectd-python.5.shtml) 12 | plugin. 13 | 14 | 15 | Why? 16 | ---- 17 | 18 | Disk performance is quite crucial for most of modern server 19 | applications, especially databases (e.g. MySQL - check out [this 20 | slides](http://www.percona.com/live/mysql-conference-2013/sessions/monitoring-io-performance-using-iostat-and-pt-diskstats) 21 | from Percona Live conference). 22 | 23 | Although Collectd [provides disk statistics out of the 24 | box](https://collectd.org/wiki/index.php/Plugin:Disk), graphing the metrics as 25 | shown by `iostat` was found to be more useful and graphic, because `iostat` 26 | reports usage of block devices, partitions, multipath devices and LVM volumes. 27 | 28 | Also this plugin was rewritten in Python, because it's a preferable language for 29 | siteops' tools on my current job, and choice of using 30 | [collectd-python](http://collectd.org/documentation/manpages/collectd-python.5.shtml) 31 | instead of 32 | [collectd-exec](https://collectd.org/documentation/manpages/collectd-exec.5.shtml) 33 | was made for performance and stability reasons. 34 | 35 | 36 | How? 37 | ---- 38 | 39 | `collectd-iostat-python` functions by calling `iostat` with some predefined 40 | intervals and push that data to Collectd using Collectd `python` plugin. 41 | 42 | Collectd can be then configured to write the Collected data into many output 43 | formats that are supported by it's write plugins, such as `graphite`, which was 44 | the primary use case for this plugin. 45 | 46 | 47 | Setup 48 | ----- 49 | 50 | Deploy the Collectd Python plugin into a suitable plugin directory for your 51 | Collectd instance. 52 | 53 | Configure Collectd's `python` plugin to execute the `iostat` plugin using a 54 | stanza similar to the following: 55 | 56 | 57 | ``` 58 | 59 | Globals true 60 | 61 | 62 | 63 | ModulePath "/usr/lib/collectd/plugins/python" 64 | Import "collectd_iostat_python" 65 | 66 | 67 | Path "/usr/bin/iostat" 68 | Interval 2 69 | Count 2 70 | Verbose false 71 | NiceNames false 72 | PluginName collectd_iostat_python 73 | 74 | 75 | ``` 76 | 77 | If you need to select a subset of the devices listed by iostat you can utilize 78 | `DisksRegex` to write a regular expression to select the appropriate devices for your environment. 79 | ``` 80 | # Only collect data for these devices 81 | DisksRegex "^[hs]d" 82 | ``` 83 | 84 | In large and changing environments it benefital if your device statistics maintain the same device names across reboots or reconfigurations so that historical data is not compromised. This can be achived by enabling persistent naming based on udev attributes. 85 | Simply enable persistent naming by setting UdevNameAttr to the attribute you want to use to name your devices. A good example would be ID_SERIAL which is persistent and unique per device. To find useful attributes you can use `udevadm info /dev/` 86 | ``` 87 | # Enable persistent device naming 88 | UdevNameAttr "ID_SERIAL" 89 | ``` 90 | 91 | Please note that you need to install [pyudev](https://pyudev.readthedocs.io/en/latest/) Python module for this functionality. 92 | 93 | In a multipath environment, a single physical disk can be exposed as two /dev entries. A device mapper entry is created by multipathd to handle interacting with the disk. Setting SkipPhysicalMultipath causes the physical multipath disks to be skipped, and only the dm- entry to be processed. Physical non-multipathed disks will be processed normally. Enable NoDisplayDMName as well to display the /dev entry instead of the registered device name. 94 | 95 | ``` 96 | SkipPhysicalMultipath true 97 | NoDisplayDMName true 98 | ``` 99 | 100 | Please note that you need to install [pyudev](https://pyudev.readthedocs.io/en/latest/) Python module for this functionality. 101 | 102 | 103 | If you would like to use more legible metric names (e.g. 104 | `requests_merged_per_second-read` instead of `rrqm_s`), you have to set 105 | `NiceNames` to `true` and add load the custom types database (see the 106 | `iostat_types.db` file) by adding the following into the Collectd config file: 107 | 108 | ``` 109 | # The default Collectd types database 110 | TypesDB "/usr/share/collectd/types.db" 111 | # The custom types database 112 | TypesDB "/usr/share/collectd/iostat_types.db" 113 | ``` 114 | 115 | Once functioning, the `iostat` data should then be visible via your various 116 | output plugins. Please note, that you need to restart collectd service after 117 | plugin installation, as usual. 118 | 119 | In the case of Graphite, Collectd should be writing data in the 120 | `hostname_domain_tld.collectd_iostat_python.DEVICE.column-name` style namespaces. 121 | Symbols like `/`, `-` and `%` in metric names (but not in device names) are 122 | automatically replaced by underscores (i.e. `_`). 123 | 124 | Please note that this plugin will take only last line of `iostat` 125 | output, so big `Count` numbers also have no sense, but `Count` needs 126 | to be more than `1` to get actual and not historical data. The data 127 | collection will run every `Interval * (Count-1)` seconds and this 128 | value needs to be less than `Collectd.INTERVAL`. Default 129 | `Collectd.INTERVAL` is 10 seconds, so default value of `Count=2` and 130 | `Interval=2` works quite well for me. 131 | 132 | 133 | Technical notes 134 | --------------- 135 | 136 | For parsing `iostat` output, I'm using 137 | [jakamkon's](https://bitbucket.org/jakamkon) 138 | [python-iostat](https://bitbucket.org/jakamkon/python-iostat) Python module, but 139 | as an internal part of the script instead of a separate module because of couple 140 | of fixes I have made (using Kbytes instead of blocks, adding -N to `iostat` for 141 | LVM endpoint resolving, migration to `subprocess` module as replacement of 142 | deprecated `popen3`, `objectification` etc). 143 | 144 | 145 | Compatibility 146 | ------------- 147 | 148 | Plugin was tested on Ubuntu 12.04/14.04 (Collectd 5.2/5.3/5.4, Python 2.7) and 149 | CentOS (Collectd 5.4 / Python 2.6). Please note that if running Python 2.6 or 150 | older (i.e. on CentOS and its derivatives) we trying to restore `SIGCHLD` signal 151 | handler to mitigate a known [bug](http://bugs.python.org/issue1731717) which 152 | according to the Collectd's 153 | [documentation](https://collectd.org/documentation/manpages/collectd-python.5.shtml#configuration) 154 | breaks the `exec` plugin, unfortunately. 155 | 156 | 157 | TODO 158 | ---- 159 | 160 | * Maybe some data aggregation needed (e.g. we can use some max / avg aggregation 161 | of data across intervals instead of picking last line of `iostat` output). 162 | * The `Disks` parameter could support regexp. 163 | 164 | 165 | Additional reading 166 | ------------------ 167 | 168 | * [man iostat(1)](http://linux.die.net/man/1/iostat) 169 | * [Custom Collectd Plug-ins for Linux](http://support.rightscale.com/12-Guides/RightScale_101/08-Management_Tools/Monitoring_System/Writing_custom_collectd_plugins/Custom_Collectd_Plug-ins_for_Linux) 170 | * [python-iostat](https://bitbucket.org/jakamkon/python-iostat) 171 | * [collectd-iostat](https://github.com/keirans/collectd-iostat) 172 | * [Graphite @ The Architecture of Open Source Applications](http://www.aosabook.org/en/graphite.html) 173 | 174 | License 175 | ------- 176 | 177 | [MIT](http://mit-license.org/) 178 | 179 | 180 | Support 181 | ------- 182 | 183 | Please do not send me PMs in Twitter with issues. Just open an [issue](https://github.com/deniszh/collectd-iostat-python/issues) on [projects' Github](https://github.com/deniszh/collectd-iostat-python) instead and I'll respond ASAP! 184 | 185 | 186 | Contact 187 | ------- 188 | 189 | [Denis Zhdanov](mailto:denis.zhdanov@gmail.com) 190 | ([@deniszh](http://twitter.com/deniszh)) 191 | -------------------------------------------------------------------------------- /collectd_iostat_python.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # collectd-iostat-python 5 | # ====================== 6 | # 7 | # Collectd-iostat-python is an iostat plugin for collectd that allows you to 8 | # graph Linux iostat metrics in Graphite or other output formats that are 9 | # supported by collectd. 10 | # 11 | # https://github.com/powdahound/redis-collectd-plugin 12 | # - was used as template 13 | # https://github.com/keirans/collectd-iostat/ 14 | # - was used as inspiration and contains some code from 15 | # https://bitbucket.org/jakamkon/python-iostat 16 | # - by Kuba Kończyk 17 | # 18 | 19 | import signal 20 | import subprocess 21 | import sys 22 | import re 23 | try: 24 | import pyudev 25 | pyudev_available = True 26 | except ImportError: 27 | pyudev_available = False 28 | 29 | try: 30 | maketrans = ''.maketrans 31 | except AttributeError: 32 | # fallback for Python 2 33 | from string import maketrans 34 | 35 | __version__ = '0.0.5' 36 | __author__ = 'denis.zhdanov@gmail.com' 37 | 38 | 39 | class IOStatError(Exception): 40 | pass 41 | 42 | 43 | class CmdError(IOStatError): 44 | pass 45 | 46 | 47 | class ParseError(IOStatError): 48 | pass 49 | 50 | 51 | class IOStat(object): 52 | def __init__(self, path='/usr/bin/iostat', interval=2, count=2, disks=[], no_dm_name=False): 53 | self.path = path 54 | self.interval = interval 55 | self.count = count 56 | self.disks = disks 57 | self.no_dm_name = no_dm_name 58 | 59 | def parse_diskstats(self, input): 60 | """ 61 | Parse iostat -d and -dx output.If there are more 62 | than one series of statistics, get the last one. 63 | By default parse statistics for all avaliable block devices. 64 | 65 | @type input: C{string} 66 | @param input: iostat output 67 | 68 | @type disks: list of C{string}s 69 | @param input: lists of block devices that 70 | statistics are taken for. 71 | 72 | @return: C{dictionary} contains per block device statistics. 73 | Statistics are in form of C{dictonary}. 74 | Main statistics: 75 | tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn 76 | Extended staistics (available with post 2.5 kernels): 77 | rrqm/s wrqm/s r/s w/s rsec/s wsec/s rkB/s wkB/s avgrq-sz \ 78 | avgqu-sz await svctm %util 79 | See I{man iostat} for more details. 80 | """ 81 | dstats = {} 82 | dsi = input.rfind('Device') 83 | if dsi == -1: 84 | raise ParseError('Unknown input format: %r' % input) 85 | 86 | ds = input[dsi:].splitlines() 87 | hdr = ds.pop(0).split()[1:] 88 | 89 | for d in ds: 90 | if d: 91 | d = d.split() 92 | d = [re.sub(r',','.',element) for element in d] 93 | dev = d.pop(0) 94 | if (dev in self.disks) or not self.disks: 95 | dstats[dev] = dict([(k, float(v)) for k, v in zip(hdr, d)]) 96 | 97 | return dstats 98 | 99 | def sum_dstats(self, stats, smetrics): 100 | """ 101 | Compute the summary statistics for chosen metrics. 102 | """ 103 | avg = {} 104 | 105 | for disk, metrics in stats.items(): 106 | for mname, metric in metrics.items(): 107 | if mname not in smetrics: 108 | continue 109 | if mname in avg: 110 | avg[mname] += metric 111 | else: 112 | avg[mname] = metric 113 | 114 | return avg 115 | 116 | def _run(self, options=None): 117 | """ 118 | Run iostat command. 119 | """ 120 | close_fds = 'posix' in sys.builtin_module_names 121 | args = '%s %s %s %s %s' % ( 122 | self.path, 123 | ''.join(options), 124 | self.interval, 125 | self.count, 126 | ' '.join(self.disks)) 127 | 128 | return subprocess.Popen( 129 | args, 130 | bufsize=1, 131 | universal_newlines=True, 132 | shell=True, 133 | stdout=subprocess.PIPE, 134 | close_fds=close_fds) 135 | 136 | @staticmethod 137 | def _get_childs_data(child): 138 | """ 139 | Return child's data when avaliable. 140 | """ 141 | (stdout, stderr) = child.communicate() 142 | ecode = child.poll() 143 | 144 | if ecode != 0: 145 | raise CmdError('Command %r returned %d' % (child.cmd, ecode)) 146 | 147 | return stdout 148 | 149 | def get_diskstats(self): 150 | """ 151 | Get all avaliable disks statistics that we can get. 152 | """ 153 | options=['-','k','N','d'] 154 | extdoptions=['-','k','N','d','x'] 155 | if self.no_dm_name: 156 | options.remove('N') 157 | extdoptions.remove('N') 158 | dstats = self._run(options) 159 | extdstats = self._run(extdoptions) 160 | dsd = self._get_childs_data(dstats) 161 | edd = self._get_childs_data(extdstats) 162 | ds = self.parse_diskstats(dsd) 163 | eds = self.parse_diskstats(edd) 164 | 165 | for dk, dv in ds.items(): 166 | if dk in eds: 167 | ds[dk].update(eds[dk]) 168 | 169 | return ds 170 | 171 | 172 | class IOMon(object): 173 | def __init__(self): 174 | self.plugin_name = 'collectd-iostat-python' 175 | self.iostat_path = '/usr/bin/iostat' 176 | self.interval = 60.0 177 | self.iostat_interval = 2 178 | self.iostat_count = 2 179 | self.iostat_disks = [] 180 | self.iostat_nice_names = False 181 | self.iostat_disks_regex = '' 182 | self.iostat_udevnameattr = '' 183 | self.skip_multipath = False 184 | self.verbose_logging = False 185 | self.iostat_no_dm_name = False 186 | self.names = { 187 | 'tps': {'t': 'transfers_per_second'}, 188 | 'Blk_read/s': {'t': 'blocks_per_second', 'ti': 'read'}, 189 | 'kB_read/s': {'t': 'bytes_per_second', 'ti': 'read', 'm': 1024}, 190 | 'MB_read/s': {'t': 'bytes_per_second', 'ti': 'read', 'm': 1048576}, 191 | 'Blk_wrtn/s': {'t': 'blocks_per_second', 'ti': 'write'}, 192 | 'kB_wrtn/s': {'t': 'bytes_per_second', 'ti': 'write', 'm': 1024}, 193 | 'MB_wrtn/s': {'t': 'bytes_per_second', 'ti': 'write', 'm': 1048576}, 194 | 'Blk_read': {'t': 'blocks', 'ti': 'read'}, 195 | 'kB_read': {'t': 'bytes', 'ti': 'read', 'm': 1024}, 196 | 'MB_read': {'t': 'bytes', 'ti': 'read', 'm': 1048576}, 197 | 'Blk_wrtn': {'t': 'blocks', 'ti': 'write'}, 198 | 'kB_wrtn': {'t': 'bytes', 'ti': 'write', 'm': 1024}, 199 | 'MB_wrtn': {'t': 'bytes', 'ti': 'write', 'm': 1048576}, 200 | 'rrqm/s': {'t': 'requests_merged_per_second', 'ti': 'read'}, 201 | 'wrqm/s': {'t': 'requests_merged_per_second', 'ti': 'write'}, 202 | 'r/s': {'t': 'per_second', 'ti': 'read'}, 203 | 'w/s': {'t': 'per_second', 'ti': 'write'}, 204 | 'rsec/s': {'t': 'sectors_per_second', 'ti': 'read'}, 205 | 'rkB/s': {'t': 'bytes_per_second', 'ti': 'read', 'm': 1024}, 206 | 'rMB/s': {'t': 'bytes_per_second', 'ti': 'read', 'm': 1048576}, 207 | 'wsec/s': {'t': 'sectors_per_second', 'ti': 'write'}, 208 | 'wkB/s': {'t': 'bytes_per_second', 'ti': 'write', 'm': 1024}, 209 | 'wMB/s': {'t': 'bytes_per_second', 'ti': 'write', 'm': 1048576}, 210 | 'avgrq-sz': {'t': 'avg_request_size'}, 211 | 'avgqu-sz': {'t': 'avg_request_queue'}, 212 | 'await': {'t': 'avg_wait_time'}, 213 | 'r_await': {'t': 'avg_wait_time', 'ti': 'read'}, 214 | 'w_await': {'t': 'avg_wait_time', 'ti': 'write'}, 215 | 'svctm': {'t': 'avg_service_time'}, 216 | '%util': {'t': 'percent', 'ti': 'util'} 217 | } 218 | 219 | def log_verbose(self, msg): 220 | if not self.verbose_logging: 221 | return 222 | collectd.info('%s plugin [verbose]: %s' % (self.plugin_name, msg)) 223 | 224 | def configure_callback(self, conf): 225 | """ 226 | Receive configuration block 227 | """ 228 | for node in conf.children: 229 | val = str(node.values[0]) 230 | 231 | if node.key == 'Path': 232 | self.iostat_path = val 233 | elif node.key == 'Interval': 234 | self.interval = float(val) 235 | elif node.key == 'IostatInterval': 236 | self.iostat_interval = int(float(val)) 237 | elif node.key == 'Count': 238 | self.iostat_count = int(float(val)) 239 | elif node.key == 'Disks': 240 | self.iostat_disks = val.split(',') 241 | elif node.key == 'NiceNames': 242 | self.iostat_nice_names = val in ['True', 'true'] 243 | elif node.key == 'DisksRegex': 244 | self.iostat_disks_regex = val 245 | elif node.key == 'UdevNameAttr': 246 | self.iostat_udevnameattr = val 247 | elif node.key == 'PluginName': 248 | self.plugin_name = val 249 | elif node.key == 'Verbose': 250 | self.verbose_logging = val in ['True', 'true'] 251 | elif node.key == 'SkipPhysicalMultipath': 252 | self.skip_multipath = val in [ 'True', 'true' ] 253 | elif node.key == 'NoDisplayDMName': 254 | self.iostat_no_dm_name = val in [ 'True', 'true' ] 255 | else: 256 | collectd.warning( 257 | '%s plugin: Unknown config key: %s.' % ( 258 | self.plugin_name, 259 | node.key)) 260 | 261 | self.log_verbose( 262 | 'Configured with iostat=%s, interval=%s, count=%s, disks=%s, ' 263 | 'disks_regex=%s udevnameattr=%s skip_multipath=%s no_dm_name=%s' % ( 264 | self.iostat_path, 265 | self.iostat_interval, 266 | self.iostat_count, 267 | self.iostat_disks, 268 | self.iostat_disks_regex, 269 | self.iostat_udevnameattr, 270 | self.skip_multipath, 271 | self.iostat_no_dm_name)) 272 | 273 | collectd.register_read(self.read_callback, self.interval) 274 | 275 | def dispatch_value(self, plugin_instance, val_type, type_instance, value): 276 | """ 277 | Dispatch a value to collectd 278 | """ 279 | self.log_verbose( 280 | 'Sending value: %s-%s.%s=%s' % ( 281 | self.plugin_name, 282 | plugin_instance, 283 | '-'.join([val_type, type_instance]), 284 | value)) 285 | 286 | val = collectd.Values() 287 | val.plugin = self.plugin_name 288 | val.plugin_instance = plugin_instance 289 | val.type = val_type 290 | if len(type_instance): 291 | val.type_instance = type_instance 292 | val.values = [value, ] 293 | val.meta={'0': True} 294 | val.dispatch() 295 | 296 | def read_callback(self): 297 | """ 298 | Collectd read callback 299 | """ 300 | self.log_verbose('Read callback called') 301 | iostat = IOStat( 302 | path=self.iostat_path, 303 | interval=self.iostat_interval, 304 | count=self.iostat_count, 305 | disks=self.iostat_disks, 306 | no_dm_name=self.iostat_no_dm_name) 307 | ds = iostat.get_diskstats() 308 | 309 | if not ds: 310 | self.log_verbose('%s plugin: No info received.' % self.plugin_name) 311 | return 312 | 313 | if self.iostat_udevnameattr and pyudev_available: 314 | context = pyudev.Context() 315 | 316 | for disk in ds: 317 | if not re.match(self.iostat_disks_regex, disk): 318 | continue 319 | if self.iostat_udevnameattr and pyudev_available: 320 | device = pyudev.Device.from_device_file(context, "/dev/" + disk) 321 | if self.skip_multipath: 322 | mp_managed = device.get('DM_MULTIPATH_DEVICE_PATH') 323 | if mp_managed and mp_managed == '1': 324 | self.log_verbose('Skipping physical multipath disk %s' % disk) 325 | continue 326 | if self.iostat_udevnameattr: 327 | persistent_name = device.get(self.iostat_udevnameattr) 328 | if not persistent_name: 329 | self.log_verbose('Unable to determine disk name based on UdevNameAttr: %s' % self.iostat_udevnameattr) 330 | persistent_name = disk 331 | else: 332 | persistent_name = disk 333 | 334 | for name in ds[disk]: 335 | if self.iostat_nice_names and name in self.names: 336 | val_type = self.names[name]['t'] 337 | 338 | if 'ti' in self.names[name]: 339 | type_instance = self.names[name]['ti'] 340 | else: 341 | type_instance = '' 342 | 343 | value = ds[disk][name] 344 | if 'm' in self.names[name]: 345 | value *= self.names[name]['m'] 346 | else: 347 | val_type = 'gauge' 348 | tbl = maketrans('/-%', '___') 349 | type_instance = name.translate(tbl) 350 | value = ds[disk][name] 351 | self.dispatch_value( 352 | persistent_name, val_type, type_instance, value) 353 | 354 | def restore_sigchld(): 355 | """ 356 | Restore SIGCHLD handler for python <= v2.6 357 | It will BREAK exec plugin!!! 358 | See https://github.com/deniszh/collectd-iostat-python/issues/2 for details 359 | """ 360 | if sys.version_info[0] == 2 and sys.version_info[1] <= 6: 361 | signal.signal(signal.SIGCHLD, signal.SIG_DFL) 362 | 363 | 364 | if __name__ == '__main__': 365 | iostat = IOStat() 366 | ds = iostat.get_diskstats() 367 | 368 | for disk in ds: 369 | for metric in ds[disk]: 370 | tbl = maketrans('/-%', '___') 371 | metric_name = metric.translate(tbl) 372 | print("%s.%s:%s" % (disk, metric_name, ds[disk][metric])) 373 | 374 | sys.exit(0) 375 | else: 376 | import collectd 377 | 378 | iomon = IOMon() 379 | 380 | # Register callbacks 381 | collectd.register_init(restore_sigchld) 382 | collectd.register_config(iomon.configure_callback) 383 | -------------------------------------------------------------------------------- /iostat_types.db: -------------------------------------------------------------------------------- 1 | avg_request_queue value:GAUGE:0:U 2 | avg_request_size value:GAUGE:0:U 3 | avg_service_time value:GAUGE:0:U 4 | avg_wait_time value:GAUGE:0:U 5 | blocks value:GAUGE:0:U 6 | blocks_per_second value:GAUGE:0:U 7 | bytes_per_second value:GAUGE:0:U 8 | transfers_per_second value:GAUGE:0:U 9 | per_second value:GAUGE:0:U 10 | requests_merged_per_second value:GAUGE:0:U 11 | sectors_per_second value:GAUGE:0:U 12 | --------------------------------------------------------------------------------