commit
0a6da210d9
6 changed files with 3176 additions and 91 deletions
32
Dockerfile
32
Dockerfile
|
@ -1,28 +1,24 @@
|
||||||
FROM python:3.8-slim-buster
|
FROM python:3.8-slim-buster
|
||||||
|
LABEL maintainer="Team QLUSTOR <team@qlustor.com>" \
|
||||||
LABEL maintainer="Josh Smith" \
|
|
||||||
description="Original by Aiden Gilmartin. Speedtest to InfluxDB data bridge"
|
description="Original by Aiden Gilmartin. Speedtest to InfluxDB data bridge"
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get -q -y install --no-install-recommends apt-utils gnupg1 apt-transport-https dirmngr
|
|
||||||
|
|
||||||
# Install speedtest-cli
|
|
||||||
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61
|
|
||||||
RUN echo "deb https://ookla.bintray.com/debian buster main" | tee /etc/apt/sources.list.d/speedtest.list
|
|
||||||
RUN apt-get update && apt-get -q -y install speedtest
|
|
||||||
|
|
||||||
|
RUN true &&\
|
||||||
|
\
|
||||||
|
# Install dependencies
|
||||||
|
apt-get update && \
|
||||||
|
apt-get -q -y install --no-install-recommends apt-utils gnupg1 apt-transport-https dirmngr && \
|
||||||
|
\
|
||||||
# Install Python packages
|
# Install Python packages
|
||||||
COPY requirements.txt /
|
pip3 install pythonping influxdb && \
|
||||||
RUN pip install -r /requirements.txt
|
\
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
RUN apt-get -q -y autoremove
|
apt-get -q -y autoremove && apt-get -q -y clean && \
|
||||||
RUN apt-get -q -y clean
|
rm -rf /var/lib/apt/lists/*
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Final setup & execution
|
# Final setup & execution
|
||||||
COPY . /app
|
ADD . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["python3", "-u", "main.py"]
|
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
||||||
|
CMD ["main.py"]
|
||||||
|
|
3002
GrafanaDash-SpeedTests.json
Normal file
3002
GrafanaDash-SpeedTests.json
Normal file
File diff suppressed because it is too large
Load diff
63
README.md
63
README.md
|
@ -4,9 +4,9 @@ This is a Python script that will continuously run the official Speedtest CLI ap
|
||||||
|
|
||||||
This script will allow you to measure your internet connections speed and consistency over time. It uses env variables as configuration. It's as easy to use as telling your Docker server a 1 line command and you'll be set. Using Grafana you can start exploring this data easily.
|
This script will allow you to measure your internet connections speed and consistency over time. It uses env variables as configuration. It's as easy to use as telling your Docker server a 1 line command and you'll be set. Using Grafana you can start exploring this data easily.
|
||||||
|
|
||||||
I built a grafana dashboard for this data that can be found at https://grafana.com/grafana/dashboards/13053
|
I built a Grafana dashboard which has been exported into this repo as `GrafanaDash-SpeedTests.json` to import into Grafana for your convenience.
|
||||||
|
|
||||||
![Grafana Dashboard](https://grafana.com/api/dashboards/13053/images/8976/image)
|
![GrafanaDashboard](https://user-images.githubusercontent.com/945191/105287048-46f52a80-5b6c-11eb-9e57-038d63b67efb.png)
|
||||||
|
|
||||||
There are some added features to allow some additional details that Ookla provides as tags on your data. Some examples are your current ISP, the interface being used, the server who hosted the test. Overtime, you could identify if some serers are performing better than others.
|
There are some added features to allow some additional details that Ookla provides as tags on your data. Some examples are your current ISP, the interface being used, the server who hosted the test. Overtime, you could identify if some serers are performing better than others.
|
||||||
|
|
||||||
|
@ -15,19 +15,23 @@ There are some added features to allow some additional details that Ookla provid
|
||||||
The InfluxDB connection settings are controlled by environment variables.
|
The InfluxDB connection settings are controlled by environment variables.
|
||||||
|
|
||||||
The variables available are:
|
The variables available are:
|
||||||
- INFLUX_DB_ADDRESS = 192.168.1.xxx
|
- NAMESPACE = default - None
|
||||||
- INFLUX_DB_PORT = 8086
|
- INFLUX_DB_ADDRESS = default - influxdb
|
||||||
- INFLUX_DB_USER = user
|
- INFLUX_DB_PORT = default - 8086
|
||||||
- INFLUX_DB_PASSWORD = pass
|
- INFLUX_DB_USER = default - {blank}
|
||||||
- INFLUX_DB_DATABASE = speedtest
|
- INFLUX_DB_PASSWORD = default - {blank}
|
||||||
- INFLUX_DB_TAGS = *comma seperated list of tags. See below for options*
|
- INFLUX_DB_DATABASE = default - speedtests
|
||||||
- SPEEDTEST_INTERVAL = 60
|
- INFLUX_DB_TAGS = default - None * See below for options, '*' widcard for all *
|
||||||
- SPEEDTEST_FAIL_INTERVAL = 5
|
- SPEEDTEST_INTERVAL = default - 5 (minutes)
|
||||||
|
- SPEEDTEST_SERVER_ID = default - {blank} * id from https://c.speedtest.net/speedtest-servers-static.php *
|
||||||
|
- PING_INTERVAL = default - 5 (seconds)
|
||||||
|
- PING_TARGETS = default - 1.1.1.1, 8.8.8.8 (csv of hosts to ping)
|
||||||
|
|
||||||
### Variable Notes
|
### Variable Notes
|
||||||
- Intervals are in minutes. *Script will convert it to seconds.*
|
- Intervals are in minutes. *Script will convert it to seconds.*
|
||||||
- If any variables are not needed, don't declare them. Functions will operate with or without most variables.
|
- If any variables are not needed, don't declare them. Functions will operate with or without most variables.
|
||||||
- Tags should be input without quotes. *INFLUX_DB_TAGS = isp, interface, external_ip, server_name, speedtest_url*
|
- Tags should be input without quotes. *INFLUX_DB_TAGS = isp, interface, external_ip, server_name, speedtest_url*
|
||||||
|
- NAMESPACE is used to collect data from multiple instances of the container into one database and select which you wish to view in Grafana. i.e. I have one monitoring my Starlink, the other my TELUS connection.
|
||||||
|
|
||||||
### Tag Options
|
### Tag Options
|
||||||
The Ookla speedtest app provides a nice set of data beyond the upload and download speed. The list is below.
|
The Ookla speedtest app provides a nice set of data beyond the upload and download speed. The list is below.
|
||||||
|
@ -59,40 +63,21 @@ Be aware that this script will automatically accept the license and GDPR stateme
|
||||||
|
|
||||||
1. Build the container.
|
1. Build the container.
|
||||||
|
|
||||||
`docker build -t breadlysm/speedtest-to-influxdb ./`
|
`docker build -t qlustor/speedtest_ookla-to-influxdb ./`
|
||||||
|
|
||||||
2. Run the container.
|
2. Run the container.
|
||||||
```
|
```
|
||||||
docker run -d --name speedtest-influx \
|
docker run -d -t --name speedflux \
|
||||||
-e 'INFLUX_DB_ADDRESS'='_influxdb_host_' \
|
-e 'NAMESPACE'='None' \
|
||||||
|
-e 'INFLUX_DB_ADDRESS'='influxdb' \
|
||||||
-e 'INFLUX_DB_PORT'='8086' \
|
-e 'INFLUX_DB_PORT'='8086' \
|
||||||
-e 'INFLUX_DB_USER'='_influx_user_' \
|
-e 'INFLUX_DB_USER'='_influx_user_' \
|
||||||
-e 'INFLUX_DB_PASSWORD'='_influx_pass_' \
|
-e 'INFLUX_DB_PASSWORD'='_influx_pass_' \
|
||||||
-e 'INFLUX_DB_DATABASE'='speedtest' \
|
-e 'INFLUX_DB_DATABASE'='speedtests' \
|
||||||
-e 'SPEEDTEST_INTERVAL'='1800' \
|
-e 'SPEEDTEST_INTERVAL'='5' \
|
||||||
-e 'SPEEDTEST_FAIL_INTERVAL'='60' \
|
-e 'SPEEDTEST_FAIL_INTERVAL'='5' \
|
||||||
breadlysm/speedtest-to-influxdb
|
-e 'SPEEDTEST_SERVER_ID'='12746' \
|
||||||
|
qlustor/speedtest_ookla-to-influxdb
|
||||||
```
|
```
|
||||||
### No Container
|
|
||||||
|
|
||||||
1. Clone the repo
|
This script looks to have been originally written by https://github.com/aidengilmartin/speedtest-to-influxdb/blob/master/main.py and I forked it from https://github.com/breadlysm/speedtest-to-influxdb. They did the hard work, I've continued to modify it though to fit my needs.
|
||||||
|
|
||||||
`git clone https://github.com/breadlysm/speedtest-to-influxdb.git`
|
|
||||||
|
|
||||||
2. Configure the .env file in the repo or set the environment variables on your device.
|
|
||||||
|
|
||||||
3. [Install the Speedtest CLI application by Ookla.](https://www.speedtest.net/apps/cli)
|
|
||||||
|
|
||||||
NOTE: The `speedtest-cli` package in distro repositories is an unofficial client. It will need to be uninstalled before installing the Ookla Speedtest CLI application with the directions on their website.
|
|
||||||
|
|
||||||
4. Install the InfluxDB client for library from Python.
|
|
||||||
|
|
||||||
`pip install influxdb`
|
|
||||||
|
|
||||||
5. Run the script.
|
|
||||||
|
|
||||||
`python3 ./main.py`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This script looks to have been originally written by https://github.com/aidengilmartin/speedtest-to-influxdb/blob/master/main.py and I forked it from https://github.com/martinfrancois/speedtest-to-influxdb. They did the hard work, I've continued to modify it though to fit my needs.
|
|
||||||
|
|
16
entrypoint.sh
Normal file
16
entrypoint.sh
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
printenv >> /etc/environment
|
||||||
|
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
# Install speedtest-cli
|
||||||
|
if [ ! -e /usr/bin/speedtest ]
|
||||||
|
then
|
||||||
|
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61
|
||||||
|
echo "deb https://ookla.bintray.com/debian buster main" | tee /etc/apt/sources.list.d/speedtest.list
|
||||||
|
apt-get update && apt-get -q -y install speedtest
|
||||||
|
apt-get -q -y autoremove && apt-get -q -y clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /usr/local/bin/python3 $@
|
151
main.py
151
main.py
|
@ -1,22 +1,31 @@
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
from pythonping import ping
|
||||||
from influxdb import InfluxDBClient
|
from influxdb import InfluxDBClient
|
||||||
|
from multiprocessing import Process
|
||||||
|
|
||||||
# InfluxDB Settings
|
# InfluxDB Settings
|
||||||
DB_ADDRESS = os.environ.get('INFLUX_DB_ADDRESS')
|
NAMESPACE = os.getenv('NAMESPACE', 'None')
|
||||||
DB_PORT = int(os.environ.get('INFLUX_DB_PORT'))
|
DB_ADDRESS = os.getenv('INFLUX_DB_ADDRESS', 'influxdb')
|
||||||
DB_USER = os.environ.get('INFLUX_DB_USER')
|
DB_PORT = int(os.getenv('INFLUX_DB_PORT', '8086'))
|
||||||
DB_PASSWORD = os.environ.get('INFLUX_DB_PASSWORD')
|
DB_USER = os.getenv('INFLUX_DB_USER', '')
|
||||||
DB_DATABASE = os.environ.get('INFLUX_DB_DATABASE')
|
DB_PASSWORD = os.getenv('INFLUX_DB_PASSWORD', '')
|
||||||
DB_TAGS = os.environ.get('INFLUX_DB_TAGS')
|
DB_DATABASE = os.getenv('INFLUX_DB_DATABASE', 'speedtests')
|
||||||
|
DB_TAGS = os.getenv('INFLUX_DB_TAGS', None)
|
||||||
|
PING_TARGETS = os.getenv('PING_TARGETS', '1.1.1.1, 8.8.8.8')
|
||||||
|
|
||||||
# Speedtest Settings
|
# Speedtest Settings
|
||||||
# Time between tests (in minutes, converts to seconds).
|
# Time between tests (in minutes, converts to seconds).
|
||||||
TEST_INTERVAL = int(os.environ.get('SPEEDTEST_INTERVAL')) * 60
|
TEST_INTERVAL = int(os.getenv('SPEEDTEST_INTERVAL', '5')) * 60
|
||||||
# Time before retrying a failed Speedtest (in minutes, converts to seconds).
|
# Time before retrying a failed Speedtest (in minutes, converts to seconds).
|
||||||
TEST_FAIL_INTERVAL = int(os.environ.get('SPEEDTEST_FAIL_INTERVAL')) * 60
|
TEST_FAIL_INTERVAL = int(os.getenv('SPEEDTEST_FAIL_INTERVAL', '5')) * 60
|
||||||
|
# Specific server ID
|
||||||
|
SERVER_ID = os.getenv('SPEEDTEST_SERVER_ID', '')
|
||||||
|
# Time between ping tests (in seconds).
|
||||||
|
PING_INTERVAL = int(os.getenv('PING_INTERVAL', '5'))
|
||||||
|
|
||||||
influxdb_client = InfluxDBClient(
|
influxdb_client = InfluxDBClient(
|
||||||
DB_ADDRESS, DB_PORT, DB_USER, DB_PASSWORD, None)
|
DB_ADDRESS, DB_PORT, DB_USER, DB_PASSWORD, None)
|
||||||
|
@ -42,10 +51,11 @@ def pkt_loss(data):
|
||||||
|
|
||||||
def tag_selection(data):
|
def tag_selection(data):
|
||||||
tags = DB_TAGS
|
tags = DB_TAGS
|
||||||
if tags is None:
|
options = {}
|
||||||
return None
|
|
||||||
# tag_switch takes in _data and attaches CLIoutput to more readable ids
|
# tag_switch takes in _data and attaches CLIoutput to more readable ids
|
||||||
tag_switch = {
|
tag_switch = {
|
||||||
|
'namespace': NAMESPACE,
|
||||||
'isp': data['isp'],
|
'isp': data['isp'],
|
||||||
'interface': data['interface']['name'],
|
'interface': data['interface']['name'],
|
||||||
'internal_ip': data['interface']['internalIp'],
|
'internal_ip': data['interface']['internalIp'],
|
||||||
|
@ -63,17 +73,24 @@ def tag_selection(data):
|
||||||
'speedtest_url': data['result']['url']
|
'speedtest_url': data['result']['url']
|
||||||
}
|
}
|
||||||
|
|
||||||
options = {}
|
if tags is None:
|
||||||
|
tags = 'namespace'
|
||||||
|
elif '*' in tags:
|
||||||
|
return tag_switch
|
||||||
|
else:
|
||||||
|
tags = 'namespace, ' + tags
|
||||||
|
|
||||||
tags = tags.split(',')
|
tags = tags.split(',')
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
# split the tag string, strip and add selected tags to {options} with corresponding tag_switch data
|
# split the tag string, strip and add selected tags to {options} with corresponding tag_switch data
|
||||||
tag = tag.strip()
|
tag = tag.strip()
|
||||||
options[tag] = tag_switch[tag]
|
options[tag] = tag_switch[tag]
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def format_for_influx(cliout):
|
def format_for_influx(data):
|
||||||
data = json.loads(cliout)
|
|
||||||
# There is additional data in the speedtest-cli output but it is likely not necessary to store.
|
# There is additional data in the speedtest-cli output but it is likely not necessary to store.
|
||||||
influx_data = [
|
influx_data = [
|
||||||
{
|
{
|
||||||
|
@ -110,37 +127,107 @@ def format_for_influx(cliout):
|
||||||
'fields': {
|
'fields': {
|
||||||
'packetLoss': pkt_loss(data)
|
'packetLoss': pkt_loss(data)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'measurement': 'speeds',
|
||||||
|
'time': data['timestamp'],
|
||||||
|
'fields': {
|
||||||
|
'jitter': data['ping']['jitter'],
|
||||||
|
'latency': data['ping']['latency'],
|
||||||
|
'packetLoss': pkt_loss(data),
|
||||||
|
# Byte to Megabit
|
||||||
|
'bandwidth_down': data['download']['bandwidth'] / 125000,
|
||||||
|
'bytes_down': data['download']['bytes'],
|
||||||
|
'elapsed_down': data['download']['elapsed'],
|
||||||
|
# Byte to Megabit
|
||||||
|
'bandwidth_up': data['upload']['bandwidth'] / 125000,
|
||||||
|
'bytes_up': data['upload']['bytes'],
|
||||||
|
'elapsed_up': data['upload']['elapsed']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
tags = tag_selection(data)
|
tags = tag_selection(data)
|
||||||
if tags is None:
|
if tags is not None:
|
||||||
return influx_data
|
|
||||||
else:
|
|
||||||
for measurement in influx_data:
|
for measurement in influx_data:
|
||||||
measurement['tags'] = tags
|
measurement['tags'] = tags
|
||||||
return influx_data
|
|
||||||
|
|
||||||
|
return influx_data
|
||||||
|
|
||||||
|
|
||||||
|
def speedtest():
|
||||||
|
if not SERVER_ID:
|
||||||
|
speedtest = subprocess.run(
|
||||||
|
["speedtest", "--accept-license", "--accept-gdpr", "-f", "json"], capture_output=True)
|
||||||
|
print("Automatic server choice")
|
||||||
|
else:
|
||||||
|
speedtest = subprocess.run(
|
||||||
|
["speedtest", "--accept-license", "--accept-gdpr", "-f", "json", "--server-id=" + SERVER_ID], capture_output=True)
|
||||||
|
print("Manual server choice : ID = " + SERVER_ID)
|
||||||
|
|
||||||
|
if speedtest.returncode == 0: # Speedtest was successful.
|
||||||
|
print("Speedtest Successful :")
|
||||||
|
data_json = json.loads(speedtest.stdout)
|
||||||
|
print("time: " + str(data_json['timestamp']) + " - ping: " + str(data_json['ping']['latency']) + " ms - download: " + str(data_json['download']['bandwidth']/125000) + " Mb/s - upload: " + str(data_json['upload']['bandwidth'] / 125000) + " Mb/s - isp: " + data_json['isp'] + " - ext. IP: " + data_json['interface']['externalIp'] + " - server id: " + str(data_json['server']['id']) + " (" + data_json['server']['name'] + " @ " + data_json['server']['location'] + ")")
|
||||||
|
data = format_for_influx(data_json)
|
||||||
|
if influxdb_client.write_points(data) == True:
|
||||||
|
print("Data written to DB successfully")
|
||||||
|
else: # Speedtest failed.
|
||||||
|
print("Speedtest Failed :")
|
||||||
|
print(speedtest.stderr)
|
||||||
|
print(speedtest.stdout)
|
||||||
|
# time.sleep(TEST_FAIL_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
|
def pingtest():
|
||||||
|
timestamp = datetime.datetime.utcnow()
|
||||||
|
for target in PING_TARGETS.split(','):
|
||||||
|
target = target.strip()
|
||||||
|
pingtest = ping(target, verbose=False, timeout=1, count=1, size=128)
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'measurement': 'pings',
|
||||||
|
'time': timestamp,
|
||||||
|
'tags': {
|
||||||
|
'namespace': NAMESPACE,
|
||||||
|
'target' : target
|
||||||
|
},
|
||||||
|
'fields': {
|
||||||
|
'success' : int(pingtest._responses[0].error_message is None),
|
||||||
|
'rtt': float(0 if pingtest._responses[0].error_message is not None else pingtest.rtt_avg_ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if influxdb_client.write_points(data) == True:
|
||||||
|
print("Ping data written to DB successfully")
|
||||||
|
else: # Speedtest failed.
|
||||||
|
print("Ping Failed.")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
pPing = Process(target=pingtest)
|
||||||
|
pSpeed = Process(target=speedtest)
|
||||||
|
|
||||||
init_db() # Setup the database if it does not already exist.
|
init_db() # Setup the database if it does not already exist.
|
||||||
|
|
||||||
|
loopcount = 0
|
||||||
while (1): # Run a Speedtest and send the results to influxDB indefinitely.
|
while (1): # Run a Speedtest and send the results to influxDB indefinitely.
|
||||||
speedtest = subprocess.run(
|
if loopcount == 0 or loopcount % PING_INTERVAL == 0:
|
||||||
["speedtest", "--accept-license", "--accept-gdpr", "-f", "json"], capture_output=True)
|
if pPing.is_alive():
|
||||||
|
pPing.terminate()
|
||||||
|
pPing = Process(target=pingtest)
|
||||||
|
pPing.start()
|
||||||
|
|
||||||
if speedtest.returncode == 0: # Speedtest was successful.
|
if loopcount == 0 or loopcount % TEST_INTERVAL == 0:
|
||||||
data = format_for_influx(speedtest.stdout)
|
if pSpeed.is_alive():
|
||||||
print("Speedtest Successful:")
|
pSpeed.terminate()
|
||||||
if influxdb_client.write_points(data) == True:
|
pSpeed = Process(target=speedtest)
|
||||||
print("Data written to DB successfully")
|
pSpeed.start()
|
||||||
time.sleep(TEST_INTERVAL)
|
|
||||||
else: # Speedtest failed.
|
|
||||||
print("Speedtest Failed:")
|
|
||||||
print(speedtest.stderr)
|
|
||||||
print(speedtest.stdout)
|
|
||||||
time.sleep(TEST_FAIL_INTERVAL)
|
|
||||||
|
|
||||||
|
if loopcount % ( PING_INTERVAL * TEST_INTERVAL ) == 0:
|
||||||
|
loopcount = 0
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
loopcount += 1
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print('Speedtest CLI Data Logger to InfluxDB')
|
print('Speedtest CLI data logger to InfluxDB started...')
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
influxdb
|
|
Loading…
Reference in a new issue