Skip to content

Commit

Permalink
Add config option for customizing scraped metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
mbugert committed May 3, 2020
1 parent 73ad247 commit 090443c
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 28 deletions.
7 changes: 5 additions & 2 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ ip_address: 192.168.0.1
# Connect Box web interface password
password: WhatEverYourPasswordIs

# The following parameters are optional:
# All following parameters are optional.
#exporter:
# port on which this exporter exposes metrics (default: 9705)
#port: 9705

# timeout duration for connections to the Connect Box (default: 9)
#timeout_seconds: 9
#timeout_seconds: 9

# Customize the family of metrics to scrape. By default, all metrics are scraped.
#metrics: [device_status, downstream, upstream, lan_users, temperature]
28 changes: 24 additions & 4 deletions connectbox_exporter/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
from pathlib import Path
import deepmerge
from typing import Union, Dict

from deepmerge import Merger
from ruamel.yaml import YAML

from connectbox_exporter.xml2metric import (
DEVICE_STATUS,
DOWNSTREAM,
UPSTREAM,
LAN_USERS,
TEMPERATURE,
)

IP_ADDRESS = "ip_address"
PASSWORD = "password"
EXPORTER = "exporter"
PORT = "port"
TIMEOUT_SECONDS = "timeout_seconds"
EXTRACTORS = "metrics"

# pick default timeout one second less than the default prometheus timeout of 10s
DEFAULT_CONFIG = {EXPORTER: {PORT: 9705, TIMEOUT_SECONDS: 9}}
DEFAULT_CONFIG = {
EXPORTER: {
PORT: 9705,
TIMEOUT_SECONDS: 9,
EXTRACTORS: {DEVICE_STATUS, DOWNSTREAM, UPSTREAM, LAN_USERS, TEMPERATURE},
}
}


def load_config(config_file: Union[str, Path]) -> Dict:
Expand All @@ -24,8 +39,9 @@ def load_config(config_file: Union[str, Path]) -> Dict:
with open(config_file) as f:
config = yaml.load(f)

# merge with defaults
config = deepmerge.always_merger.merge(DEFAULT_CONFIG, config)
# merge with default config: use 'override' for lists to let users replace extractor setting entirely
merger = Merger([(list, "override"), (dict, "merge")], ["override"], ["override"])
config = merger.merge(DEFAULT_CONFIG, config)

for param in [IP_ADDRESS, PASSWORD]:
if not param in config:
Expand All @@ -37,4 +53,8 @@ def load_config(config_file: Union[str, Path]) -> Dict:
if config[EXPORTER][PORT] < 0 or config[EXPORTER][PORT] > 65535:
raise ValueError(f"Invalid exporter port.")

if not config[EXPORTER][EXTRACTORS]:
raise ValueError("At least one family of metrics needs to be specified.")
config[EXPORTER][EXTRACTORS] = sorted(set(config[EXPORTER][EXTRACTORS]))

return config
30 changes: 13 additions & 17 deletions connectbox_exporter/connectbox_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,27 @@
EXPORTER,
PORT,
TIMEOUT_SECONDS,
EXTRACTORS,
)
from connectbox_exporter.logger import get_logger, VerboseLogger
from connectbox_exporter.xml2metric import (
TemperatureExtractor,
LanUserExtractor,
UpstreamStatusExtractor,
DownstreamStatusExtractor,
DeviceStatusExtractor,
)
from connectbox_exporter.xml2metric import get_metrics_extractor


class ConnectBoxCollector(object):
def __init__(
self, logger: VerboseLogger, ip_address: str, password: str, timeout: float
self,
logger: VerboseLogger,
ip_address: str,
password: str,
exporter_config: Dict,
):
self.logger = logger
self.ip_address = ip_address
self.password = password
self.timeout = timeout
self.metric_extractors = [
DeviceStatusExtractor(),
TemperatureExtractor(),
LanUserExtractor(),
DownstreamStatusExtractor(),
UpstreamStatusExtractor(),
]
self.timeout = exporter_config[TIMEOUT_SECONDS]

extractors = exporter_config[EXTRACTORS]
self.metric_extractors = [get_metrics_extractor(e) for e in extractors]

