From e46544b30c3d7f1cd028eb10ec8374d320eb733d Mon Sep 17 00:00:00 2001 From: Michael Bugert Date: Tue, 23 Mar 2021 08:56:24 +0100 Subject: [PATCH 1/2] Add 'DS scanning' provisioning mode. Replace raising with logging. Fixes #8. --- connectbox_exporter/connectbox_exporter.py | 2 +- connectbox_exporter/xml2metric.py | 61 +++++++++++++--------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/connectbox_exporter/connectbox_exporter.py b/connectbox_exporter/connectbox_exporter.py index 8d5f4e2..dab1234 100755 --- a/connectbox_exporter/connectbox_exporter.py +++ b/connectbox_exporter/connectbox_exporter.py @@ -52,7 +52,7 @@ def __init__( self.timeout = exporter_config[TIMEOUT_SECONDS] extractors = exporter_config[EXTRACTORS] - self.metric_extractors = [get_metrics_extractor(e) for e in extractors] + self.metric_extractors = [get_metrics_extractor(e, logger) for e in extractors] def collect(self): # Collect scrape duration and scrape success for each extractor. Scrape success is initialized with False for diff --git a/connectbox_exporter/xml2metric.py b/connectbox_exporter/xml2metric.py index 8a9f034..bc19e21 100644 --- a/connectbox_exporter/xml2metric.py +++ b/connectbox_exporter/xml2metric.py @@ -1,6 +1,7 @@ import re from datetime import timedelta from enum import Enum +from logging import Logger from pathlib import Path from typing import Iterable, Set, Dict @@ -26,8 +27,9 @@ class XmlMetricsExtractor: PROJECT_ROOT = Path(__file__).parent.parent SCHEMA_ROOT = PROJECT_ROOT / "resources" / "schema" - def __init__(self, name: str, functions: Set[int]): + def __init__(self, name: str, functions: Set[int], logger: Logger): self._name = name + self._logger = logger # create one parser per function, use an XML schema if available self._parsers = {} @@ -67,9 +69,9 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: class DownstreamStatusExtractor(XmlMetricsExtractor): - def __init__(self): + def __init__(self, logger: Logger): super(DownstreamStatusExtractor, self).__init__( - DOWNSTREAM, {GET.DOWNSTREAM_TABLE, GET.SIGNAL_TABLE} + DOWNSTREAM, {GET.DOWNSTREAM_TABLE, GET.SIGNAL_TABLE}, logger ) def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: @@ -157,8 +159,8 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: class UpstreamStatusExtractor(XmlMetricsExtractor): - def __init__(self): - super(UpstreamStatusExtractor, self).__init__(UPSTREAM, {GET.UPSTREAM_TABLE}) + def __init__(self, logger: Logger): + super(UpstreamStatusExtractor, self).__init__(UPSTREAM, {GET.UPSTREAM_TABLE}, logger) def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: assert len(raw_xmls) == 1 @@ -215,8 +217,8 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: class LanUserExtractor(XmlMetricsExtractor): - def __init__(self): - super(LanUserExtractor, self).__init__(LAN_USERS, {GET.LANUSERTABLE}) + def __init__(self, logger: Logger): + super(LanUserExtractor, self).__init__(LAN_USERS, {GET.LANUSERTABLE}, logger) def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: assert len(raw_xmls) == 1 @@ -271,8 +273,8 @@ def extract_client(client, target_metric: GaugeMetricFamily): class TemperatureExtractor(XmlMetricsExtractor): - def __init__(self): - super(TemperatureExtractor, self).__init__(TEMPERATURE, {GET.CMSTATE}) + def __init__(self, logger: Logger): + super(TemperatureExtractor, self).__init__(TEMPERATURE, {GET.CMSTATE}, logger) def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: assert len(raw_xmls) == 1 @@ -307,12 +309,17 @@ class ProvisioningStatus(Enum): PARTIAL_SERVICE_DS = "Partial Service (DS only)" PARTIAL_SERVICE_USDS = "Partial Service (US+DS)" MODEM_MODE = "Modem Mode" + DS_SCANNING = "DS scanning" # confirmed to exist + US_SCANNING = "US scanning" # probably exists too + + # default case for all future unknown provisioning status + UNKNOWN = "unknown" class DeviceStatusExtractor(XmlMetricsExtractor): - def __init__(self): + def __init__(self, logger: Logger): super(DeviceStatusExtractor, self).__init__( - DEVICE_STATUS, {GET.GLOBALSETTINGS, GET.CM_SYSTEM_INFO, GET.CMSTATUS} + DEVICE_STATUS, {GET.GLOBALSETTINGS, GET.CM_SYSTEM_INFO, GET.CMSTATUS}, logger ) def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: @@ -327,6 +334,11 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: gw_provision_mode = root.find("GwProvisionMode").text operator_id = root.find("OperatorId").text + # `cm_provision_mode` is known to be None in case `provisioning_status` is "DS scanning". We need to set it to + # some string, otherwise the InfoMetricFamily call fails with AttributeError. + if cm_provision_mode is None: + cm_provision_mode = "Unknown" + # parse cm_system_info root = etree.fromstring( raw_xmls[GET.CM_SYSTEM_INFO], parser=self._parsers[GET.CM_SYSTEM_INFO] @@ -359,10 +371,10 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: # return an enum-style metric for the provisioning status try: enum_provisioning_status = ProvisioningStatus(provisioning_status) - except: - raise ValueError( - f"Unknown provisioning status '{provisioning_status}'. Please open an issue on Github." - ) + except ValueError: + self._logger.warning(f"Unknown provisioning status '{provisioning_status}'. Please open an issue on Github.") + enum_provisioning_status = ProvisioningStatus.UNKNOWN + yield StateSetMetricFamily( "connectbox_provisioning_status", "Provisioning status description", @@ -375,14 +387,12 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: # uptime is reported in a format like "36day(s)15h:24m:58s" which needs parsing uptime_pattern = r"(\d+)day\(s\)(\d+)h:(\d+)m:(\d+)s" m = re.fullmatch(uptime_pattern, uptime_as_str) - if m is None: - raise ValueError( - f"Unexpected duration format '{uptime_as_str}', please open an issue on github." - ) - uptime_timedelta = timedelta( - days=int(m[1]), hours=int(m[2]), minutes=int(m[3]), seconds=int(m[4]) - ) - uptime_seconds = uptime_timedelta.total_seconds() + if m is not None: + uptime_timedelta = timedelta(days=int(m[1]), hours=int(m[2]), minutes=int(m[3]), seconds=int(m[4])) + uptime_seconds = uptime_timedelta.total_seconds() + else: + self._logger.warning(f"Unexpected duration format '{uptime_as_str}', please open an issue on github.") + uptime_seconds = -1 yield GaugeMetricFamily( "connectbox_uptime", @@ -392,10 +402,11 @@ def extract(self, raw_xmls: Dict[int, bytes]) -> Iterable[Metric]: ) -def get_metrics_extractor(ident: str): +def get_metrics_extractor(ident: str, logger: Logger): """ Factory method for metrics extractors. :param ident: metric extractor identifier + :param logger: logging logger :return: extractor instance """ extractors = { @@ -410,4 +421,4 @@ def get_metrics_extractor(ident: str): f"Unknown extractor '{ident}', supported are: {', '.join(extractors.keys())}" ) cls = extractors[ident] - return cls() + return cls(logger) From 08cf5b04db17af362c5e6aa5ac0d7f6c40f0f3c2 Mon Sep 17 00:00:00 2001 From: Michael Bugert Date: Tue, 23 Mar 2021 08:59:39 +0100 Subject: [PATCH 2/2] Bump project version to 0.2.8 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f8ae98..b4f9f5f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="connectbox-prometheus", - version="0.2.7", + version="0.2.8", author="Michael Bugert", author_email="git@mbugert.de", description='Prometheus exporter for Compal CH7465LG cable modems, commonly sold as "Connect Box"',