diff --git a/.env b/.env index 2099de8..db0ebf9 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ -# Speedtest-grafana container environment variables +# Speeder container environment variables # The interval in seconds to run speedtests on # Defaults to 5 minutes -SPEEDTEST_INTERVAL=300 -SPEEDTEST_SERVER_ID= +SPEEDER_SPEEDTEST_INTERVAL=300 +SPEEDER_SPEEDTEST_SERVER_ID= # InfluxDB container environment variables INFLUXDB_IMAGE_TAG=2.7.1 # You SHOULD change these! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bbb969b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3e7476a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,12 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable + with: + options: "--check --verbose" diff --git a/.gitignore b/.gitignore index 0c9c949..46cf398 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index a1f1e68..382ad82 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,76 @@ -# Speedtest-grafana +# Speeder -Use [Grafana](https://grafana.com/), [InfluxDB](https://www.influxdata.com/products/influxdb/) and the [librespeed/speedtest-cli](https://github.com/librespeed/speedtest-cli) to monitor your internet speed! 🚀 +![Build](https://github.com/dbrennand/speeder/actions/workflows/build.yml/badge.svg) +![Lint](https://github.com/dbrennand/speeder/actions/workflows/lint.yml/badge.svg) -![Dashboard](images/dashboard.png) +Python script to monitor your internet speed! 🚀 -## Prerequisites +Periodically run [librespeed/speedtest-cli](https://github.com/librespeed/speedtest-cli) and send results to [InfluxDB](https://www.influxdata.com/products/influxdb/). -1. Docker +# Quick Start -2. Docker Compose +> **Note** +> The assumption is made that you've already setup `InfluxDB 2.x.x`. Alternatively, you can use the [Docker Compose](#docker-compose-stack---influxdb-and-grafana) method where this is setup for you. -## Usage +List available server IDs: -1. Build the speedtest-grafana container image: +```bash +docker run --rm -it ghcr.io/dbrennand/speeder:latest /librespeed --list +``` - ```bash - docker compose build - ``` +Next, start and configure speeder using the environment variables: + +```bash +docker run -itd --rm --name speeder \ + -e "SPEEDER_SPEEDTEST_INTERVAL=300" \ + -e "SPEEDER_SPEEDTEST_SERVER_ID=49" \ + -e "SPEEDER_INFLUXDB_HOST=influx.example.com" \ + -e "SPEEDER_INFLUXDB_TOKEN=" \ + -e "SPEEDER_INFLUXDB_ORG=speeder" \ + -e "SPEEDER_INFLUXDB_BUCKET=speeder" \ + ghcr.io/dbrennand/speeder:latest +``` + +# Environment Variables + +The [speeder](speeder.py) script is configured using the below environment variables: + +| Name | Description | Default Value | +| ----------------------------- | ------------------------------------------------------------------------------------------ | ------------- | +| `SPEEDER_SPEEDTEST_INTERVAL` | Interval in seconds to run speedtests on. | 300 | +| `SPEEDER_SPEEDTEST_SERVER_ID` | Server ID to run speedtests against. Supports multiple IDs using a comma separated string. | "" | +| `SPEEDER_INFLUXDB_HOST` | InfluxDB hostname. | influxdb | +| `SPEEDER_INFLUXDB_PORT` | InfluxDB port. | 8086 | +| `SPEEDER_INFLUXDB_TOKEN` | InfluxDB token. | root | +| `SPEEDER_INFLUXDB_ORG` | InfluxDB organisation name. | speeder | +| `SPEEDER_INFLUXDB_BUCKET` | InfluxDB bucket name to write speedtest results to. | speeder | + +# Docker Compose Stack - InfluxDB and Grafana -2. Set the `SPEEDTEST_SERVER_ID` environment variable located in the [.env](.env) file to the server ID to perform speedtests against. +The [docker-compose.yml](docker-compose.yml) file in this repository will deploy speeder, InfluxDB `2.7.1` and Grafana containers. Grafana will be provisioned with InfluxDB as the [data source](grafana-config/datasources/datasource.yml) and the pre-created [dashboard](grafana-config/dashboards/dashboard.json): + +![Dashboard](images/dashboard.png) + +![Dashboard 1](images/dashboard1.png) + +1. Set the `SPEEDER_SPEEDTEST_SERVER_ID` environment variable located in the [.env](.env) file to the server IDs to perform speedtests against. > **Note** > > If you don't know any server IDs, run the following command to list them: > ```bash - > docker run --rm -it speedtest-grafana:1.0.0 /librespeed --list + > docker run --rm -it ghcr.io/dbrennand/speeder:latest /librespeed --list > ``` -3. Set the `DOCKER_INFLUXDB_INIT_PASSWORD`, `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN` and `GF_SECURITY_ADMIN_PASSWORD` environment variables located in the [.env](.env) file. +2. Set the `DOCKER_INFLUXDB_INIT_PASSWORD`, `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN` and `GF_SECURITY_ADMIN_PASSWORD` environment variables located in the [.env](.env) file. -4. Start the containers: +3. Start the compose stack: ```bash docker compose up -d ``` -5. Access Grafana at [`http://localhost:3000`](http://localhost:3000) - - > **Note** - > - > Grafana will also be available from your host's IP address. +Grafana will be accessible at [`http://localhost:3000`](http://localhost:3000) and your host's IP address. ## Disclaimer @@ -52,4 +83,5 @@ If you like this project then please give their repositories a star! ⭐ [**Daniel Brennand**](https://github.com/dbrennand) - *Author* ## License + This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details. diff --git a/docker-compose.yml b/docker-compose.yml index 26e7e2c..d2b7565 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,30 +9,29 @@ services: - influxdb:/var/lib/influxdb2 restart: always networks: - - speedtest-grafana + - speeder environment: - "DOCKER_INFLUXDB_INIT_MODE=setup" - "DOCKER_INFLUXDB_INIT_USERNAME=${DOCKER_INFLUXDB_INIT_USERNAME}" - "DOCKER_INFLUXDB_INIT_PASSWORD=${DOCKER_INFLUXDB_INIT_PASSWORD}" - "DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${DOCKER_INFLUXDB_INIT_ADMIN_TOKEN}" - - "DOCKER_INFLUXDB_INIT_ORG=internet_speed" - - "DOCKER_INFLUXDB_INIT_BUCKET=internet_speed" - speedtest-grafana: - build: . - image: speedtest-grafana:1.0.0 - container_name: speedtest-grafana + - "DOCKER_INFLUXDB_INIT_ORG=speeder" + - "DOCKER_INFLUXDB_INIT_BUCKET=speeder" + speeder: + image: ghcr.io/dbrennand/speeder:latest + container_name: speeder depends_on: - influxdb networks: - - speedtest-grafana + - speeder environment: - - "SPEEDTEST_INTERVAL=${SPEEDTEST_INTERVAL}" - - "SPEEDTEST_SERVER_ID=${SPEEDTEST_SERVER_ID}" - - "INFLUXDB_HOST=influxdb" - - "INFLUXDB_PORT=8086" - - "INFLUXDB_ORG=internet_speed" - - "INFLUXDB_BUCKET=internet_speed" - - "INFLUXDB_TOKEN=${DOCKER_INFLUXDB_INIT_ADMIN_TOKEN}" + - "SPEEDER_SPEEDTEST_INTERVAL=${SPEEDER_SPEEDTEST_INTERVAL}" + - "SPEEDER_SPEEDTEST_SERVER_ID=${SPEEDER_SPEEDTEST_SERVER_ID}" + - "SPEEDER_INFLUXDB_HOST=influxdb" + - "SPEEDER_INFLUXDB_PORT=8086" + - "SPEEDER_INFLUXDB_ORG=speeder" + - "SPEEDER_INFLUXDB_BUCKET=speeder" + - "SPEEDER_INFLUXDB_TOKEN=${DOCKER_INFLUXDB_INIT_ADMIN_TOKEN}" grafana: image: grafana/grafana:10.0.3 container_name: grafana @@ -43,14 +42,14 @@ services: - ./grafana-config/:/etc/grafana/provisioning depends_on: - influxdb - - speedtest-grafana + - speeder networks: - - speedtest-grafana + - speeder environment: - "GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}" - "GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}" - - "INFLUXDB_ORG=internet_speed" - - "INFLUXDB_BUCKET=internet_speed" + - "INFLUXDB_ORG=speeder" + - "INFLUXDB_BUCKET=speeder" - "INFLUXDB_TOKEN=${DOCKER_INFLUXDB_INIT_ADMIN_TOKEN}" volumes: @@ -58,4 +57,4 @@ volumes: grafana-storage: networks: - speedtest-grafana: + speeder: diff --git a/dockerfile b/dockerfile index 4e8c6db..c2f305c 100644 --- a/dockerfile +++ b/dockerfile @@ -14,9 +14,9 @@ FROM python:3.9-alpine COPY requirements.txt / RUN pip install --no-cache-dir -r /requirements.txt # Copy speedtest script -COPY speedtest.py / +COPY speeder.py / WORKDIR / # Copy the librespeed CLI binary to the Python alpine image to reduce size COPY --from=build /speedtest-cli/librespeed . # Run speedtest script -CMD ["python", "speedtest.py"] +CMD ["python", "speeder.py"] diff --git a/grafana-config/dashboards/dashboard.json b/grafana-config/dashboards/dashboard.json index a7c90f7..dc053e8 100644 --- a/grafana-config/dashboards/dashboard.json +++ b/grafana-config/dashboards/dashboard.json @@ -146,10 +146,10 @@ "type": "fill" } ], - "measurement": "internet_speed", + "measurement": "speeder", "orderByTime": "ASC", "policy": "default", - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"upload\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"upload\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "A", "resultFormat": "time_series", "select": [ @@ -198,7 +198,7 @@ "uid": "P5697886F9CA74929" }, "hide": false, - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"download\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"download\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "B" } ], @@ -330,10 +330,10 @@ "type": "fill" } ], - "measurement": "internet_speed", + "measurement": "speeder", "orderByTime": "ASC", "policy": "default", - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"bytes_sent\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"bytes_sent\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "A", "resultFormat": "time_series", "select": [ @@ -382,7 +382,7 @@ "uid": "P5697886F9CA74929" }, "hide": false, - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"bytes_received\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"bytes_received\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "B" } ], @@ -513,10 +513,10 @@ "type": "fill" } ], - "measurement": "internet_speed", + "measurement": "speeder", "orderByTime": "ASC", "policy": "default", - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"ping\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"ping\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "A", "resultFormat": "time_series", "select": [ @@ -565,7 +565,7 @@ "uid": "P5697886F9CA74929" }, "hide": false, - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"internet_speed\" and\n r._field == \"jitter\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => \n r._measurement == \"speeder\" and\n r._field == \"jitter\" and\n r.server_name == \"${Server}\"\n )\n |> drop(columns: [\"server_name\", \"server_url\"])", "refId": "B" } ], @@ -769,7 +769,7 @@ ], "orderByTime": "ASC", "policy": "default", - "query": "from(bucket: \"internet_speed\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => r._measurement == \"internet_speed\")", + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => r._measurement == \"speeder\" and\n r.server_name == \"${Server}\")", "rawQuery": true, "refId": "A", "resultFormat": "table", @@ -794,14 +794,17 @@ "transformations": [ { "id": "prepareTimeSeries", - "options": {} + "options": { + "format": "wide" + } }, { "id": "labelsToFields", "options": { "keepLabels": [ "server_name" - ] + ], + "mode": "columns" } }, { @@ -840,10 +843,32 @@ "style": "dark", "tags": [], "templating": { - "list": [] + "list": [ + { + "current": {}, + "datasource": { + "type": "influxdb", + "uid": "P5697886F9CA74929" + }, + "definition": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => r._measurement == \"speeder\")\n |> keep(columns: [\"server_name\"])\n |> distinct(column: \"server_name\")\n |> keep(columns: [\"_value\"])", + "description": "Server Name.", + "hide": 0, + "includeAll": false, + "label": "Server", + "multi": false, + "name": "Server", + "options": [], + "query": "from(bucket: \"speeder\")\n |> range(start: ${__from:date:iso})\n |> filter(fn: (r) => r._measurement == \"speeder\")\n |> keep(columns: [\"server_name\"])\n |> distinct(column: \"server_name\")\n |> keep(columns: [\"_value\"])", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] }, "time": { - "from": "now-1h", + "from": "now-6h", "to": "now" }, "timepicker": { @@ -864,6 +889,6 @@ "timezone": "", "title": "Speedtest Results", "uid": "ll6ARVfGk", - "version": 3, + "version": 2, "weekStart": "" } diff --git a/images/dashboard.png b/images/dashboard.png index 676872a..6bccfd6 100644 Binary files a/images/dashboard.png and b/images/dashboard.png differ diff --git a/images/dashboard1.png b/images/dashboard1.png new file mode 100644 index 0000000..12a4763 Binary files /dev/null and b/images/dashboard1.png differ diff --git a/speeder.py b/speeder.py new file mode 100644 index 0000000..7f33f6c --- /dev/null +++ b/speeder.py @@ -0,0 +1,131 @@ +""" +MIT License + +Copyright (c) 2023 dbrennand + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from influxdb_client.client.write_api import SYNCHRONOUS +from influxdb_client import InfluxDBClient +from loguru import logger +import subprocess +import os +import json +import time + +__version__ = "1.1.0" + +logger.debug(f"Starting speeder version: {__version__}.") + +# Retrieve environment variables +SPEEDER_SPEEDTEST_INTERVAL = int( + os.environ.get("SPEEDER_SPEEDTEST_INTERVAL", 300) +) # 5 minutes +SPEEDER_SPEEDTEST_SERVER_ID = os.environ.get("SPEEDER_SPEEDTEST_SERVER_ID", "") +SPEEDER_INFLUXDB_HOST = os.environ.get("SPEEDER_INFLUXDB_HOST", "influxdb") +SPEEDER_INFLUXDB_PORT = int(os.environ.get("SPEEDER_INFLUXDB_PORT", 8086)) +SPEEDER_INFLUXDB_TOKEN = os.environ.get("SPEEDER_INFLUXDB_TOKEN", "root") +SPEEDER_INFLUXDB_ORG = os.environ.get("SPEEDER_INFLUXDB_ORG", "speeder") +SPEEDER_INFLUXDB_BUCKET = os.environ.get("SPEEDER_INFLUXDB_BUCKET", "speeder") + +# Check environment variable has not been provided +if not SPEEDER_SPEEDTEST_SERVER_ID: + logger.debug( + "SPEEDER_SPEEDTEST_SERVER_ID environment variable has no server IDs. Choose from the list below and set the environment variable." + ) + servers = subprocess.run(["/librespeed", "--list"], capture_output=True, text=True) + logger.debug(servers.stdout) + exit(1) +else: + # Create list from comma separated string of server IDs + SPEEDER_SPEEDTEST_SERVER_ID = SPEEDER_SPEEDTEST_SERVER_ID.split(",") + +# Connect to InfluxDB +logger.debug( + f"Connecting to InfluxDB: {SPEEDER_INFLUXDB_HOST}:{SPEEDER_INFLUXDB_PORT} with organisation: {SPEEDER_INFLUXDB_ORG}." +) +with InfluxDBClient( + url=f"http://{SPEEDER_INFLUXDB_HOST}:{SPEEDER_INFLUXDB_PORT}", + token=SPEEDER_INFLUXDB_TOKEN, + org=SPEEDER_INFLUXDB_ORG, +) as client: + # Run the speedtest using the librespeed/speedtest-cli on an interval + while True: + for server_id in SPEEDER_SPEEDTEST_SERVER_ID: + logger.debug(f"Running speedtest for server ID: {server_id}.") + result = subprocess.run( + [ + "/librespeed", + "--server", + server_id, + "--telemetry-level", + "disabled", + "--json", + ], + capture_output=True, + text=True, + ) + # Check if the speedtest failed + if result.returncode != 0: + # CLI errors go to stdout + logger.debug( + f"Speedtest for server ID: {server_id} failed with exit code: {result.returncode}.\nError: {result.stdout}" + ) + else: + logger.debug(f"Speedtest for server ID: {server_id} succeeded.") + try: + json_result = json.loads(result.stdout) + except json.decoder.JSONDecodeError as err: + logger.debug( + f"Failed to parse JSON results for server ID: {server_id}.\nError: {err}" + ) + continue + with client.write_api(write_options=SYNCHRONOUS) as write_api: + record = [ + { + "measurement": "speeder", + "tags": { + "server_name": json_result[0]["server"]["name"], + "server_url": json_result[0]["server"]["url"], + "ip": json_result[0]["client"]["ip"], + "hostname": json_result[0]["client"]["hostname"], + "region": json_result[0]["client"]["region"], + "city": json_result[0]["client"]["city"], + "country": json_result[0]["client"]["country"], + "org": json_result[0]["client"]["org"], + "timezone": json_result[0]["client"]["timezone"], + }, + "time": json_result[0]["timestamp"], + "fields": { + "bytes_sent": json_result[0]["bytes_sent"], + "bytes_received": json_result[0]["bytes_received"], + "ping": float(json_result[0]["ping"]), + "jitter": float(json_result[0]["jitter"]), + "upload": float(json_result[0]["upload"]), + "download": float(json_result[0]["download"]), + }, + } + ] + logger.debug( + f"Writing record to InfluxDB bucket: {SPEEDER_INFLUXDB_BUCKET} for speedtest at {json_result[0]['timestamp']} using server ID: {server_id}." + ) + logger.debug(f"Record:\n{record}") + write_api.write(bucket=SPEEDER_INFLUXDB_BUCKET, record=record) + logger.debug(f"Sleeping for {SPEEDER_SPEEDTEST_INTERVAL} seconds.") + time.sleep(SPEEDER_SPEEDTEST_INTERVAL) diff --git a/speedtest.py b/speedtest.py deleted file mode 100644 index a0aa4dd..0000000 --- a/speedtest.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -MIT License - -Copyright (c) 2023 dbrennand - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" -from loguru import logger -import os -from influxdb_client import InfluxDBClient -from influxdb_client.client.write_api import SYNCHRONOUS -import subprocess -import json -import time - -__version__ = "1.0.0" - -logger.debug(f"Starting speedtest-grafana version: {__version__}.") - -# Retrieve environment variables -# If not found, use defaults -SPEEDTEST_INTERVAL = int(os.environ.get("SPEEDTEST_INTERVAL", 300)) # 5 minutes -SPEEDTEST_SERVER_ID = os.environ.get("SPEEDTEST_SERVER_ID", None) -INFLUXDB_HOST = os.environ.get("INFLUXDB_HOST", "influxdb") -INFLUXDB_PORT = int(os.environ.get("INFLUXDB_PORT", 8086)) -INFLUXDB_TOKEN = os.environ.get("INFLUXDB_TOKEN", "root") -INFLUXDB_ORG = os.environ.get("INFLUXDB_ORG", "internet_speed") -INFLUXDB_BUCKET = os.environ.get("INFLUXDB_BUCKET", "internet_speed") - -# Check if SPEEDTEST_SERVER_ID environment variable has not been provided -if not SPEEDTEST_SERVER_ID: - logger.debug( - "SPEEDTEST_SERVER_ID environment variable has no server ID. Choose from the list below and set the environment variable." - ) - servers = subprocess.run(["/librespeed", "--list"], capture_output=True, text=True) - logger.debug(servers.stdout) - exit() - -# Connect to InfluxDB -logger.debug( - f"Connecting to InfluxDB {INFLUXDB_HOST}:{INFLUXDB_PORT}, bucket: {INFLUXDB_BUCKET}." -) -with InfluxDBClient( - url=f"http://{INFLUXDB_HOST}:{INFLUXDB_PORT}", - token=INFLUXDB_TOKEN, - org=INFLUXDB_ORG, -) as client: - # Run the speedtest using the librespeed/speedtest-cli on an interval - while True: - logger.debug( - f"Running speedtest with server ID: {SPEEDTEST_SERVER_ID} and telemetry disabled." - ) - result = subprocess.run( - [ - "/librespeed", - "--server", - SPEEDTEST_SERVER_ID, - "--telemetry-level", - "disabled", - "--json", - ], - capture_output=True, - text=True, - ) - # Check if speedtest failed - if result.returncode != 0: - # Speedtest failed - # CLI errors go to stdout - logger.debug( - f"Speedtest failed with exit code: {result.returncode}.\nError: {result.stdout}" - ) - else: - # Speedtest succeeded - logger.debug(f"Speedtest succeeded. Parsing JSON results.") - # Parse JSON results - try: - json_result = json.loads(result.stdout) - except json.decoder.JSONDecodeError as err: - logger.debug(f"Failed to parse JSON results.\nError: {err}") - logger.debug(f"Sleeping for {SPEEDTEST_INTERVAL} seconds.") - # Sleep on the specified interval - time.sleep(SPEEDTEST_INTERVAL) - continue - with client.write_api(write_options=SYNCHRONOUS) as write_api: - # Create InfluxDB record - record = [ - { - "measurement": "internet_speed", - "tags": { - "server_name": json_result[0]["server"]["name"], - "server_url": json_result[0]["server"]["url"], - "ip": json_result[0]["client"]["ip"], - "hostname": json_result[0]["client"]["hostname"], - "region": json_result[0]["client"]["region"], - "city": json_result[0]["client"]["city"], - "country": json_result[0]["client"]["country"], - "org": json_result[0]["client"]["org"], - "timezone": json_result[0]["client"]["timezone"], - }, - "time": json_result[0]["timestamp"], - "fields": { - "bytes_sent": json_result[0]["bytes_sent"], - "bytes_received": json_result[0]["bytes_received"], - "ping": float(json_result[0]["ping"]), - "jitter": float(json_result[0]["jitter"]), - "upload": float(json_result[0]["upload"]), - "download": float(json_result[0]["download"]), - }, - } - ] - # Write results to InfluxDB - logger.debug( - f"Writing results to InfluxDB bucket: {INFLUXDB_BUCKET}.\nResults: {record}" - ) - write_api.write(bucket=INFLUXDB_BUCKET, record=record) - logger.debug(f"Sleeping for {SPEEDTEST_INTERVAL} seconds.") - # Sleep on the specified interval - time.sleep(SPEEDTEST_INTERVAL)