1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
|
#!/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
from prometheus_client import CollectorRegistry, Gauge, generate_latest
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 "<zombie>"
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 main(args_unused=None):
registry = CollectorRegistry()
g = Gauge('inotify_instances', 'Total number of inotify instances held open by a process.',
['pid', 'uid', 'command'], registry=registry)
for proc in _get_processes_nontrivial():
g.labels(proc.pid, proc.uid, proc.command).set(proc.inotify_instances)
print(generate_latest(registry).decode(), end='')
if __name__ == "__main__":
sys.exit(main(sys.argv))
|