Skip to content

Commit 2ac8c57

Browse files
committed
inotify_instance: add commandline arguments for command and user filtering
on systems with many short lived processes, the cardinality of the rendered metrics can become quite bad. this change adds commandline arguments to the script to support filtering of processes based on the process command or user. the script logic remains backwards compatible as it continues to emit metrics for all accessible process information if not filters are provided. alternatively the rendered metrics can be summed up by user to reduce the cardinality even further. Signed-off-by: Gordon Bleux <33967640+UiP9AV6Y@users.noreply.github.com>
1 parent b36a2ea commit 2ac8c57

114 files changed

Lines changed: 568 additions & 113 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
__pypackages__/
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
htmlcov/
12+
cover/
13+
target/
14+
eggs/
15+
.eggs/
16+
lib/
17+
lib64/
18+
parts/
19+
sdist/
20+
var/
21+
wheels/
22+
share/python-wheels/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
MANIFEST
27+
*.manifest
28+
*.spec
29+
.tox/
30+
.nox/
31+
.coverage
32+
.coverage.*
33+
.cache
34+
nosetests.xml
35+
coverage.xml
36+
*.cover
37+
*.py,cover
38+
.hypothesis/
39+
.pytest_cache/
40+
.pybuilder/
41+
.env
42+
.venv
43+

inotify-instances

Lines changed: 0 additions & 113 deletions
This file was deleted.

inotify_instances.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Expose Linux inotify(7) instance resource consumption.
5+
6+
Operational properties:
7+
8+
- This script may be invoked as an unprivileged user; in this case, metrics
9+
will only be exposed for processes owned by that unprivileged user.
10+
11+
- No metrics will be exposed for processes that do not hold any inotify fds.
12+
13+
Requires Python 3.5 or later.
14+
"""
15+
16+
import argparse
17+
import collections
18+
import os
19+
import re
20+
import sys
21+
22+
from prometheus_client import CollectorRegistry, Gauge, generate_latest
23+
24+
__doc__ = "Exponse Linux Kernel inode information as Prometheus metrics."
25+
_NotifyTarget = "anon_inode:inotify"
26+
_Proc = "/proc"
27+
_CommFile = "cmdline"
28+
_StatsFile = "status"
29+
_DescriptorDir = "fd"
30+
31+
32+
class Error(Exception):
33+
pass
34+
35+
36+
class _PIDGoneError(Error):
37+
pass
38+
39+
40+
_Process = collections.namedtuple(
41+
"Process", ["pid", "uid", "command", "inotify_instances"]
42+
)
43+
44+
45+
def _read_bytes(name):
46+
with open(name, mode="rb") as f:
47+
return f.read()
48+
49+
50+
def _pids(proc=_Proc):
51+
for n in os.listdir(proc):
52+
if not n.isdigit():
53+
continue
54+
yield int(n)
55+
56+
57+
def _pid_uid(status):
58+
try:
59+
s = os.stat(status)
60+
except FileNotFoundError:
61+
raise _PIDGoneError()
62+
return s.st_uid
63+
64+
65+
def _pid_command(cmdline):
66+
# Avoid GNU ps(1) for it truncates comm.
67+
# https://bugs.launchpad.net/ubuntu/+source/procps/+bug/295876/comments/3
68+
try:
69+
cmdline = _read_bytes(cmdline)
70+
except FileNotFoundError:
71+
raise _PIDGoneError()
72+
73+
if not len(cmdline):
74+
return "<zombie>"
75+
76+
try:
77+
prog = cmdline[0 : cmdline.index(0x00)]
78+
except ValueError:
79+
prog = cmdline
80+
return os.path.basename(prog).decode(encoding="ascii", errors="surrogateescape")
81+
82+
83+
def _pid_inotify_instances(descriptors):
84+
instances = 0
85+
try:
86+
for fd in os.listdir(descriptors):
87+
try:
88+
target = os.readlink(os.path.join(descriptors, fd))
89+
except FileNotFoundError:
90+
continue
91+
if target == _NotifyTarget:
92+
instances += 1
93+
except FileNotFoundError:
94+
raise _PIDGoneError()
95+
return instances
96+
97+
98+
def _get_processes(proc=_Proc):
99+
for p in _pids(proc):
100+
try:
101+
pid = str(p)
102+
status = os.path.join(proc, pid, _StatsFile)
103+
cmdline = os.path.join(proc, pid, _CommFile)
104+
descriptors = os.path.join(proc, pid, _DescriptorDir)
105+
yield _Process(
106+
p,
107+
_pid_uid(status),
108+
_pid_command(cmdline),
109+
_pid_inotify_instances(descriptors),
110+
)
111+
except (PermissionError, _PIDGoneError):
112+
continue
113+
114+
115+
def _generate_process_metrics(command=None, users=[], proc=_Proc):
116+
registry = CollectorRegistry()
117+
namespace = "inotify"
118+
119+
g = Gauge(
120+
"instances",
121+
"Total number of inotify instances held open by a process.",
122+
["pid", "uid", "command"],
123+
namespace=namespace,
124+
registry=registry,
125+
)
126+
127+
for proc in _get_processes(proc):
128+
if proc.inotify_instances <= 0:
129+
continue
130+
elif users and proc.uid not in users:
131+
continue
132+
elif command and not command.match(proc.command):
133+
continue
134+
135+
g.labels(proc.pid, proc.uid, proc.command).set(proc.inotify_instances)
136+
137+
return generate_latest(registry).decode()
138+
139+
140+
def _generate_user_metrics(users=[], proc=_Proc):
141+
registry = CollectorRegistry()
142+
namespace = "inotify"
143+
144+
g = Gauge(
145+
"user_instances",
146+
"Total number of inotify instances held open by a user.",
147+
["uid"],
148+
namespace=namespace,
149+
registry=registry,
150+
)
151+
152+
for proc in _get_processes(proc):
153+
if proc.inotify_instances <= 0:
154+
continue
155+
elif users and proc.uid not in users:
156+
continue
157+
158+
g.labels(proc.uid).inc()
159+
160+
return generate_latest(registry).decode()
161+
162+
163+
def main(argv=[__name__]):
164+
parser = argparse.ArgumentParser(
165+
prog=os.path.basename(argv[0]),
166+
exit_on_error=False,
167+
description=__doc__,
168+
)
169+
parser.add_argument(
170+
"-c",
171+
"--command",
172+
default=".+",
173+
type=re.compile,
174+
dest="command",
175+
metavar="REGEX",
176+
help="Filter metrics based on the process command",
177+
)
178+
parser.add_argument(
179+
"-u",
180+
"--user",
181+
action="append",
182+
type=int,
183+
dest="users",
184+
metavar="UID",
185+
help="Filter metrics based on the process user",
186+
)
187+
parser.add_argument(
188+
"-U",
189+
"--user-summary",
190+
action="store_true",
191+
dest="user_summary",
192+
help="Generate per-user metric summaries instead of per process",
193+
)
194+
195+
try:
196+
args = parser.parse_args(argv[1:])
197+
except argparse.ArgumentError as err:
198+
print(err, file=sys.stderr)
199+
return 1
200+
201+
if args.user_summary:
202+
print(_generate_user_metrics(args.users), end="")
203+
else:
204+
print(_generate_process_metrics(args.command, args.users), end="")
205+
return 0
206+
207+
208+
if __name__ == "__main__":
209+
sys.exit(main(sys.argv))
11 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/dev/null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/dev/null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
anon_inode:inotify
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
anon_inode:inotify
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
anon_inode:inotify
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
anon_inode:inotify

0 commit comments

Comments
 (0)