Upload files to "bgw210-prom-collector"
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
bgw210-700_prom_collector.py
|
||||
=============================
|
||||
|
||||
Prometheus collector script to pull stats from AT&T / Arris BGW210-700 and
|
||||
expose them as Prometheus metrics.
|
||||
|
||||
Tested With
|
||||
-----------
|
||||
|
||||
* AT&T/Arris BGW210-700 (Software 1.5.12 / Hardware 02001C0046004D)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- requests
|
||||
- lxml
|
||||
- prometheus-client
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Run as a standalone HTTP server:
|
||||
python3 bgw210-700_prom_collector.py --port 8000 --ip 192.168.1.254
|
||||
|
||||
Then scrape metrics from http://localhost:8000/metrics
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright 2025 Jason Antman <[email protected]> <http://www.jasonantman.com>
|
||||
Free for any use provided that patches are submitted back to me.
|
||||
|
||||
The latest version of this script can be found at:
|
||||
<https://github.com/jantman/misc-scripts/blob/master/bgw210-700_prom_collector.py>
|
||||
|
||||
CHANGELOG
|
||||
---------
|
||||
|
||||
2025-05-29 Jason Antman <[email protected]>:
|
||||
- initial version of script based on bgw210-700_to_graphite.py
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from lxml import etree
|
||||
from prometheus_client import start_http_server, Gauge, Counter, REGISTRY
|
||||
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily
|
||||
|
||||
FORMAT = "[%(asctime)s %(levelname)s] %(message)s"
|
||||
logging.basicConfig(level=logging.WARNING, format=FORMAT)
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
class BGW210700Collector:
|
||||
"""
|
||||
Prometheus collector for BGW210-700 modem metrics
|
||||
"""
|
||||
|
||||
def __init__(self, modem_ip):
|
||||
"""
|
||||
Initialize the collector
|
||||
|
||||
:param modem_ip: modem IP address
|
||||
:type modem_ip: str
|
||||
"""
|
||||
self.modem_ip = modem_ip
|
||||
self.stats_getter = BGW210700StatsGetter(modem_ip)
|
||||
|
||||
def collect(self):
|
||||
"""
|
||||
Collect metrics from the modem and yield them
|
||||
"""
|
||||
try:
|
||||
stats = self.stats_getter.get_stats()
|
||||
|
||||
# Create gauge metrics
|
||||
connection_status = GaugeMetricFamily(
|
||||
'bgw210_connection_status',
|
||||
'Broadband connection status (1=up, 0=down)'
|
||||
)
|
||||
connection_status.add_metric([], stats.get('connection', 0))
|
||||
yield connection_status
|
||||
|
||||
uptime_seconds = GaugeMetricFamily(
|
||||
'bgw210_uptime_seconds',
|
||||
'Time since last reboot in seconds'
|
||||
)
|
||||
uptime_seconds.add_metric([], stats.get('uptime_sec', 0))
|
||||
yield uptime_seconds
|
||||
|
||||
line_speed_mbps = GaugeMetricFamily(
|
||||
'bgw210_line_speed_mbps',
|
||||
'Current line speed in Mbps'
|
||||
)
|
||||
line_speed_mbps.add_metric([], stats.get('line_speed', 0))
|
||||
yield line_speed_mbps
|
||||
|
||||
line_state = GaugeMetricFamily(
|
||||
'bgw210_line_state',
|
||||
'Line state (1=up, 0=down)'
|
||||
)
|
||||
line_state.add_metric([], stats.get('line_state', 0))
|
||||
yield line_state
|
||||
|
||||
duplex_status = GaugeMetricFamily(
|
||||
'bgw210_duplex_status',
|
||||
'Duplex status (1=full, 0.5=half)'
|
||||
)
|
||||
duplex_status.add_metric([], stats.get('duplex', 0))
|
||||
yield duplex_status
|
||||
|
||||
# Create counter metrics for IPv4
|
||||
ipv4_rx_bytes = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_bytes_total',
|
||||
'IPv4 received bytes total'
|
||||
)
|
||||
ipv4_rx_bytes.add_metric([], stats.get('ipv4.rx_bytes', 0))
|
||||
yield ipv4_rx_bytes
|
||||
|
||||
ipv4_tx_bytes = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_bytes_total',
|
||||
'IPv4 transmitted bytes total'
|
||||
)
|
||||
ipv4_tx_bytes.add_metric([], stats.get('ipv4.tx_bytes', 0))
|
||||
yield ipv4_tx_bytes
|
||||
|
||||
ipv4_rx_packets = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_packets_total',
|
||||
'IPv4 received packets total'
|
||||
)
|
||||
ipv4_rx_packets.add_metric([], stats.get('ipv4.rx_packets', 0))
|
||||
yield ipv4_rx_packets
|
||||
|
||||
ipv4_tx_packets = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_packets_total',
|
||||
'IPv4 transmitted packets total'
|
||||
)
|
||||
ipv4_tx_packets.add_metric([], stats.get('ipv4.tx_packets', 0))
|
||||
yield ipv4_tx_packets
|
||||
|
||||
ipv4_rx_unicast = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_unicast_total',
|
||||
'IPv4 received unicast packets total'
|
||||
)
|
||||
ipv4_rx_unicast.add_metric([], stats.get('ipv4.rx_unicast', 0))
|
||||
yield ipv4_rx_unicast
|
||||
|
||||
ipv4_tx_unicast = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_unicast_total',
|
||||
'IPv4 transmitted unicast packets total'
|
||||
)
|
||||
ipv4_tx_unicast.add_metric([], stats.get('ipv4.tx_unicast', 0))
|
||||
yield ipv4_tx_unicast
|
||||
|
||||
ipv4_rx_multicast = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_multicast_total',
|
||||
'IPv4 received multicast packets total'
|
||||
)
|
||||
ipv4_rx_multicast.add_metric([], stats.get('ipv4.rx_multicast', 0))
|
||||
yield ipv4_rx_multicast
|
||||
|
||||
ipv4_tx_multicast = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_multicast_total',
|
||||
'IPv4 transmitted multicast packets total'
|
||||
)
|
||||
ipv4_tx_multicast.add_metric([], stats.get('ipv4.tx_multicast', 0))
|
||||
yield ipv4_tx_multicast
|
||||
|
||||
ipv4_rx_errors = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_errors_total',
|
||||
'IPv4 receive errors total'
|
||||
)
|
||||
ipv4_rx_errors.add_metric([], stats.get('ipv4.rx_errors', 0))
|
||||
yield ipv4_rx_errors
|
||||
|
||||
ipv4_tx_errors = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_errors_total',
|
||||
'IPv4 transmit errors total'
|
||||
)
|
||||
ipv4_tx_errors.add_metric([], stats.get('ipv4.tx_errors', 0))
|
||||
yield ipv4_tx_errors
|
||||
|
||||
ipv4_rx_drops = CounterMetricFamily(
|
||||
'bgw210_ipv4_rx_drops_total',
|
||||
'IPv4 receive drops total'
|
||||
)
|
||||
ipv4_rx_drops.add_metric([], stats.get('ipv4.rx_drops', 0))
|
||||
yield ipv4_rx_drops
|
||||
|
||||
ipv4_tx_drops = CounterMetricFamily(
|
||||
'bgw210_ipv4_tx_drops_total',
|
||||
'IPv4 transmit drops total'
|
||||
)
|
||||
ipv4_tx_drops.add_metric([], stats.get('ipv4.tx_drops', 0))
|
||||
yield ipv4_tx_drops
|
||||
|
||||
ipv4_collisions = CounterMetricFamily(
|
||||
'bgw210_ipv4_collisions_total',
|
||||
'IPv4 collisions total'
|
||||
)
|
||||
ipv4_collisions.add_metric([], stats.get('ipv4.collisions', 0))
|
||||
yield ipv4_collisions
|
||||
|
||||
# Create counter metrics for IPv6
|
||||
ipv6_tx_packets = CounterMetricFamily(
|
||||
'bgw210_ipv6_tx_packets_total',
|
||||
'IPv6 transmitted packets total'
|
||||
)
|
||||
ipv6_tx_packets.add_metric([], stats.get('ipv6.tx_packets', 0))
|
||||
yield ipv6_tx_packets
|
||||
|
||||
ipv6_tx_errors = CounterMetricFamily(
|
||||
'bgw210_ipv6_tx_errors_total',
|
||||
'IPv6 transmit errors total'
|
||||
)
|
||||
ipv6_tx_errors.add_metric([], stats.get('ipv6.tx_errors', 0))
|
||||
yield ipv6_tx_errors
|
||||
|
||||
ipv6_tx_discards = CounterMetricFamily(
|
||||
'bgw210_ipv6_tx_discards_total',
|
||||
'IPv6 transmit discards total'
|
||||
)
|
||||
ipv6_tx_discards.add_metric([], stats.get('ipv6.tx_discards', 0))
|
||||
yield ipv6_tx_discards
|
||||
|
||||
logger.debug('Successfully collected metrics from modem')
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error collecting metrics from modem: %s', e)
|
||||
# Yield a connection status metric set to 0 on error
|
||||
connection_status = GaugeMetricFamily(
|
||||
'bgw210_connection_status',
|
||||
'Broadband connection status (1=up, 0=down)'
|
||||
)
|
||||
connection_status.add_metric([], 0)
|
||||
yield connection_status
|
||||
|
||||
|
||||
class BGW210700StatsGetter:
|
||||
"""
|
||||
Class to get statistics from BGW210-700 modem
|
||||
"""
|
||||
|
||||
def __init__(self, ip):
|
||||
"""
|
||||
:param ip: modem IP address
|
||||
:type ip: str
|
||||
"""
|
||||
logger.debug('Initializing BGW210700StatsGetter ip=%s', ip)
|
||||
self.ip = ip
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get statistics from modem; return a dict
|
||||
|
||||
:return: stats
|
||||
:rtype: dict
|
||||
"""
|
||||
res = {}
|
||||
res.update(self._sysinfo())
|
||||
res.update(self._broadband_stats())
|
||||
return res
|
||||
|
||||
def _get_page(self, url):
|
||||
logger.debug('GET: %s', url)
|
||||
r = requests.get(url, timeout=10)
|
||||
r.raise_for_status()
|
||||
tree = etree.HTML(r.text)
|
||||
return tree
|
||||
|
||||
def _do_kv_table(self, table):
|
||||
res = {}
|
||||
for tr in table.iter('tr'):
|
||||
if len(tr) != 2 or tr[0].tag != 'th' or tr[1].tag != 'td':
|
||||
continue
|
||||
if tr[1].text is not None:
|
||||
res[tr[0].text.strip()] = tr[1].text.strip()
|
||||
return res
|
||||
|
||||
def _broadband_stats(self):
|
||||
tree = self._get_page(
|
||||
'http://%s/cgi-bin/broadbandstatistics.ha' % self.ip
|
||||
)
|
||||
data = {}
|
||||
for tbl in tree.iter('table'):
|
||||
summary = tbl.get('summary', None)
|
||||
if summary is None:
|
||||
continue
|
||||
data[summary] = self._do_kv_table(tbl)
|
||||
|
||||
bs = 'Summary of the most important WAN information'
|
||||
|
||||
# Parse line state correctly
|
||||
line_state_text = data.get('Ethernet Statistics Table', {}).get('Current Status', 'Down')
|
||||
line_state = 1 if line_state_text == 'Up' else 0
|
||||
|
||||
return {
|
||||
'connection': 1 if data[bs]['Broadband Connection'] == 'Up' else 0,
|
||||
'ipv6.tx_discards': int(
|
||||
data['IPv6 Statistics Table']['Transmit Discards']
|
||||
),
|
||||
'ipv6.tx_errors': int(
|
||||
data['IPv6 Statistics Table']['Transmit Errors']
|
||||
),
|
||||
'ipv6.tx_packets': int(
|
||||
data['IPv6 Statistics Table']['Transmit Packets']
|
||||
),
|
||||
'ipv4.rx_drops': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Drops']
|
||||
),
|
||||
'ipv4.tx_drops': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Drops']
|
||||
),
|
||||
'ipv4.rx_unicast': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Unicast']
|
||||
),
|
||||
'ipv4.tx_bytes': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Bytes']
|
||||
),
|
||||
'ipv4.rx_errors': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Errors']
|
||||
),
|
||||
'ipv4.rx_bytes': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Bytes']
|
||||
),
|
||||
'ipv4.tx_packets': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Packets']
|
||||
),
|
||||
'ipv4.rx_packets': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Packets']
|
||||
),
|
||||
'ipv4.collisions': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Collisions']
|
||||
),
|
||||
'ipv4.rx_multicast': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Receive Multicast']
|
||||
),
|
||||
'ipv4.tx_multicast': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Multicast']
|
||||
),
|
||||
'ipv4.tx_unicast': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Unicast']
|
||||
),
|
||||
'ipv4.tx_errors': int(
|
||||
data['Ethernet IPv4 Statistics Table']['Transmit Errors']
|
||||
),
|
||||
'line_speed': int(
|
||||
data['Ethernet Statistics Table']['Current Speed (Mbps)']
|
||||
),
|
||||
'line_state': line_state,
|
||||
'duplex': 1 if data[
|
||||
'Ethernet Statistics Table'
|
||||
]['Current Duplex'] == 'full' else 0.5
|
||||
}
|
||||
|
||||
def _sysinfo(self):
|
||||
tree = self._get_page('http://%s/cgi-bin/sysinfo.ha' % self.ip)
|
||||
info = self._do_kv_table(tree.xpath('//table')[0])
|
||||
return {
|
||||
'uptime_sec': int(info['Time Since Last Reboot'])
|
||||
}
|
||||
|
||||
|
||||
def set_log_info():
|
||||
"""set logger level to INFO"""
|
||||
set_log_level_format(logging.INFO,
|
||||
'%(asctime)s %(levelname)s:%(name)s:%(message)s')
|
||||
|
||||
|
||||
def set_log_debug():
|
||||
"""set logger level to DEBUG, and debug-level output format"""
|
||||
set_log_level_format(
|
||||
logging.DEBUG,
|
||||
"%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - "
|
||||
"%(name)s.%(funcName)s() ] %(message)s"
|
||||
)
|
||||
|
||||
|
||||
def set_log_level_format(level, format):
|
||||
"""
|
||||
Set logger level and format.
|
||||
|
||||
:param level: logging level; see the :py:mod:`logging` constants.
|
||||
:type level: int
|
||||
:param format: logging formatter format string
|
||||
:type format: str
|
||||
"""
|
||||
formatter = logging.Formatter(fmt=format)
|
||||
logger.handlers[0].setFormatter(formatter)
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
p = argparse.ArgumentParser(
|
||||
description='BGW210-700 Prometheus metrics collector'
|
||||
)
|
||||
p.add_argument('-v', '--verbose', dest='verbose', action='count', default=0,
|
||||
help='verbose output. specify twice for debug-level output.')
|
||||
p.add_argument('-p', '--port', dest='port', type=int,
|
||||
action='store', default=8000,
|
||||
help='HTTP server port (default: 8000)')
|
||||
p.add_argument('-i', '--ip', dest='modem_ip', action='store', type=str,
|
||||
default='192.168.1.254',
|
||||
help='Modem IP address (default: 192.168.1.254)')
|
||||
p.add_argument('--host', dest='host', action='store', type=str,
|
||||
default='0.0.0.0',
|
||||
help='HTTP server bind address (default: 0.0.0.0)')
|
||||
args = p.parse_args(argv)
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args(sys.argv[1:])
|
||||
|
||||
# set logging level
|
||||
if args.verbose > 1:
|
||||
set_log_debug()
|
||||
elif args.verbose > 0:
|
||||
set_log_info()
|
||||
|
||||
# Create and register the collector
|
||||
collector = BGW210700Collector(args.modem_ip)
|
||||
REGISTRY.register(collector)
|
||||
|
||||
# Start the HTTP server
|
||||
logger.info('Starting HTTP server on %s:%d', args.host, args.port)
|
||||
start_http_server(args.port, addr=args.host)
|
||||
|
||||
logger.info('Metrics available at http://%s:%d/metrics', args.host, args.port)
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(60)
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Shutting down...')
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user