#!/usr/bin/env python3 """ Expose Linux inotify(7) instance resource consumption. Operational properties: - This script may be invoked as an unprivileged user; in this case, metrics will only be exposed for processes owned by that unprivileged user. - No metrics will be exposed for processes that do not hold any inotify fds. Requires Python 3.5 or later. """ import collections import os import sys class Error(Exception): pass class _PIDGoneError(Error): pass _Process = collections.namedtuple( "Process", ["pid", "uid", "command", "inotify_instances"]) def _read_bytes(name): with open(name, mode='rb') as f: return f.read() def _pids(): for n in os.listdir("/proc"): if not n.isdigit(): continue yield int(n) def _pid_uid(pid): try: s = os.stat("/proc/{}".format(pid)) except FileNotFoundError: raise _PIDGoneError() return s.st_uid def _pid_command(pid): # Avoid GNU ps(1) for it truncates comm. # https://bugs.launchpad.net/ubuntu/+source/procps/+bug/295876/comments/3 try: cmdline = _read_bytes("/proc/{}/cmdline".format(pid)) except FileNotFoundError: raise _PIDGoneError() if not len(cmdline): return "" try: prog = cmdline[0:cmdline.index(0x00)] except ValueError: prog = cmdline return os.path.basename(prog).decode(encoding="ascii", errors="surrogateescape") def _pid_inotify_instances(pid): instances = 0 try: for fd in os.listdir("/proc/{}/fd".format(pid)): try: target = os.readlink("/proc/{}/fd/{}".format(pid, fd)) except FileNotFoundError: continue if target == "anon_inode:inotify": instances += 1 except FileNotFoundError: raise _PIDGoneError() return instances def _get_processes(): for p in _pids(): try: yield _Process(p, _pid_uid(p), _pid_command(p), _pid_inotify_instances(p)) except (PermissionError, _PIDGoneError): continue def _get_processes_nontrivial(): return (p for p in _get_processes() if p.inotify_instances > 0) def _format_gauge_metric(metric_name, metric_help, samples, value_func, tags_func=None, stream=sys.stdout): def _println(*args, **kwargs): if "file" not in kwargs: kwargs["file"] = stream print(*args, **kwargs) def _print(*args, **kwargs): if "end" not in kwargs: kwargs["end"] = "" _println(*args, **kwargs) _println("# HELP {} {}".format(metric_name, metric_help)) _println("# TYPE {} gauge".format(metric_name)) for s in samples: value = value_func(s) tags = None if tags_func: tags = tags_func(s) _print(metric_name) if tags: _print("{") _print(",".join(["{}=\"{}\"".format(k, v) for k, v in tags])) _print("}") _print(" ") _println(value) def main(args_unused=None): _format_gauge_metric( "inotify_instances", "Total number of inotify instances held open by a process.", _get_processes_nontrivial(), lambda s: s.inotify_instances, lambda s: [("pid", s.pid), ("uid", s.uid), ("command", s.command)]) if __name__ == "__main__": sys.exit(main(sys.argv))