mirror of
https://gitlab.nic.cz/knot/knot-dns.git
synced 2026-02-03 18:49:28 -05:00
python: add knot_exporter
Based on https://github.com/salzmdan/knot_exporter, commit 564a6daa3e17f68e410d25fe4b876d79e418d4d1
This commit is contained in:
parent
4d7eb96bf9
commit
80b604f1df
8 changed files with 374 additions and 1 deletions
|
|
@ -855,6 +855,9 @@ AC_CONFIG_FILES([Makefile
|
|||
samples/Makefile
|
||||
distro/Makefile
|
||||
python/Makefile
|
||||
python/knot_exporter/Makefile
|
||||
python/knot_exporter/pyproject.toml
|
||||
python/knot_exporter/setup.py
|
||||
python/libknot/Makefile
|
||||
python/libknot/pyproject.toml
|
||||
python/libknot/setup.py
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
SUBDIRS = libknot
|
||||
SUBDIRS = knot_exporter libknot
|
||||
TARGETS = dist upload
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
$(TARGETS):
|
||||
$(MAKE) -C knot_exporter $@
|
||||
$(MAKE) -C libknot $@
|
||||
|
|
|
|||
19
python/knot_exporter/Makefile.am
Normal file
19
python/knot_exporter/Makefile.am
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
EXTRA_DIST = \
|
||||
knot_exporter/__init__.py \
|
||||
knot_exporter/knot_exporter.py \
|
||||
pyproject.toml.in \
|
||||
setup.py.in \
|
||||
README.md
|
||||
|
||||
clean-local:
|
||||
-rm -rf dist *.egg-info
|
||||
|
||||
dist: clean-local
|
||||
@if hatchling -h &> /dev/null; then \
|
||||
hatchling build; \
|
||||
else \
|
||||
python3 setup.py sdist; \
|
||||
fi
|
||||
|
||||
upload:
|
||||
twine upload dist/*
|
||||
20
python/knot_exporter/README.md
Normal file
20
python/knot_exporter/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# knot-exporter
|
||||
|
||||
A Prometheus exporter for [Knot DNS](https://www.knot-dns.cz/)'s server and query statistics.
|
||||
|
||||
# Getting Started
|
||||
|
||||
The Knot instance also needs to be configured to collect statistics using
|
||||
[Statistics module](https://www.knot-dns.cz/docs/latest/html/modules.html?highlight=mod%20stats#stats-query-statistics)
|
||||
|
||||
The exporter can be started via:
|
||||
|
||||
```bash
|
||||
$ knot-exporter
|
||||
```
|
||||
|
||||
To get a complete list of the available options, run:
|
||||
|
||||
```bash
|
||||
$ knot-exporter --help
|
||||
```
|
||||
0
python/knot_exporter/knot_exporter/__init__.py
Normal file
0
python/knot_exporter/knot_exporter/__init__.py
Normal file
246
python/knot_exporter/knot_exporter/knot_exporter.py
Executable file
246
python/knot_exporter/knot_exporter/knot_exporter.py
Executable file
|
|
@ -0,0 +1,246 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import http.server
|
||||
import ipaddress
|
||||
import psutil
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
import libknot
|
||||
import libknot.control
|
||||
|
||||
from prometheus_client.core import REGISTRY
|
||||
from prometheus_client.core import GaugeMetricFamily
|
||||
from prometheus_client.exposition import MetricsHandler
|
||||
|
||||
|
||||
def memory_usage():
|
||||
out = dict()
|
||||
pids = subprocess.check_output(['pidof', 'knotd']).split(b' ')
|
||||
for pid in pids:
|
||||
if not pid:
|
||||
continue
|
||||
key = int(pid)
|
||||
out[key] = psutil.Process(key).memory_info()._asdict()['rss']
|
||||
return out
|
||||
|
||||
|
||||
class KnotCollector(object):
|
||||
def __init__(self, lib, sock, ttl,
|
||||
collect_meminfo : bool,
|
||||
collect_stats : bool,
|
||||
collect_zone_stats : bool,
|
||||
collect_zone_status : bool,
|
||||
collect_zone_timers : bool,):
|
||||
libknot.Knot(lib)
|
||||
self._sock = sock
|
||||
self._ttl = ttl
|
||||
self.collect_meminfo = collect_meminfo
|
||||
self.collect_stats = collect_stats
|
||||
self.collect_zone_stats = collect_zone_stats
|
||||
self.collect_zone_status = collect_zone_status
|
||||
self.collect_zone_timers = collect_zone_timers
|
||||
|
||||
def convert_state_time(time):
|
||||
if time == "pending" or time == "running" or time == "frozen":
|
||||
return 0
|
||||
elif time == "not scheduled" or time == "-":
|
||||
return None
|
||||
else:
|
||||
match = re.match("([+-])((\d+)D)?((\d+)h)?((\d+)m)?((\d+)s)?", time)
|
||||
seconds = -1 if match.group(1) == '-' else 1
|
||||
if match.group(3):
|
||||
seconds = seconds + 86400 * int(match.group(3))
|
||||
if match.group(5):
|
||||
seconds = seconds + 3600 * int(match.group(5))
|
||||
if match.group(7):
|
||||
seconds = seconds + 60 * int(match.group(7))
|
||||
if match.group(9):
|
||||
seconds = seconds + int(match.group(9))
|
||||
|
||||
return seconds
|
||||
|
||||
def collect(self):
|
||||
ctl = libknot.control.KnotCtl()
|
||||
ctl.connect(self._sock)
|
||||
ctl.set_timeout(self._ttl)
|
||||
metric_families = dict()
|
||||
|
||||
def metric_families_append(family, labels, labels_val, data):
|
||||
m = metric_families.get(family, GaugeMetricFamily(family, '', labels=labels))
|
||||
m.add_metric(labels_val, data)
|
||||
metric_families[family] = m
|
||||
|
||||
if self.collect_meminfo:
|
||||
# Get global metrics.
|
||||
for pid, usage in memory_usage().items():
|
||||
metric_families_append('knot_memory_usage', ['section', 'type'], ['server', str(pid)], usage)
|
||||
|
||||
if self.collect_stats:
|
||||
ctl.send_block(cmd="stats", flags="")
|
||||
global_stats = ctl.receive_stats()
|
||||
|
||||
for section, section_data in global_stats.items():
|
||||
for item, item_data in section_data.items():
|
||||
name = ('knot_' + item).replace('-', '_')
|
||||
try:
|
||||
for kind, kind_data in item_data.items():
|
||||
metric_families_append(name, ['section', 'type'], [section, kind], kind_data)
|
||||
|
||||
except AttributeError:
|
||||
metric_families_append(name, ['section'], [section], item_data)
|
||||
|
||||
if self.collect_zone_stats:
|
||||
# Get zone metrics.
|
||||
ctl.send_block(cmd="zone-stats", flags="")
|
||||
zone_stats = ctl.receive_stats()
|
||||
|
||||
if "zone" in zone_stats:
|
||||
for zone, zone_data in zone_stats["zone"].items():
|
||||
for section, section_data in zone_data.items():
|
||||
for item, item_data in section_data.items():
|
||||
name = ('knot_' + item).replace('-', '_')
|
||||
try:
|
||||
for kind, kind_data in item_data.items():
|
||||
metric_families_append(name, ['zone', 'section', 'type'], [zone, section, kind], kind_data)
|
||||
except AttributeError:
|
||||
metric_families_append(name, ['zone', 'section'], [zone, section], item_data)
|
||||
|
||||
if self.collect_zone_status:
|
||||
# zone state metrics
|
||||
ctl.send_block(cmd="zone-status")
|
||||
zone_states = ctl.receive_block()
|
||||
|
||||
for zone, info in zone_states.items():
|
||||
serial = info.get('serial', False)
|
||||
if serial and serial != "none" and serial != "-":
|
||||
metric_families_append('knot_zone_serial', ['zone'], [zone], int(serial))
|
||||
|
||||
metrics = ['expiration', 'refresh']
|
||||
|
||||
for metric in metrics:
|
||||
seconds = KnotCollector.convert_state_time(info[metric])
|
||||
if seconds == None:
|
||||
continue
|
||||
|
||||
metric_families_append('knot_zone_stats_' + metric, ['zone'], [zone], seconds)
|
||||
|
||||
if self.collect_zone_timers:
|
||||
# zone configuration metrics
|
||||
ctl.send_block(cmd="zone-read", rtype="SOA")
|
||||
zones = ctl.receive_block()
|
||||
|
||||
for name, params in zones.items():
|
||||
metrics = [
|
||||
{"name": "knot_zone_refresh", "index": 3},
|
||||
{"name": "knot_zone_retry", "index": 4},
|
||||
{"name": "knot_zone_expiration", "index": 5},
|
||||
]
|
||||
|
||||
zone_config = params[name]['SOA']['data'][0].split(" ")
|
||||
|
||||
for metric in metrics:
|
||||
metric_families_append(metric['name'], ['zone'], [name], int(zone_config[metric['index']]))
|
||||
|
||||
for val in metric_families.values():
|
||||
yield val
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--web-listen-addr",
|
||||
default="127.0.0.1",
|
||||
help="address on which to expose metrics."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--web-listen-port",
|
||||
type=int,
|
||||
default=9433,
|
||||
help="port on which to expose metrics."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--knot-library-path",
|
||||
default=None,
|
||||
help="path to libknot."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--knot-socket-path",
|
||||
default="/run/knot/knot.sock",
|
||||
help="path to knot control socket."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--knot-socket-timeout",
|
||||
type=int,
|
||||
default=2000,
|
||||
help="timeout for Knot control socket operations."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-meminfo",
|
||||
action='store_false',
|
||||
help="disable collection of memory usage"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-global-stats",
|
||||
action='store_false',
|
||||
help="disable collection of global statistics"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-zone-stats",
|
||||
action='store_false',
|
||||
help="disable collection of zone statistics"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-zone-status",
|
||||
action='store_false',
|
||||
help="disable collection of zone status"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-zone-timers",
|
||||
action='store_false',
|
||||
help="disable collection of zone timer settings"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
REGISTRY.register(KnotCollector(
|
||||
args.knot_library_path,
|
||||
args.knot_socket_path,
|
||||
args.knot_socket_timeout,
|
||||
args.no_meminfo,
|
||||
args.no_global_stats,
|
||||
args.no_zone_stats,
|
||||
args.no_zone_status,
|
||||
args.no_zone_timers,
|
||||
))
|
||||
|
||||
class Server(http.server.HTTPServer):
|
||||
def __init__(self, server_address, RequestHandlerClass):
|
||||
ip = ipaddress.ip_address(server_address[0])
|
||||
self.address_family = socket.AF_INET6 if ip.version == 6 else socket.AF_INET
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
|
||||
httpd = Server(
|
||||
(args.web_listen_addr, args.web_listen_port),
|
||||
MetricsHandler,
|
||||
)
|
||||
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
46
python/knot_exporter/pyproject.toml.in
Normal file
46
python/knot_exporter/pyproject.toml.in
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "knot_exporter"
|
||||
version = "@PACKAGE_VERSION@"
|
||||
description = "Prometheus exporter for Knot DNS"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.5"
|
||||
license = { text = "GPL-3.0" }
|
||||
authors = [
|
||||
{ name = "CZ.NIC, z.s.p.o.", email = "knot-dns@labs.nic.cz" },
|
||||
{ name = "Alessandro Ghedini", email = "alessandro@ghedini.me" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Internet :: Name Service (DNS)",
|
||||
"Topic :: System :: Systems Administration",
|
||||
]
|
||||
dependencies = [
|
||||
"libknot",
|
||||
"prometheus-client",
|
||||
"psutil",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://www.knot-dns.cz/documentation"
|
||||
Issues = "https://gitlab.nic.cz/knot/knot-dns/-/issues"
|
||||
Source = "https://gitlab.nic.cz/knot/knot-dns/python/knot_exporter"
|
||||
|
||||
[project.scripts]
|
||||
knot-exporter = "knot_exporter.knot_exporter:main"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
artifacts = [
|
||||
"knot_exporter/__init__.py",
|
||||
]
|
||||
exclude = [
|
||||
".*",
|
||||
"*.in",
|
||||
"Makefile*",
|
||||
]
|
||||
38
python/knot_exporter/setup.py.in
Normal file
38
python/knot_exporter/setup.py.in
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import pathlib
|
||||
import setuptools
|
||||
|
||||
p = pathlib.Path("README.md")
|
||||
if p.exists():
|
||||
long_description = p.read_text()
|
||||
|
||||
setuptools.setup(
|
||||
name='knot_exporter',
|
||||
version='@PACKAGE_VERSION@',
|
||||
description='Prometheus exporter for Knot DNS',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author='CZ.NIC, z.s.p.o.',
|
||||
author_email='knot-dns@labs.nic.cz',
|
||||
url='https://gitlab.nic.cz/knot/knot-dns/python/knot_exporter',
|
||||
license='GPL-3.0',
|
||||
packages=['knot_exporter'],
|
||||
classifiers=[ # See https://pypi.org/classifiers
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Internet :: Name Service (DNS)',
|
||||
'Topic :: System :: Systems Administration',
|
||||
],
|
||||
python_requires='>=3.5',
|
||||
install_requires=[
|
||||
'libknot',
|
||||
'prometheus-client',
|
||||
'psutil',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'knot-exporter = knot_exporter.knot_exporter:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
Loading…
Reference in a new issue