init
This commit is contained in:
commit
ce4c98cbcb
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
result
|
||||
*~
|
||||
*.deb
|
||||
64
flake.lock
generated
Normal file
64
flake.lock
generated
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"nodes": {
|
||||
"debBundler": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1632973430,
|
||||
"narHash": "sha256-9G8zo+0nfYAALV5umCyQR/2hVUFNH10JropBkyxZGGw=",
|
||||
"owner": "juliosueiras-nix",
|
||||
"repo": "nix-utils",
|
||||
"rev": "b44e1ffd726aa03056db9df469efb497d8b9871b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "juliosueiras-nix",
|
||||
"repo": "nix-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1623875721,
|
||||
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1696019113,
|
||||
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"debBundler": "debBundler",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
38
flake.nix
Normal file
38
flake.nix
Normal file
@ -0,0 +1,38 @@
|
||||
rec {
|
||||
description = "PVE prometheus exporter that collects metrics locally rather than use the PVE API";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = github:nixos/nixpkgs/nixos-unstable;
|
||||
debBundler = {
|
||||
url = github:juliosueiras-nix/nix-utils;
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, debBundler }: {
|
||||
|
||||
packages.x86_64-linux = with nixpkgs.legacyPackages.x86_64-linux; rec {
|
||||
pvemon = python3Packages.buildPythonApplication {
|
||||
pname = "pvemon";
|
||||
version = "0.1.0";
|
||||
src = ./src;
|
||||
propagatedBuildInputs = with python3Packages; [
|
||||
pexpect
|
||||
prometheus-client
|
||||
psutil
|
||||
];
|
||||
|
||||
meta = {
|
||||
inherit description;
|
||||
license = lib.licenses.mit;
|
||||
};
|
||||
};
|
||||
default = pvemon;
|
||||
deb = debBundler.bundlers.deb {
|
||||
inherit system;
|
||||
program = "${default}/bin/${default.pname}";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
187
src/pvemon/__init__.py
Normal file
187
src/pvemon/__init__.py
Normal file
@ -0,0 +1,187 @@
|
||||
from prometheus_client import start_http_server, Gauge, Info
|
||||
import psutil
|
||||
import time
|
||||
import argparse
|
||||
import re
|
||||
import itertools
|
||||
import os
|
||||
|
||||
import pexpect
|
||||
|
||||
DEFAULT_PORT = 9116
|
||||
DEFAULT_INTERVAL = 10
|
||||
|
||||
gauge_settings = [
|
||||
('pve_kvm_cpu', 'CPU time for VM', ['id', 'mode']),
|
||||
('pve_kvm_vcores', 'vCores allocated to the VM', ['id']),
|
||||
('pve_kvm_maxmem', 'Maximum memory (bytes) allocated to the VM', ['id']),
|
||||
('pve_kvm_memory_percent', 'Percentage of host memory used by VM', ['id']),
|
||||
('pve_kvm_memory_extended', 'Detailed memory metrics for VM', ['id', 'type']),
|
||||
('pve_kvm_threads', 'Threads used by the KVM process', ['id']),
|
||||
('pve_kvm_io_read_count', 'Number of read system calls made by the KVM process', ['id']),
|
||||
('pve_kvm_io_read_bytes', 'Number of bytes read from disk', ['id']),
|
||||
('pve_kvm_io_read_chars', 'Number of bytes read including buffers', ['id']),
|
||||
('pve_kvm_ctx_switches', 'Context switches', ['id', 'type']),
|
||||
('pve_kvm_io_write_count', 'Number of write system calls made by the KVM process', ['id']),
|
||||
('pve_kvm_io_write_bytes', 'Number of bytes written to disk', ['id']),
|
||||
('pve_kvm_io_write_chars', 'Number of bytes written including buffers', ['id']),
|
||||
|
||||
('pve_kvm_nic_queues', 'Number of queues in multiqueue config', ['id', 'ifname']),
|
||||
]
|
||||
|
||||
gauge_dict = {}
|
||||
|
||||
for name, description, labels in gauge_settings:
|
||||
gauge_dict[name] = Gauge(name, description, labels)
|
||||
|
||||
label_flags = [ "-id", "-name", "-cpu" ]
|
||||
get_label_name = lambda flag: flag[1:]
|
||||
info_settings = [
|
||||
('pve_kvm', 'information for each KVM process'),
|
||||
]
|
||||
|
||||
info_dict = {}
|
||||
|
||||
for name, description in info_settings:
|
||||
info_dict[name] = Info(name, description)
|
||||
|
||||
flag_to_label_value = lambda args, match: next((args[i+1] for i, x in enumerate(args[:-1]) if x == match), "unknown").split(",")[0]
|
||||
|
||||
dynamic_gauges = {}
|
||||
|
||||
def create_or_get_gauge(metric_name, labels):
|
||||
if metric_name not in dynamic_gauges:
|
||||
dynamic_gauges[metric_name] = Gauge(metric_name, f'{metric_name} for KVM process', labels)
|
||||
return dynamic_gauges[metric_name]
|
||||
|
||||
dynamic_infos = {}
|
||||
def create_or_get_info(info_name, labels):
|
||||
if (info_name,str(labels)) not in dynamic_infos:
|
||||
dynamic_infos[(info_name,str(labels))] = Info(info_name, f'{info_name} for {str(labels)}', labels)
|
||||
return dynamic_infos[(info_name,str(labels))]
|
||||
|
||||
def extract_nic_info_from_monitor(vm_id):
|
||||
child = pexpect.spawn(f'qm monitor {vm_id}')
|
||||
|
||||
# Wait for the QEMU monitor prompt
|
||||
child.expect('qm>', timeout=10)
|
||||
|
||||
# Execute 'info network'
|
||||
child.sendline('info network')
|
||||
|
||||
# Wait for the prompt again
|
||||
child.expect('qm>', timeout=10)
|
||||
|
||||
# Parse the output
|
||||
raw_output = child.before.decode('utf-8').strip()
|
||||
child.close()
|
||||
nic_info_list = re.findall(r'(net\d+:.*?)(?=(net\d+:|$))', raw_output, re.S)
|
||||
|
||||
nics_map = {}
|
||||
|
||||
for netdev, cfg in [x.strip().split(": ") for x in re.findall(r'[^\n]*(net\d+:[^\n]*)\n', raw_output, re.S)]:
|
||||
for cfg_pair in cfg.split(","):
|
||||
if cfg_pair=='':
|
||||
continue
|
||||
key, value = cfg_pair.split('=')
|
||||
if netdev not in nics_map:
|
||||
nics_map[netdev] = {}
|
||||
nics_map[netdev][key] = value
|
||||
|
||||
return [
|
||||
{
|
||||
"netdev": netdev,
|
||||
"queues": int(cfg["index"])+1,
|
||||
"type": cfg["type"],
|
||||
"model": cfg["model"],
|
||||
"macaddr": cfg["macaddr"],
|
||||
"ifname": cfg["ifname"]
|
||||
|
||||
}
|
||||
for netdev, cfg in nics_map.items()
|
||||
]
|
||||
|
||||
def read_interface_stats(ifname):
|
||||
stats_dir = f"/sys/class/net/{ifname}/statistics/"
|
||||
stats = {}
|
||||
try:
|
||||
for filename in os.listdir(stats_dir):
|
||||
with open(f"{stats_dir}{filename}", "r") as f:
|
||||
stats[filename] = int(f.read().strip())
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return stats
|
||||
|
||||
def collect_kvm_metrics():
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cpu_percent', 'memory_percent', 'num_threads']):
|
||||
if 'kvm' == proc.info['name']:
|
||||
cmdline = proc.cmdline()
|
||||
id = flag_to_label_value(cmdline,"-id")
|
||||
|
||||
# Extract vm labels from cmdline
|
||||
info_label_dict = {get_label_name(l): flag_to_label_value(cmdline,l) for l in label_flags}
|
||||
info_label_dict['pid']=str(proc.pid)
|
||||
info_dict["pve_kvm"].info(info_label_dict)
|
||||
|
||||
d = {
|
||||
"pve_kvm_vcores": flag_to_label_value(cmdline,"-smp"),
|
||||
"pve_kvm_maxmem": int(flag_to_label_value(cmdline,"-m"))*1024,
|
||||
"pve_kvm_memory_percent": proc.info['memory_percent'],
|
||||
"pve_kvm_threads": proc.info['num_threads'],
|
||||
}
|
||||
|
||||
for k, v in d.items():
|
||||
gauge_dict[k].labels(id=id).set(v)
|
||||
|
||||
cpu_times = proc.cpu_times()
|
||||
for mode in ['user', 'system', 'iowait']:
|
||||
gauge_dict["pve_kvm_cpu"].labels(id=id, mode=mode).set(getattr(cpu_times, mode))
|
||||
|
||||
io = proc.io_counters()
|
||||
for io_type, attr in itertools.product(['read', 'write'], ['count', 'bytes', 'chars']):
|
||||
gauge = globals()["gauge_dict"][f'pve_kvm_io_{io_type}_{attr}']
|
||||
gauge.labels(id=id).set(getattr(io, f"{io_type}_{attr}"))
|
||||
|
||||
for type in [ "voluntary", "involuntary" ]:
|
||||
gauge_dict["pve_kvm_ctx_switches"].labels(id=id, type=type).set(getattr(proc.num_ctx_switches(),type))
|
||||
|
||||
for attr in dir(proc.memory_full_info()):
|
||||
if not attr.startswith('_'):
|
||||
value = getattr(proc.memory_full_info(), attr)
|
||||
if not callable(value):
|
||||
gauge_dict["pve_kvm_memory_extended"].labels(id=id, type=attr).set(value)
|
||||
|
||||
for nic_info in extract_nic_info_from_monitor(id):
|
||||
queues = nic_info["queues"]
|
||||
del nic_info["queues"]
|
||||
nic_labels = {"id": id, "ifname": nic_info["ifname"]}
|
||||
prom_nic_info = create_or_get_info("pve_kvm_nic", nic_labels.keys())
|
||||
prom_nic_info.labels(**nic_labels).info({k: v for k, v in nic_info.items() if k not in nic_labels.keys()})
|
||||
|
||||
gauge_dict["pve_kvm_nic_queues"].labels(**nic_labels).set(queues)
|
||||
|
||||
interface_stats = read_interface_stats(nic_info["ifname"])
|
||||
for filename, value in interface_stats.items():
|
||||
metric_name = f"pve_kvm_nic_{filename}"
|
||||
gauge = create_or_get_gauge(metric_name, nic_labels.keys())
|
||||
gauge.labels(**nic_labels).set(value)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='PVE metrics exporter for Prometheus')
|
||||
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Port for the exporter to listen on')
|
||||
parser.add_argument('--interval', type=int, default=DEFAULT_INTERVAL, help='Interval between metric collections in seconds')
|
||||
parser.add_argument('--collect-running-vms', type=str, default='true', help='Enable or disable collecting running VMs metric (true/false)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
start_http_server(args.port)
|
||||
|
||||
while True:
|
||||
if args.collect_running_vms.lower() == 'true':
|
||||
collect_kvm_metrics()
|
||||
time.sleep(args.interval)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
12
src/setup.py
Normal file
12
src/setup.py
Normal file
@ -0,0 +1,12 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='pvemon',
|
||||
version='0.1',
|
||||
packages=find_packages(),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pvemon=pvemon:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user