Skip to content

Commit

Permalink
Update to latest speedtest binary
Browse files Browse the repository at this point in the history
- Fix docker build to latest instructions as per https://www.speedtest.net/apps/cli
- Go >= 3.10
- Update README on how to dev so it's easier next time
- Use a unittest once I got new JSON from using `--debug` causing the JSON to be printed to stdout

Test:
- Build new docker container and see metrics
- ``
  • Loading branch information
cooperlees committed Sep 22, 2022
1 parent 815d28b commit 95e55be
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 14 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.10", "3.11.0-rc - 3.11"]
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.2.2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
FROM python:slim

RUN echo 'UTC' > /etc/localtime && apt update && apt install -y curl
RUN curl -s https://install.speedtest.net/app/cli/install.deb.sh | bash
RUN curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash
RUN apt-get install speedtest
RUN mkdir /speedtest_wrapper
COPY README.md setup.py speedtest_wrapper.py /speedtest_wrapper/
RUN pip3 --no-cache-dir install /speedtest_wrapper
RUN apt remove -y curl
RUN apt autoremove -y

CMD ["speedtest-wrapper"]
CMD ["speedtest-wrapper", "--debug"]
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# speedtest

Docker Container with latest Official Speedtest CLI wrapped into a prometheus exporter.

Seems to outperform all the native python alternatives out there.
Expand All @@ -7,6 +8,29 @@ Seems to outperform all the native python alternatives out there.

Python code using `prometheus_client` to run a small webserver to run a speedtest using the offical CLIs

We only support latest version of python as we're predominately a `Docker` deployed wrapper.

### Develop

- venv to run tests

```console
python3.10 -m venv --upgrade-deps /tmp/ts
/tmp/ts/bin/pip install black coverage
/tmp/ts/bin/coverage run tests.py
/tmp/ts/bin/coverage report -m
```

### Docker Build + run

- `docker build -t cooperlees/speedtest-wrapper .`
- `docker run --network host --rm --name speedtest_dev cooperlees/speedtest-wrapper`

To see stats:

- `curl -v http://localhost:6970/metrics`
- Does not work on MacOS tho ... didn't debug / workout options

### Keys

All are prefixed with `speedtest_`
Expand Down
7 changes: 3 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def get_long_desc() -> str:

setup(
name="speedtest_wrapper",
version="22.2.1",
version="22.9.21",
description="Wrap the speedtest cli and exports stats for prometheus",
long_description=get_long_desc(),
long_description_content_type="text/markdown",
Expand All @@ -32,11 +32,10 @@ def get_long_desc() -> str:
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
],
python_requires=">=3.8",
python_requires=">=3.10",
entry_points={"console_scripts": ["speedtest-wrapper = speedtest_wrapper:main"]},
install_requires=["prometheus_client"],
)
22 changes: 18 additions & 4 deletions speedtest_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class SpeedtestCollector:
key_prefix = "speedtest"
labels = ["hostname", "speedtest_host"]

def __init__(self, debug: bool) -> None:
self.debug = debug

def _handle_counter(self, category: str, value: float) -> GaugeMetricFamily:
normalized_category = category.replace(" ", "_")
key = f"{self.key_prefix}_{normalized_category}"
Expand All @@ -46,16 +49,27 @@ def collect(self) -> Generator[GaugeMetricFamily, None, None]:
)
return
elif not speedtest_data:
LOG.error("Got no speedtest data")
return

if self.debug:
print(speedtest_data, flush=True)

self.speedtest_host = speedtest_data["server"]["host"]
for category, value in speedtest_data.items():
if isinstance(value, (float, int)):
yield self._handle_counter(category, float(value))
elif category not in self.IGNORE_CATEGORIES and isinstance(value, dict):
for subcategory, subvalue in value.items():
combined_category = f"{category}_{subcategory}"
yield self._handle_counter(combined_category, float(subvalue))
if combined_category in {"download_latency", "upload_latency"}:
for subsubcategory, subsubvalue in subvalue.items():
yield self._handle_counter(
f"{combined_category}_{subsubcategory}",
float(subsubvalue),
)
else:
yield self._handle_counter(combined_category, float(subvalue))

run_time = time.time() - start_time
LOG.info(f"Collection finished in {run_time}s")
Expand Down Expand Up @@ -94,7 +108,7 @@ def main() -> int:

LOG.info(f"Starting {sys.argv[0]}")
start_http_server(args.port)
REGISTRY.register(SpeedtestCollector())
REGISTRY.register(SpeedtestCollector(args.debug))
LOG.info(f"Speedtest Prometheus Exporter - listening on {args.port}")
try:
while True:
Expand All @@ -104,5 +118,5 @@ def main() -> int:
return 0


if __name__ == "__main__":
sys.exit(main()) # pragma: no cover
if __name__ == "__main__": # pragma: no cover
sys.exit(main())
61 changes: 60 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,73 @@
#!/usr/bin/env python3

import unittest
from unittest.mock import Mock, patch

import speedtest_wrapper


# 20220921 return
MOCK_SPEEDTEST_JSON = {
"type": "result",
"timestamp": "2022-09-22T04:29:10Z",
"ping": {"jitter": 4.077, "latency": 13.814, "low": 9.838, "high": 17.448},
"download": {
"bandwidth": 73593870,
"bytes": 561306538,
"elapsed": 7814,
"latency": {"iqm": 49.645, "low": 12.847, "high": 81.176, "jitter": 11.933},
},
"upload": {
"bandwidth": 4550470,
"bytes": 27235440,
"elapsed": 5903,
"latency": {"iqm": 13.94, "low": 10.793, "high": 140.264, "jitter": 4.597},
},
"packetLoss": 0,
"isp": "Spectrum",
"interface": {
"internalIp": "172.17.0.2",
"name": "eth0",
"macAddr": "02:42:AC:11:00:02",
"isVpn": False,
"externalIp": "47.33.15.75",
},
"server": {
"id": 2408,
"host": "spt01renonv.reno.nv.charter.com",
"port": 8080,
"name": "Spectrum",
"location": "Reno, NV",
"country": "United States",
"ip": "24.205.192.190",
},
"result": {
"id": "00420ff7-e453-44d9-8caa-b018cf72105e",
"url": "https://www.speedtest.net/result/c/00420ff7-e453-44d9-8caa-b018cf72105e",
"persisted": True,
},
}


# Shitty tests just to ensure file has valid syntax and imports
class TestSpeedTest(unittest.TestCase):
def setUp(self) -> None:
self.stc = speedtest_wrapper.SpeedtestCollector()
self.stc = speedtest_wrapper.SpeedtestCollector(debug=False)

def test_stc_valid(self) -> None:
self.assertTrue(self.stc)

@patch(
"speedtest_wrapper.SpeedtestCollector.run_speedtest",
return_value=MOCK_SPEEDTEST_JSON,
)
def test_collect(self, mock_run_speedtest: Mock) -> None:
guages = []
for guage in self.stc.collect():
guages.append(guage)
self.assertEqual("spt01renonv.reno.nv.charter.com", self.stc.speedtest_host)
self.assertEqual(19, len(guages))


if __name__ == "__main__":
unittest.main()

0 comments on commit 95e55be

Please sign in to comment.