node_exporter/text_collector_examples/inotify-instances

142 lines
3.4 KiB
Python
Executable File

#!/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))