142 lines
3.4 KiB
Plaintext
142 lines
3.4 KiB
Plaintext
|
#!/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 "<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 _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))
|