File: inotify-instances

package info (click to toggle)
prometheus-node-exporter-collectors 0.0~git20241119.a2b43e1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 380 kB
  • sloc: python: 1,681; sh: 596; awk: 74; makefile: 23
file content (113 lines) | stat: -rwxr-xr-x 2,813 bytes parent folder | download
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))