def collect(self):
# Collect scrape duration and scrape success for each extractor. Scrape success is initialized with False for
Expand Down Expand Up @@ -87,6 +82,7 @@ def collect(self):
scrape_success[extractor.name] = True
except (XMLSyntaxError, AttributeError) as e:
# in case of a less serious error, log and continue scraping the next extractor
# TODO make this more useful: log in which extractor the error happened and print the xml at fault
self.logger.error(repr(e))
except (ConnectionError, Timeout) as e:
# in case of serious connection issues, abort and do not try the next extractor
Expand Down Expand Up @@ -152,7 +148,7 @@ def main(config_file, verbose):
logger,
ip_address=config[IP_ADDRESS],
password=config[PASSWORD],
timeout=exporter_config[TIMEOUT_SECONDS],
exporter_config=config[EXPORTER],
)
)

Expand Down
37 changes: 32 additions & 5 deletions connectbox_exporter/xml2metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
StateSetMetricFamily,
)

DEVICE_STATUS = "device_status"
DOWNSTREAM = "downstream"
UPSTREAM = "upstream"
LAN_USERS = "lan_users"
TEMPERATURE = "temperature"


class XmlMetricsExtractor:

Expand Down Expand Up @@ -63,7 +69,7 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
class DownstreamStatusExtractor(XmlMetricsExtractor):
def __init__(self):
super(DownstreamStatusExtractor, self).__init__(
"downstream", {GET.DOWNSTREAM_TABLE, GET.SIGNAL_TABLE}
DOWNSTREAM, {GET.DOWNSTREAM_TABLE, GET.SIGNAL_TABLE}
)

def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
Expand Down Expand Up @@ -152,7 +158,7 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:

class UpstreamStatusExtractor(XmlMetricsExtractor):
def __init__(self):
super(UpstreamStatusExtractor, self).__init__("upstream", {GET.UPSTREAM_TABLE})
super(UpstreamStatusExtractor, self).__init__(UPSTREAM, {GET.UPSTREAM_TABLE})

def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
assert len(raw_xmls) == 1
Expand Down Expand Up @@ -210,7 +216,7 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:

class LanUserExtractor(XmlMetricsExtractor):
def __init__(self):
super(LanUserExtractor, self).__init__("lan_users", {GET.LANUSERTABLE})
super(LanUserExtractor, self).__init__(LAN_USERS, {GET.LANUSERTABLE})

def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
assert len(raw_xmls) == 1
Expand Down Expand Up @@ -266,7 +272,7 @@ def extract_client(client, target_metric: GaugeMetricFamily):

class TemperatureExtractor(XmlMetricsExtractor):
def __init__(self):
super(TemperatureExtractor, self).__init__("temperature", {GET.CMSTATE})
super(TemperatureExtractor, self).__init__(TEMPERATURE, {GET.CMSTATE})

def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
assert len(raw_xmls) == 1
Expand Down Expand Up @@ -302,7 +308,7 @@ class ProvisioningStatus(Enum):
class DeviceStatusExtractor(XmlMetricsExtractor):
def __init__(self):
super(DeviceStatusExtractor, self).__init__(
"device_status", {GET.GLOBALSETTINGS, GET.CM_SYSTEM_INFO, GET.CMSTATUS}
DEVICE_STATUS, {GET.GLOBALSETTINGS, GET.CM_SYSTEM_INFO, GET.CMSTATUS}
)

def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
Expand Down Expand Up @@ -380,3 +386,24 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]:
unit="seconds",
value=uptime_seconds,
)


def get_metrics_extractor(ident: str):
"""
Factory method for metrics extractors.
:param ident: metric extractor identifier
:return: extractor instance
"""
extractors = {
DEVICE_STATUS: DeviceStatusExtractor,
DOWNSTREAM: DownstreamStatusExtractor,
UPSTREAM: UpstreamStatusExtractor,
LAN_USERS: LanUserExtractor,
TEMPERATURE: TemperatureExtractor,
}
if not ident in extractors.keys():
raise ValueError(
f"Unknown extractor '{ident}', supported are: {','.join(extractors.keys())}"
)
cls = extractors[ident]
return cls()

0 comments on commit 090443c

Please sign in to comment.