parent
fe8119d660
commit
b2de356334
4 changed files with 513 additions and 3 deletions
@ -1,3 +1,103 @@ |
||||
# fritzbox_exporter |
||||
|
||||
Exports statistics from FritzBox to Prometheus |
||||
# fritzbox_exporter |
||||
|
||||
Collects data from FRITZ!Box via its SOAP api and exports it to Prometheus via http. |
||||
|
||||
The program is split up into two parts: |
||||
|
||||
## Bash script |
||||
Collects various information from your FRITZ!Box via its SOAP api. It outputs the data to stdout in JSON format. |
||||
|
||||
It can also be used from the command line. Script can output: |
||||
|
||||
- link uptime |
||||
- connection status |
||||
- maximum upstream sync speed on current connection |
||||
- maximum downstream sync speed on current connection |
||||
- Current downstream bandwidth usage |
||||
- Current upstream bandwidth usage |
||||
- Total download usage on current connection |
||||
- Total upload usage on current connection |
||||
|
||||
This bash script is taken from https://github.com/mrwhale/fritzbox-api |
||||
|
||||
### Requirement |
||||
|
||||
The Universal Plug & Play (UPnP) service must be enabled. |
||||
|
||||
You can do that in the settings: Home Network » Network » Network Settings |
||||
|
||||
Enable "Transmit status information over UPnP" |
||||
|
||||
### Usage |
||||
|
||||
```text |
||||
usage: fritz-api.sh [-f <function>] [-h hostname] [-b rate] [-j] [-d] |
||||
-f: function to be executed [Default: bandwidthdown] |
||||
-h: hostname or IP of the FRITZ!Box [Default: fritz.box] |
||||
-b: rate to display. b, k, m. all in bytes |
||||
-j: JSON output |
||||
Does not accept any functions. |
||||
Will display all output in JSON format. |
||||
Useful for running in cron and ingesting into another program |
||||
-d: enable debug output |
||||
|
||||
functions: |
||||
linkuptime connection time in seconds |
||||
connection connection status |
||||
downstream maximum downstream on current connection (Downstream Sync) |
||||
upstream maximum upstream on current connection (Upstream Sync) |
||||
bandwidthdown current bandwidth down |
||||
bandwidthup current bandwidth up |
||||
totalbwdown total downloads |
||||
totalbwup total uploads |
||||
|
||||
Example: fritz-api.sh -f downstream -h 192.168.100.1 -b m |
||||
``` |
||||
|
||||
### Dependancies |
||||
|
||||
- curl |
||||
- bc |
||||
|
||||
## Python program |
||||
|
||||
This is the actual exporter. It takes the output from the bash script, processes it and exposes it via http (default port 9000) to Prometheus. |
||||
|
||||
### Usage |
||||
|
||||
Configure prometheus.yml: |
||||
|
||||
```text |
||||
- job_name: 'FritzBox' |
||||
|
||||
static_configs: |
||||
- targets: ['localhost:9001'] |
||||
``` |
||||
|
||||
Start program as background process: |
||||
|
||||
```text |
||||
python3 ./fritzbox_exporter.py & |
||||
``` |
||||
|
||||
Use Graphana to display the data. |
||||
|
||||
### Configuration |
||||
|
||||
It can be configured via the file config.yml. |
||||
|
||||
```text |
||||
# Set port (default: 9000) |
||||
port: 9001 |
||||
# Set scrape frequency (default: 15 seconds) |
||||
scrape_frequency: 60 |
||||
``` |
||||
|
||||
### Dependancies |
||||
|
||||
- Python 3 |
||||
- subprocess |
||||
- json |
||||
- pyyaml |
||||
- prometheus_client |
||||
|
||||
|
@ -0,0 +1,2 @@ |
||||
port: 9001 |
||||
scrape_frequency: 60 |
@ -0,0 +1,304 @@ |
||||
#!/usr/bin/bash |
||||
|
||||
RC_OK=0 |
||||
RC_WARN=1 |
||||
RC_CRIT=2 |
||||
RC_UNKNOWN=3 |
||||
HOSTNAME="fritz.box" |
||||
CHECK="bandwidthdown" |
||||
MY_SCRIPT_NAME=$(basename "$0") |
||||
# Duration we wait for curl response. |
||||
MY_CURL_TIMEOUT="5" |
||||
|
||||
usage(){ |
||||
echo "usage: $MY_SCRIPT_NAME [-f <function>] [-h hostname] [-b rate] [-j] [-d]" |
||||
echo " -f: function to be executed [Default: ${CHECK}]" |
||||
echo " -h: hostname or IP of the FRITZ!Box [Default: ${HOSTNAME}]" |
||||
echo " -b: rate to display. b, k, m. all in bytes" |
||||
echo " -j: JSON output" |
||||
echo " Does not accept any functions." |
||||
echo " Will display all output in JSON format." |
||||
echo " Useful for running in cron and ingesting into another program" |
||||
echo " -d: enable debug output" |
||||
echo |
||||
echo "functions:" |
||||
echo " linkuptime connection time in seconds" |
||||
echo " connection connection status" |
||||
echo " downstream maximum downstream on current connection (Downstream Sync)" |
||||
echo " upstream maximum upstream on current connection (Upstream Sync)" |
||||
echo " bandwidthdown current bandwidth down" |
||||
echo " bandwidthup current bandwidth up" |
||||
echo " totalbwdown total downloads" |
||||
echo " totalbwup total uploads" |
||||
echo |
||||
echo "Example: $MY_SCRIPT_NAME -f downstream -h 192.168.100.1 -b m" |
||||
exit ${RC_UNKNOWN} |
||||
} |
||||
|
||||
require_number() |
||||
{ |
||||
VAR=$1 |
||||
MSG=$2 |
||||
|
||||
if [[ ! "${VAR}" =~ ^[0-9]+$ ]] ; then |
||||
echo "ERROR - ${MSG} (${VAR})" |
||||
exit ${RC_UNKNOWN} |
||||
fi |
||||
} |
||||
|
||||
find_xml_value() |
||||
{ |
||||
XML=$1 |
||||
VAL=$2 |
||||
|
||||
echo "${XML}" | grep "${VAL}" | sed "s/<${VAL}>\([^<]*\)<\/${VAL}>/\1/" |
||||
} |
||||
|
||||
check_greater() |
||||
{ |
||||
VAL=$1 |
||||
WARN=$2 |
||||
CRIT=$3 |
||||
MSG=$4 |
||||
|
||||
if [ "${VAL}" -gt "${WARN}" ] || [ "${WARN}" -eq 0 ]; then |
||||
echo "OK - ${MSG}" |
||||
exit ${RC_OK} |
||||
elif [ "${VAL}" -gt "${CRIT}" ] || [ "${CRIT}" -eq 0 ]; then |
||||
echo "WARNING - ${MSG}" |
||||
exit ${RC_WARN} |
||||
else |
||||
echo "CRITICAL - ${MSG}" |
||||
exit ${RC_CRIT} |
||||
fi |
||||
} |
||||
|
||||
print_json(){ |
||||
VERB1=GetStatusInfo |
||||
URL1=WANIPConn1 |
||||
NS1=WANIPConnection |
||||
|
||||
VERB2=GetCommonLinkProperties |
||||
URL2=WANCommonIFC1 |
||||
NS2=WANCommonInterfaceConfig |
||||
|
||||
VERB3=GetAddonInfos |
||||
URL3=WANCommonIFC1 |
||||
NS3=WANCommonInterfaceConfig |
||||
|
||||
STATUS1=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL1}" \ |
||||
-H "Content-Type: text/xml; charset=\"utf-8\"" \ |
||||
-H "SoapAction:urn:schemas-upnp-org:service:${NS1}:1#${VERB1}" \ |
||||
-d "<?xml version='1.0' encoding='utf-8'?> <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> <s:Body> <u:${VERB1} xmlns:u=\"urn:schemas-upnp-org:service:${NS1}:1\" /> </s:Body> </s:Envelope>" \ |
||||
-s) |
||||
|
||||
if [ "$?" -ne "0" ]; then |
||||
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}' |
||||
exit ${RC_CRIT} |
||||
fi |
||||
|
||||
|
||||
STATUS2=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL2}" \ |
||||
-H "Content-Type: text/xml; charset=\"utf-8\"" \ |
||||
-H "SoapAction:urn:schemas-upnp-org:service:${NS2}:1#${VERB2}" \ |
||||
-d "<?xml version='1.0' encoding='utf-8'?> <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> <s:Body> <u:${VERB2} xmlns:u=\"urn:schemas-upnp-org:service:${NS2}:1\" /> </s:Body> </s:Envelope>" \ |
||||
-s) |
||||
|
||||
if [ "$?" -ne "0" ]; then |
||||
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}' |
||||
exit ${RC_CRIT} |
||||
fi |
||||
|
||||
STATUS3=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL3}" \ |
||||
-H "Content-Type: text/xml; charset=\"utf-8\"" \ |
||||
-H "SoapAction:urn:schemas-upnp-org:service:${NS3}:1#${VERB3}" \ |
||||
-d "<?xml version='1.0' encoding='utf-8'?> <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> <s:Body> <u:${VERB3} xmlns:u=\"urn:schemas-upnp-org:service:${NS3}:1\" /> </s:Body> </s:Envelope>" \ |
||||
-s) |
||||
|
||||
if [ "$?" -ne "0" ]; then |
||||
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}' |
||||
exit ${RC_CRIT} |
||||
fi |
||||
CONNECTIONSTATUS=$(find_xml_value "${STATUS1}" NewConnectionStatus) |
||||
UPTIME=$(find_xml_value "${STATUS1}" NewUptime) |
||||
DOWNSTREAM=$(find_xml_value "${STATUS2}" NewLayer1DownstreamMaxBitRate) |
||||
UPSTREAM=$(find_xml_value "${STATUS2}" NewLayer1UpstreamMaxBitRate) |
||||
BANDWIDTHDOWNBYTES=$(find_xml_value "${STATUS3}" NewByteReceiveRate) |
||||
BANDWIDTHUPBYTES=$(find_xml_value "${STATUS3}" NewByteSendRate) |
||||
TOTALBWDOWNBYTES=$(find_xml_value "${STATUS3}" NewTotalBytesReceived) |
||||
TOTALBWUPBYTES=$(find_xml_value "${STATUS3}" NewTotalBytesSent) |
||||
if [ "${DEBUG}" -eq 1 ]; then |
||||
echo "DEBUG - Status:" |
||||
echo "$CONNECTIONSTATUS" |
||||
echo "$UPTIME" |
||||
echo "$DOWNSTREAM" |
||||
echo "$UPSTREAM" |
||||
echo "$BANDWIDTHDOWNBYTES" |
||||
echo "$BANDWIDTHUPBYTES" |
||||
echo "$TOTALBWDOWNBYTES" |
||||
echo "$TOTALBWUPBYTES" |
||||
fi |
||||
printf '{"Connection":"%s","Uptime":%d,"UpstreamSync":%d,"DownstreamSync":%d,"UploadBW":%d,"DownloadBW":%d,"TotalUploads":%d,"TotalDownloads":%d}\n' "$CONNECTIONSTATUS" "$UPTIME" "$UPSTREAM" "$DOWNSTREAM" "$BANDWIDTHUPBYTES" "$BANDWIDTHDOWNBYTES" "$TOTALBWUPBYTES" "$TOTALBWDOWNBYTES" |
||||
exit #exit so we dont get unknown service check error |
||||
} |
||||
|
||||
|
||||
# Check Commands |
||||
command -v curl >/dev/null 2>&1 || { echo >&2 "ERROR: 'curl' is needed. Please install 'curl'. More details can be found at https://curl.haxx.se/"; exit 1; } |
||||
command -v bc >/dev/null 2>&1 || { echo >&2 "ERROR: 'bc' is needed. Please install 'bc'."; exit 1; } |
||||
|
||||
PORT=49000 |
||||
DEBUG=0 |
||||
WARN=0 |
||||
CRIT=0 |
||||
RATE=1 |
||||
PRE= |
||||
|
||||
while getopts h:jf:db: OPTNAME; do |
||||
case "${OPTNAME}" in |
||||
h) |
||||
HOSTNAME="${OPTARG}" |
||||
;; |
||||
j) |
||||
CHECK="" |
||||
print_json |
||||
;; |
||||
f) |
||||
CHECK="${OPTARG}" |
||||
;; |
||||
d) |
||||
DEBUG=1 |
||||
;; |
||||
b) |
||||
case "${OPTARG}" in |
||||
b) |
||||
RATE=1 |
||||
PRE= |
||||
;; |
||||
k) |
||||
RATE=1000 |
||||
PRE=kilo |
||||
;; |
||||
m) |
||||
RATE=1000000 |
||||
PRE=mega |
||||
;; |
||||
*) |
||||
echo "Wrong prefix" |
||||
;; |
||||
esac |
||||
;; |
||||
*) |
||||
echo "$OPTNAME" |
||||
usage |
||||
;; |
||||
esac |
||||
done |
||||
|
||||
case ${CHECK} in |
||||
linkuptime|connection) |
||||
VERB=GetStatusInfo |
||||
URL=WANIPConn1 |
||||
NS=WANIPConnection |
||||
;; |
||||
downstream|upstream) |
||||
VERB=GetCommonLinkProperties |
||||
URL=WANCommonIFC1 |
||||
NS=WANCommonInterfaceConfig |
||||
;; |
||||
bandwidthup|bandwidthdown|totalbwup|totalbwdown) |
||||
VERB=GetAddonInfos |
||||
URL=WANCommonIFC1 |
||||
NS=WANCommonInterfaceConfig |
||||
;; |
||||
*) |
||||
echo "ERROR - Unknown service check ${CHECK}" |
||||
exit ${RC_UNKNOWN} |
||||
;; |
||||
esac |
||||
|
||||
STATUS=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL}" \ |
||||
-H "Content-Type: text/xml; charset=\"utf-8\"" \ |
||||
-H "SoapAction:urn:schemas-upnp-org:service:${NS}:1#${VERB}" \ |
||||
-d "<?xml version='1.0' encoding='utf-8'?> <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> <s:Body> <u:${VERB} xmlns:u=\"urn:schemas-upnp-org:service:${NS}:1\" /> </s:Body> </s:Envelope>" \ |
||||
-s) |
||||
|
||||
if [ "$?" -ne "0" ]; then |
||||
echo "ERROR - Could not retrieve status from FRITZ!Box" |
||||
exit ${RC_CRIT} |
||||
fi |
||||
|
||||
if [ ${DEBUG} -eq 1 ]; then |
||||
echo "DEBUG - Status:" |
||||
echo "${STATUS}" |
||||
fi |
||||
|
||||
case ${CHECK} in |
||||
linkuptime) |
||||
UPTIME=$(find_xml_value "${STATUS}" NewUptime) |
||||
require_number "${UPTIME}" "Could not parse uptime" |
||||
HOURS=$((${UPTIME}/3600)) |
||||
MINUTES=$(((${UPTIME}-(${HOURS}*3600))/60)) |
||||
SECONDS=$((${UPTIME}-(${HOURS}*3600)-(${MINUTES}*60))) |
||||
RESULT="Link uptime ${UPTIME} seconds [${HOURS}h ${MINUTES}m ${SECONDS}s]" |
||||
echo "${RESULT}" |
||||
;; |
||||
upstream) |
||||
UPSTREAMBITS=$(find_xml_value "${STATUS}" NewLayer1UpstreamMaxBitRate) |
||||
require_number "${UPSTREAMBITS}" "Could not parse upstream" |
||||
UPSTREAM=$(echo "scale=3;$UPSTREAMBITS/$RATE" | bc) |
||||
RESULT="Upstream ${UPSTREAM} ${PRE}bits per second" |
||||
echo "${RESULT}" |
||||
;; |
||||
downstream) |
||||
DOWNSTREAMBITS=$(find_xml_value "${STATUS}" NewLayer1DownstreamMaxBitRate) |
||||
require_number "${DOWNSTREAMBITS}" "Could not parse downstream" |
||||
DOWNSTREAM=$(echo "scale=3;$DOWNSTREAMBITS/$RATE" | bc) |
||||
RESULT="Downstream ${DOWNSTREAM} ${PRE}bits per second" |
||||
echo "${RESULT}" |
||||
;; |
||||
bandwidthdown) |
||||
BANDWIDTHDOWNBYTES=$(find_xml_value "${STATUS}" NewByteReceiveRate) |
||||
BANDWIDTHDOWN=$(echo "scale=3;$BANDWIDTHDOWNBYTES/$RATE" | bc) |
||||
RESULT="Current download ${BANDWIDTHDOWN} ${PRE}bytes per second" |
||||
echo "${RESULT}" |
||||
;; |
||||
bandwidthup) |
||||
BANDWIDTHUPBYTES=$(find_xml_value "${STATUS}" NewByteSendRate) |
||||
BANDWIDTHUP=$(echo "scale=3;$BANDWIDTHUPBYTES/$RATE" | bc) |
||||
RESULT="Current upload ${BANDWIDTHUP} ${PRE}bytes per second" |
||||
echo "${RESULT}" |
||||
;; |
||||
totalbwdown) |
||||
TOTALBWDOWNBYTES=$(find_xml_value "${STATUS}" NewTotalBytesReceived) |
||||
TOTALBWDOWN=$(echo "scale=3;$TOTALBWDOWNBYTES/$RATE" | bc) |
||||
RESULT="total download ${TOTALBWDOWN} ${PRE}bytes" |
||||
echo "$RESULT" |
||||
;; |
||||
totalbwup) |
||||
TOTALBWUPBYTES=$(find_xml_value "${STATUS}" NewTotalBytesSent) |
||||
TOTALBWUP=$(echo "scale=3;$TOTALBWUPBYTES/$RATE" | bc) |
||||
RESULT="total uploads ${TOTALBWUP} ${PRE}bytes" |
||||
echo "$RESULT" |
||||
;; |
||||
connection) |
||||
STATE=$(find_xml_value "${STATUS}" NewConnectionStatus) |
||||
case ${STATE} in |
||||
Connected) |
||||
echo "OK - Connected" |
||||
exit ${RC_OK} |
||||
;; |
||||
Connecting | Disconnected) |
||||
echo "WARNING - Connection lost" |
||||
exit ${RC_WARN} |
||||
;; |
||||
*) |
||||
echo "ERROR - Unknown connection state ${STATE}" |
||||
exit ${RC_UNKNOWN} |
||||
;; |
||||
esac |
||||
;; |
||||
*) |
||||
echo "ERROR - Unknown service check ${CHECK}" |
||||
exit ${RC_UNKNOWN} |
||||
esac |
@ -0,0 +1,104 @@ |
||||
################################################################################## |
||||
# # |
||||
# Takes the output from fritz-api.sh , processes it and exports it to Promethues # |
||||
# via http on default port 9000 (can be configured via config.yml # |
||||
# # |
||||
# (C)2022 M.T. Konstapel https://meezenest.nl/mees # |
||||
# # |
||||
# This file is part of fritzbox_exporter. # |
||||
# # |
||||
# fritzbox_exporter is free software: you can redistribute it and/or modify # |
||||
# it under the terms of the GNU General Public License as published by # |
||||
# the Free Software Foundation, either version 3 of the License, or # |
||||
# (at your option) any later version. # |
||||
# # |
||||
# fritzbox_exporter is distributed in the hope that it will be useful, # |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of # |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # |
||||
# GNU General Public License for more details. # |
||||
# # |
||||
# You should have received a copy of the GNU General Public License # |
||||
# along with fritzbox_exporter. If not, see <https://www.gnu.org/licenses/>. # |
||||
# # |
||||
################################################################################## |
||||
|
||||
import time |
||||
import random |
||||
import subprocess |
||||
import json |
||||
from os import path |
||||
import yaml |
||||
from prometheus_client.core import GaugeMetricFamily, REGISTRY, CounterMetricFamily |
||||
from prometheus_client import start_http_server |
||||
totalRandomNumber = 0 |
||||
|
||||
# Collect Fritzbox data for the first time. This way, the json_object is defined globally. This is not the proper way to do it, but it works. |
||||
result = subprocess.run(['./fritz-api.sh', '-j'], stdout=subprocess.PIPE) |
||||
#convert string to object |
||||
json_object = json.loads(result.stdout) |
||||
|
||||
class Fritzbox_collector(object): |
||||
def __init__(self): |
||||
pass |
||||
def collect(self): |
||||
# Connection state |
||||
if json_object["Connection"] in ['Connected']: |
||||
Fritzbox_connection_state = 2 |
||||
else: |
||||
Fritzbox_connection_state = 1 |
||||
|
||||
gauge = GaugeMetricFamily("fritz_connect_state", "Fritzbox connection state (1=not connected, 2=connected)", labels=["Fritzbox"]) |
||||
gauge.add_metric(['1'], Fritzbox_connection_state) |
||||
yield gauge |
||||
|
||||
gauge = GaugeMetricFamily("fritz_down_speed", "Fritzbox downlink speed", labels=["Fritzbox"]) |
||||
gauge.add_metric(['1'], json_object["DownstreamSync"]) |
||||
yield gauge |
||||
|
||||
gauge = GaugeMetricFamily("fritz_up_speed", "Fritzbox uplink speed", labels=["Fritzbox"]) |
||||
gauge.add_metric(['1'], json_object["UpstreamSync"]) |
||||
yield gauge |
||||
|
||||
count = CounterMetricFamily("fritz_uptime", "Fritzbox uptime", labels=['Fritzbox']) |
||||
count.add_metric(['1'], json_object["Uptime"]) |
||||
yield count |
||||
|
||||
count = CounterMetricFamily("fritz_upload_bw", "Fritzbox upload BW", labels=['Fritzbox']) |
||||
count.add_metric(['1'], json_object["UploadBW"]) |
||||
yield count |
||||
|
||||
count = CounterMetricFamily("fritz_download_bw", "Fritzbox download BW", labels=['Fritzbox']) |
||||
count.add_metric(['1'], json_object["DownloadBW"]) |
||||
yield count |
||||
|
||||
count = CounterMetricFamily("fritz_total_uploads", "Fritzbox total uploads", labels=['Fritzbox']) |
||||
count.add_metric(['1'], json_object["TotalUploads"]) |
||||
yield count |
||||
|
||||
count = CounterMetricFamily("fritz_total_downloads", "Fritzbox total downloads", labels=['Fritzbox']) |
||||
count.add_metric(['1'], json_object["TotalDownloads"]) |
||||
yield count |
||||
|
||||
if __name__ == "__main__": |
||||
port = 9000 |
||||
frequency = 15 |
||||
if path.exists('config.yml'): |
||||
with open('config.yml', 'r') as config_file: |
||||
try: |
||||
config = yaml.safe_load(config_file) |
||||
port = int(config['port']) |
||||
frequency = config['scrape_frequency'] |
||||
except yaml.YAMLError as error: |
||||
print(error) |
||||
|
||||
start_http_server(port) |
||||
REGISTRY.register(Fritzbox_collector()) |
||||
|
||||
while True: |
||||
# Collect Fritzbox data |
||||
result = subprocess.run(['./fritz-api.sh', '-j'], stdout=subprocess.PIPE) |
||||
#convert string to json object |
||||
json_object = json.loads(result.stdout) |
||||
|
||||
# period between collection |
||||
time.sleep(frequency) |
Loading…
Reference in new issue