Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to serve metrics via http #20

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ If the directory is not already present, it will be created by the exporter.
The command `pkg_exporter` provided by the package or the binary shall be executed in a appropriate interval, e.g. using cron or systemd timers.
The exporter needs to be executed with appropriate privileges, which are not necessarily root privileges.

Several command line options can be used along with the `pkg_exporter` command, which are as follows:
| Option | Description |
| ------- | --------- |
| -f, --exporter-file | The file where prometheus metrics are exported, default is `/var/prometheus/pkg-exporter.prom` |
| -d, --daemon | Run as daemon and serve metrics via http. |
| -a, --bind-addr | The address to serve metrics via http. Default is `0.0.0.0` |
| -p, --port | The port to serve metrics via http. Default is `8089` |
| -r, --rootdir | Custom root directory for dpkg, if any. |
| -t, --time-wait | Time, in seconds, to wait between metrics updates. Default is 300 |

An example configuration will be provided in this repository in the future.

### apt hook
Expand Down
6 changes: 4 additions & 2 deletions src/pkg_exporter/pkgmanager/apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@


class AptPkgManager:
def __init__(self):
def __init__(self, rootdir=None):
self.rootdir = rootdir

self.metricDict = {}
self.metricDict["installed"] = {"description": "Installed packages per origin"}
self.metricDict["upgradable"] = {
Expand All @@ -21,7 +23,7 @@ def __init__(self):
self.metaMetrics["update_start_time"] = 0
self.metaMetrics["update_end_time"] = 0

self.cache = apt.Cache()
self.cache = apt.Cache(rootdir=self.rootdir)
self.cache.open(None)

def labelValues(self, origin):
Expand Down
114 changes: 101 additions & 13 deletions src/pkg_exporter/textfile.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
#!/usr/bin/env python3

from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
from prometheus_client import Gauge, write_to_textfile, start_http_server
from prometheus_client import GC_COLLECTOR, PLATFORM_COLLECTOR, PROCESS_COLLECTOR # noqa E501
from prometheus_client.core import REGISTRY
from time import sleep
from pkg_exporter.pkgmanager import apt
from pkg_exporter import reboot
import argparse
import os


def main():
registry = CollectorRegistry()

def populate_registry(rootdir=None):
# get updates from apt
pkgmanager = apt.AptPkgManager()
pkgmanager = apt.AptPkgManager(rootdir=rootdir)

# initially, check which metrics and labels are available
metrics = pkgmanager.getMetricDict()
Expand All @@ -20,20 +22,27 @@ def main():

# also add reboot metrics
rebootmanager = reboot.RebootManager()
reboot_gauge = Gauge(
"pkg_reboot_required", "Node Requires an Reboot", [], registry=registry
)
reboot_gauge = REGISTRY._names_to_collectors.get(
"pkg_reboot_required", None)
if not reboot_gauge:
reboot_gauge = Gauge(
"pkg_reboot_required",
"Node Requires an Reboot",
[])

# add update statistics
meta_metric = pkgmanager.getMetaMetricDict()
for key, value in meta_metric.items():
meta_gauges[key] = Gauge(f"pkg_{key}", value["description"], registry=registry)
meta_gauges[key] = REGISTRY._names_to_collectors.get(
f"pkg_{key}", None)
if not meta_gauges[key]:
meta_gauges[key] = Gauge(f"pkg_{key}", value["description"])

# Create all the gauge metrics
for key, value in metrics.items():
gauges[key] = Gauge(
f"pkg_{key}", value["description"], labels, registry=registry
)
gauges[key] = REGISTRY._names_to_collectors.get(f"pkg_{key}", None)
if not gauges[key]:
gauges[key] = Gauge(f"pkg_{key}", value["description"], labels)

# let the pkgmanager query its internal metrics
pkgmanager.query()
Expand All @@ -50,12 +59,91 @@ def main():
rebootmanager.query()
reboot_gauge.set(rebootmanager.getMetricValue())

exporter_file = os.getenv("PKG_EXPORTER_FILE", "/var/prometheus/pkg-exporter.prom")

def write_registry_to_file(registry, exporter_file=None):
if not exporter_file:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is duplicate, exporter_file is already filled with a default in line 87

exporter_file = os.getenv(
"PKG_EXPORTER_FILE", "/var/prometheus/pkg-exporter.prom"
)
exporter_dir = os.path.dirname(exporter_file)
os.makedirs(exporter_dir, exist_ok=True)

write_to_textfile(exporter_file, registry)


def serve(addr, port, timewait, rootdir):
start_http_server(addr=addr, port=port)
while True:
sleep(timewait)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a more elegant way of solving this? I'm not a fan of a static sleep, especially since this does not include the time "populate_registry" takes.

populate_registry(rootdir)


def processArgs():
parser = argparse.ArgumentParser(
description="Collect metrics from apt and export it as a service"
)
group = parser.add_mutually_exclusive_group()

group.add_argument(
"-f",
"--exporter-file",
type=str,
default=os.getenv(
"PKG_EXPORTER_FILE",
"/var/prometheus/pkg-exporter.prom"),
help="File to export, if used the content will not be served",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, the default is "output to a file" anyway? The comment indicates that by default the HTTP Server is run.

)
group.add_argument(
"-d",
"--daemon",
action="store_true",
help="Run as daemon and server metrics via http",
)
parser.add_argument(
"-a",
"--bind-addr",
type=str,
default=os.getenv("PKG_EXPORTER_ADDR", "0.0.0.0"),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should bind to a dualstack address, probably ::

help="Bind address",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help should display the default bind address.

)
parser.add_argument(
"-p",
"--port",
type=int,
default=os.getenv("PKG_EXPORTER_PORT", 8089),
help="Bind port",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help should display the default port

)
parser.add_argument(
"-r",
"--rootdir",
type=str,
default=os.getenv("PKG_EXPORTER_ROOT_DIR", None),
help="Custom root directory for dpkg",
)
parser.add_argument(
"-t",
"--time-wait",
type=int,
default=os.getenv("PKG_EXPORTER_TIME_WAIT", 300),
help="time (in second) to wait between data updates",
)
return parser.parse_args()


def main():
args = processArgs()

REGISTRY.unregister(GC_COLLECTOR)
REGISTRY.unregister(PLATFORM_COLLECTOR)
REGISTRY.unregister(PROCESS_COLLECTOR)

populate_registry(args.rootdir)

if not args.daemon:
write_registry_to_file(REGISTRY, args.exporter_file)
else:
serve(args.bind_addr, args.port, args.time_wait, args.rootdir)


if __name__ == "__main__":
main()
Loading