From c4d32d2e646ecfcf23d6ae4984d03626a4836408 Mon Sep 17 00:00:00 2001 From: QuadStingray Date: Wed, 3 Feb 2021 08:40:46 +0100 Subject: [PATCH] refactoring to ndt7 go client --- Dockerfile | 5 +- Gopkg.lock | 91 ++++- Gopkg.toml | 35 +- README.md | 69 ++-- app.go | 191 ++++++--- docker-compose.yml | 5 +- .../provisioning/dashboards/speedtest.json | 382 ++++++++++++------ model/comandline.go | 20 +- model/geoip.go | 147 +++++++ model/influxdb.go | 12 +- model/model.go | 40 +- model/speedtest/output_humanreadable.go | 99 +++++ model/speedtest/output_interface.go | 36 ++ model/speedtest/output_silent.go | 47 +++ model/speedtest/runner.go | 62 +++ model/speedtest/servers.go | 66 +++ model/speedtest/summary.go | 45 +++ run.sh | 14 +- 18 files changed, 1078 insertions(+), 288 deletions(-) create mode 100644 model/geoip.go create mode 100644 model/speedtest/output_humanreadable.go create mode 100644 model/speedtest/output_interface.go create mode 100644 model/speedtest/output_silent.go create mode 100644 model/speedtest/runner.go create mode 100644 model/speedtest/servers.go create mode 100644 model/speedtest/summary.go diff --git a/Dockerfile b/Dockerfile index d8a1ed1..903ac60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,9 @@ ENV INTERVAL=3600 \ HOST="local" \ SPEEDTEST_SERVER="" \ SPEEDTEST_LIST_SERVERS="false" \ - SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING="false" \ - SPEEDTEST_ALGO_TYPE="max" \ + SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING="true" \ + SPEEDTEST_DISTANCE_UNIT="K" \ + INCLUDE_READABLE_OUTPUT="false" \ SHOW_EXTERNAL_IP="false" RUN apk add ca-certificates diff --git a/Gopkg.lock b/Gopkg.lock index 1b1b89a..39a65ac 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,14 +3,22 @@ [[projects]] branch = "master" - digest = "1:7219a79e7d775b1813e267812e39f04ed15ece1428b1c3e5b24df78e37ab7795" - name = "github.com/dchest/uniuri" + digest = "1:a1ca357da2f3338f46f1bea17c3d12bac9844c9ee4f4c946f6e988ceceb0f430" + name = "github.com/araddon/dateparse" packages = ["."] pruneopts = "UT" - revision = "7aecb25e1fe5a22533fab90a637a8f74a9cf7340" + revision = "8aadafed4dc4aee1363ec2a04c9c954544ee54dc" [[projects]] - digest = "1:9b73396bc7a21f88702fe3d314ca7860843c08f9c787bb47f008075f840df23d" + digest = "1:6d29f02f0f01c627c2be40fb7347669a9ff2aa215cb97747294c1d13ffa74bdd" + name = "github.com/gorilla/websocket" + packages = ["."] + pruneopts = "UT" + revision = "b65e62901fc1c0d968042419e74789f6af455eb9" + version = "v1.4.2" + +[[projects]] + digest = "1:9503164eeca57526c77c3c64872566feaa164be6675616e4442b682e3d0f3360" name = "github.com/influxdata/influxdb" packages = [ "client/v2", @@ -18,32 +26,85 @@ "pkg/escape", ] pruneopts = "UT" - revision = "f46f63d4e2d9684a2dd716594ab609ccd32f0a5b" - version = "v1.7.10" + revision = "bc8ec4384eed25436d31045f974bf39f3310fa3c" + version = "v1.8.4" [[projects]] branch = "master" - digest = "1:55f089b1e361a4f914da543f651f6ffe2d878d8186d64d261f174ecb32b92cc9" + digest = "1:2c74dbaaa02fb3ea3caae8488cec68a50e66f5673fa4a50bf89e7e1b85ba71c9" name = "github.com/kylegrantlucas/speedtest" + packages = ["coords"] + pruneopts = "UT" + revision = "f8512f58e7ead921fded157e7fc4261a694679f5" + +[[projects]] + digest = "1:02a42284672777908afc09ce52c70798ad8da06c553205540522e196bb862488" + name = "github.com/m-lab/go" + packages = [ + "anonymize", + "flagx", + "rtx", + ] + pruneopts = "UT" + revision = "5fa2a44869946e033b55656c443575b2c7affbad" + version = "v0.1.44" + +[[projects]] + digest = "1:6cdd662b3a81cb4eae59d01141b63a37ccf4a0da881cab4027e5c16ec5047749" + name = "github.com/m-lab/locate" + packages = [ + "api/locate", + "api/v2", + ] + pruneopts = "UT" + revision = "8459a0eee3d6872c1a22e25500b194fad1d993f3" + version = "v0.7.0" + +[[projects]] + digest = "1:bdb2da1f4bd4b91971965f7a7124d352196d6e37f27851adac2c83bc96d350d9" + name = "github.com/m-lab/ndt-server" + packages = [ + "metadata", + "ndt7/model", + ] + pruneopts = "UT" + revision = "91f8780890e5829454af479232931907138ee459" + version = "v0.20.5" + +[[projects]] + digest = "1:7349aa614b949a12137c8f4575f91dd56988c9364f80bbcc4b5ff2d1c5bfd4b3" + name = "github.com/m-lab/ndt7-client-go" packages = [ ".", - "coords", - "http", - "util", - "xml", + "internal/download", + "internal/params", + "internal/upload", + "internal/websocketx", + "spec", ] pruneopts = "UT" - revision = "f8512f58e7ead921fded157e7fc4261a694679f5" + revision = "10bc591e88fea3c55aa81bbb39812047733a2bb5" + version = "v0.4.1" + +[[projects]] + digest = "1:4958b2b86de6ad6ecf9c1401c49bdc5ab32ba1d6041eb9a21d4be89da63c80c8" + name = "github.com/m-lab/tcp-info" + packages = [ + "inetdiag", + "tcp", + ] + pruneopts = "UT" + revision = "7481b6e6971fc76ee2b55f3b238d0d444b702f12" + version = "v1.5.3" [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/dchest/uniuri", "github.com/influxdata/influxdb/client/v2", - "github.com/kylegrantlucas/speedtest", "github.com/kylegrantlucas/speedtest/coords", - "github.com/kylegrantlucas/speedtest/http", + "github.com/m-lab/ndt7-client-go", + "github.com/m-lab/ndt7-client-go/spec", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 24f0f17..1b674e2 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,30 +1,11 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/m-lab/ndt7-client-go" + version = "0.4.1" + +[[constraint]] + name = "github.com/influxdata/influxdb" + version = "1.8.3" diff --git a/README.md b/README.md index 0f8bce9..d4773da 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,72 @@ -# speedtest-influxdb:0.9.3 +# speedtest-influxdb:1.0.0 - [Introduction](#introduction) - - [Contributing](#contributing) - - [Issues](#issues) + - [Contributing](#contributing) + - [Issues](#issues) - [Getting started](#getting-started) - - [Installation](#installation) - - [Quickstart](#quickstart) - - [Environment Variables](#environment-variables) - - [Grafana](#grafana) + - [Installation](#installation) + - [Quickstart](#quickstart) + - [Environment Variables](#environment-variables) + - [Grafana](#grafana) # Introduction -Git-Repository to build [Docker](https://www.docker.com/) Container Image to run speedtest with [speedtest.net](http://www.speedtest.net/) to influxdb. The Implementation is inspired by https://github.com/frdmn/docker-speedtest + +Git-Repository to build [Docker](https://www.docker.com/) Container Image to run speedtest with [NDT7 Server](https://github.com/m-lab/ndt-server) from [mLabs](https://www.measurementlab.net/tests/ndt/ndt7/) to influxdb. The Implementation is inspired +by https://github.com/frdmn/docker-speedtest ## Contributing + If you find this image helpfull, so you can see here how you can help: + - Create an new branch and send a pull request with your features and bug fixes - Help users resolve their [issues](https://github.com/QuadStingray/docker-speedtest-influxdb/issues). ## Issues -Before reporting your issue please try updating Docker to the latest version and check if it resolves the issue. Refer to the Docker [installation guide](https://docs.docker.com/installation) for instructions. + +Before reporting your issue please try updating Docker to the latest version and check if it resolves the issue. Refer to the +Docker [installation guide](https://docs.docker.com/installation) for instructions. If that recommendations do not help then [report your issue](https://github.com/QuadStingray/docker-speedtest-influxdb/issues/new) along with the following information: - Output of the `docker version` and `docker info` commands -- The `docker run` command or `docker-compose.yml` used to start the - image. Mask out the sensitive bits. +- The `docker run` command or `docker-compose.yml` used to start the image. Mask out the sensitive bits. # Getting started + ## Installation + Automated builds of the image are available on [Dockerhub](https://hub.docker.com/r/quadstingray/speedtest-influxdb/) ```bash -docker pull speedtest-influxdb:0.9.3 +docker pull speedtest-influxdb:1.0.0 ``` Alternatively you can build the image yourself. + ```bash docker build . --tag 'speedtest-influxdb:dev'; ``` ## Quickstart + ```bash -docker run -e "HOST=local" speedtest-influxdb:0.9.3 +docker run -e "HOST=local" speedtest-influxdb:1.0.0 ``` *Alternatively, you can use the sample [docker-compose.yml](docker-compose.yml) file to start the container using [Docker Compose](https://docs.docker.com/compose/)* - ## Environment Variables | Variable | Default Value | Informations | |:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------| | INTERVAL | 3600 | Seconds between import of statistics | | HOST | local | host where the speedtest is running for grafana filter | -| [SPEEDTEST_SERVER](#environment-variable-speedtest_server) | '' | speedtest.net server. Empty string, means speedtest return server for test | -| SPEEDTEST_ALGO_TYPE | 'max' | how to calculate the speedtest up- and downlad values. changing of `SPEEDTEST_ALGO_TYPE` means avg | -| SPEEDTEST_LIST_SERVERS | 'false' | list all available speedtest.net servers at the console | -| SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING | 'false' | keep docker container running after listing all speedtet.net servers | +| [SPEEDTEST_SERVER](#environment-variable-speedtest_server) | '' | ndt 7 server. Empty string, means speedtest return server for test | +| INCLUDE_READABLE_OUTPUT | false | Log Speedtest Output to Console | +| SPEEDTEST_DISTANCE_UNIT | K | Unit for Distance Calculation K = Kilometers, N = Nautical Miles other Values = Miles | +| SPEEDTEST_LIST_SERVERS | 'false' | list all available ndt7 servers at the console | +| SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING | 'true' | keep docker container running after listing all ndt7 servers | | SHOW_EXTERNAL_IP | 'false' | You can activate logging your external Ip to InfluxDb to monitor IP changes. | | INFLUXDB_USE | 'true' | You can deactivate save speedtest results to influx | | INFLUXDB_URL | http://influxdb:8086 | Url of your InfluxDb installation | @@ -65,22 +74,30 @@ docker run -e "HOST=local" speedtest-influxdb:0.9.3 | INFLUXDB_USER | DEFAULT | optional user for insert to your InfluxDb | | INFLUXDB_PWD | DEFAULT | optional password for insert to your InfluxDb | +### Removed Variables + +* SPEEDTEST_ALGO_TYPE + ### Environment Variable: SPEEDTEST_SERVER -Per default the server is choosen by speedtest.net, but you can set `SPEEDTEST_SERVER` with the id of your favorite server. -You can get a list of all available servers by set the evironment variable `SPEEDTEST_LIST_SERVERS` to `true`. The list is ordered by country. + +Per default the server is choosen automatically, but you can set `SPEEDTEST_SERVER` with the id of your favorite server. If your favorite Server doesn't answer a default search server +is choosen. You can get a list of all available servers by set the evironment variable `SPEEDTEST_LIST_SERVERS` to `true`. The list is ordered by country. ``` ... -2018/07/18 00:16:53 County: Virgin Islands | Location: Saint Croix | ServerId: 4470 | Sponsor: Viya -2018/07/18 00:16:53 County: Virgin Islands | Location: Saint Croix | ServerId: 6762 | Sponsor: VI Next Generation Network -2018/07/18 00:16:53 County: Virgin Islands | Location: Road Town | ServerId: 7633 | Sponsor: CCTBVI -2018/07/18 00:16:53 County: Virgin Islands, British | Location: Road Town | ServerId: 17056 | Sponsor: Flow BVI -2018/07/18 00:16:53 County: Wales | Location: Pembrokeshire | ServerId: 16607 | Sponsor: Pembs Wifi Ltd -2018/07/18 00:16:53 County: Wales | Location: Newport | ServerId: 5833 | Sponsor: Hub Network Services Ltd +2021/02/02 09:16:09 County: AU | Location: Sydney | ServerId: syd03 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: AU | Location: Sydney | ServerId: syd02 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru01 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru03 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru05 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru04 | UplinkSpeed: 10g | Roundrobin: true +2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru02 | UplinkSpeed: 10g | Roundrobin: true + ... ``` ## Grafana + There is an sample grafana dashboard at this repository. You can import that to your Grafana installation. [speedtest.json](docker/grafana/provisioning/dashboards/speedtest.json) ![](https://raw.githubusercontent.com/QuadStingray/docker-speedtest-influxdb/master/images/speedtest_dashboard.png) diff --git a/app.go b/app.go index 344653c..8b5cef8 100644 --- a/app.go +++ b/app.go @@ -1,17 +1,25 @@ package main import ( - "github.com/dchest/uniuri" - "github.com/kylegrantlucas/speedtest" + "context" "github.com/kylegrantlucas/speedtest/coords" - "github.com/kylegrantlucas/speedtest/http" + "github.com/m-lab/ndt7-client-go" + "github.com/m-lab/ndt7-client-go/spec" "log" + "net" "os" "quadstingray/speedtest-influxdb/model" + "quadstingray/speedtest-influxdb/model/speedtest" "sort" "time" ) +const ( + clientName = "speedtest-influxdb" + clientVersion = "1.0.0" + defaultTimeout = 60 * time.Second +) + func main() { settings := model.Parser() @@ -26,7 +34,11 @@ func main() { } for true { - log.Printf("speed test started") + + if settings.IncludeHumanReadable { + log.Printf("speed test started") + } + stats, err := runTest(settings) if err != nil { @@ -35,12 +47,9 @@ func main() { if settings.InfluxDbSettings.Use_Influx { go model.SaveToInfluxDb(stats, settings) } - if settings.ShowMyIp { - log.Printf("Ping: %3.2f ms | Download: %3.2f Mbps | Upload: %3.2f Mbps | External_Ip: %s", stats.Ping, stats.Down_Mbs, stats.Up_Mbs, stats.Client.ExternalIp) - } else { - log.Printf("Ping: %3.2f ms | Download: %3.2f Mbps | Upload: %3.2f Mbps", stats.Ping, stats.Down_Mbs, stats.Up_Mbs) + if settings.IncludeHumanReadable { + log.Printf("sleep for %v seconds", settings.Interval) } - log.Printf("sleep for %v seconds", settings.Interval) time.Sleep(time.Duration(settings.Interval) * time.Second) } @@ -48,12 +57,8 @@ func main() { } func listServers() { - client, err := speedtest.NewDefaultClient() - if err != nil { - log.Printf("error creating client: %v", err) - } - allServers, err := client.HTTPClient.GetServers() + allServers, err := speedtest.ListServer() if err != nil { log.Printf("error creating client: %v", err) } @@ -63,56 +68,140 @@ func listServers() { }) for _, v := range allServers { - log.Printf("County: %v | Location: %v | ServerId: %v | Sponsor: %v", v.Country, v.Name, v.ID, v.Sponsor) + log.Printf("County: %v | Location: %v | ServerId: %v | UplinkSpeed: %v | Roundrobin: %v", v.Country, v.City, v.Site, v.UplinkSpeed, v.Roundrobin) } } -func speedTestClient(settings model.Settings) (*speedtest.Client, error) { - config := &http.SpeedtestConfig{ - ConfigURL: "http://c.speedtest.net/speedtest-config.php?x=" + uniuri.New(), - ServersURL: "http://c.speedtest.net/speedtest-servers-static.php?x=" + uniuri.New(), - AlgoType: settings.AlgoType, - NumClosest: 3, - NumLatencyTests: 3, - UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.21 Safari/537.36", +func runTest(settings model.Settings) (model.SpeedTestStatistics, error) { + + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + var output speedtest.OutputType + + if settings.IncludeHumanReadable { + output = speedtest.NewHumanReadable() + } else { + output = speedtest.SilentOutput{} } - timeOut := time.Hour - return speedtest.NewClient(config, speedtest.DefaultDLSizes, speedtest.DefaultULSizes, timeOut) -} -func runTest(settings model.Settings) (model.SpeedTestStatistics, error) { + var r = speedtest.TestRunner{ + ndt7.NewClient(clientName, clientVersion), + output, + } - client, err := speedtest.NewDefaultClient() + if settings.Server != "" { + r.Client.Server = "ndt-mlab3-" + settings.Server + ".mlab-oti.measurement-lab.org" + } - if err != nil { - log.Printf("error creating client: %v", err) + var code int + code += r.RunDownload(ctx) + code += r.RunUpload(ctx) + + if code != 0 { + code = 0 + log.Printf("No Connection to Server %v restart with search new NDT7 Sever", r.Client.Server) + r.Client.Server = "" + code += r.RunDownload(ctx) + code += r.RunUpload(ctx) + if code != 0 { + os.Exit(code) + } } - // Pass an empty string to select the fastest server - server, err := client.GetServer(settings.Server) - if err != nil { - log.Printf("error getting server: %v", err) + s := makeSummary(r.Client.FQDN, r.Client.Results()) + r.Output.OnSummary(s) + + geoClient, _ := model.CheckIpLocation(s.ClientIP) + time.Sleep(time.Duration(1) * time.Second) + geoSever, _ := speedtest.FindServerByFQDN(s.ServerFQDN) + + var distance float64 + if geoSever.Lat == 0 && geoSever.Lon == 0 || geoClient.Lat == 0 && geoClient.Lon == 0 { + distance = 0 } else { + distance = model.Distance(geoSever.Lat, geoSever.Lon, geoClient.Lat, geoClient.Lon, settings.DistanceUnit) + } + + return model.SpeedTestStatistics{ + model.ClientInformations{ + ExternalIp: s.ClientIP, + Provider: geoClient.Org, + Coordinate: coords.Coordinate{ + geoClient.Lat, + geoClient.Lon, + }, + }, + model.Server{ + URL: s.ServerFQDN, + Lat: geoSever.Lat, + Lon: geoSever.Lon, + Name: s.ServerFQDN, + Country: geoSever.Country, + City: geoSever.City, + Distance: distance, + Latency: 0, + }, + s.MinRTT.Value, + s.Download.Value, + s.Upload.Value, + s.DownloadRetrans.Value, + }, nil +} + +func makeSummary(FQDN string, results map[spec.TestKind]*ndt7.LatestMeasurements) *speedtest.Summary { - client, err := speedTestClient(settings) - if err != nil { - log.Printf("error creating client: %v", err) - } + s := speedtest.NewSummary(FQDN) - dmbps, err := client.Download(server) - if err != nil { - log.Printf("error getting download: %v", err) - } + if results[spec.TestDownload] != nil && + results[spec.TestDownload].ConnectionInfo != nil { + // Get UUID, ClientIP and ServerIP from ConnectionInfo. + s.DownloadUUID = results[spec.TestDownload].ConnectionInfo.UUID - umbps, err := client.Upload(server) - if err != nil { - log.Printf("error getting upload: %v", err) - } + clientIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Client) + if err == nil { + s.ClientIP = clientIP + } + + serverIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Server) + if err == nil { + s.ServerIP = serverIP + } + } - clientInformations := model.ClientInformations{client.HTTPClient.Config.IP, client.HTTPClient.Config.Isp, coords.Coordinate{client.HTTPClient.Config.Lat, client.HTTPClient.Config.Lon}} + if dl, ok := results[spec.TestDownload]; ok { + if dl.Client.AppInfo != nil && dl.Client.AppInfo.ElapsedTime > 0 { + elapsed := float64(dl.Client.AppInfo.ElapsedTime) / 1e06 + s.Download = speedtest.ValueUnitPair{ + Value: (8.0 * float64(dl.Client.AppInfo.NumBytes)) / + elapsed / (1000.0 * 1000.0), + Unit: "Mbit/s", + } + } + if dl.Server.TCPInfo != nil { + if dl.Server.TCPInfo.BytesSent > 0 { + s.DownloadRetrans = speedtest.ValueUnitPair{ + Value: float64(dl.Server.TCPInfo.BytesRetrans) / float64(dl.Server.TCPInfo.BytesSent) * 100, + Unit: "%", + } + } + s.MinRTT = speedtest.ValueUnitPair{ + Value: float64(dl.Server.TCPInfo.MinRTT) / 1000, + Unit: "ms", + } + } + } + // Upload comes from the client-side Measurement during the upload test. + if ul, ok := results[spec.TestUpload]; ok { + if ul.Client.AppInfo != nil && ul.Client.AppInfo.ElapsedTime > 0 { + elapsed := float64(ul.Client.AppInfo.ElapsedTime) / 1e06 + s.Upload = speedtest.ValueUnitPair{ + Value: (8.0 * float64(ul.Client.AppInfo.NumBytes)) / + elapsed / (1000.0 * 1000.0), + Unit: "Mbit/s", + } + } + } - result := model.SpeedTestStatistics{clientInformations, server, server.Latency, dmbps, umbps} - return result, nil - } - return nil, "Error choosing server" + return s } diff --git a/docker-compose.yml b/docker-compose.yml index 75baf28..96b0798 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '2' services: influxdb: restart: always - image: influxdb:1.5 + image: influxdb:1.7.10 #volumes: #- "./data/influxdb/:/var/lib/influxdb" environment: @@ -18,9 +18,10 @@ services: - "influxdb:influxdb" environment: - "INTERVAL=120" +# - "SPEEDTEST_LIST_SERVERS=true" grafana: restart: always - image: grafana/grafana:5.1.0 + image: grafana/grafana:7.3.7 volumes: - "./docker/grafana/provisioning:/etc/grafana/provisioning" ports: diff --git a/docker/grafana/provisioning/dashboards/speedtest.json b/docker/grafana/provisioning/dashboards/speedtest.json index 6a28be9..71fb1bc 100755 --- a/docker/grafana/provisioning/dashboards/speedtest.json +++ b/docker/grafana/provisioning/dashboards/speedtest.json @@ -13,7 +13,7 @@ } ] }, - "description": "Display speedtest.net results (ping, upload and download speed)", + "description": "Display ndt7 speedtest results (ping, upload and download speed)", "editable": true, "gnetId": null, "graphTooltip": 2, @@ -21,22 +21,43 @@ "panels": [ { "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], "datasource": "InfluxDB", - "decimals": null, - "format": "Mbits", - "gauge": { - "maxValue": 200, - "minValue": 0, - "show": true, - "thresholdLabels": true, - "thresholdMarkers": true + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [ + { + "$$hashKey": "object:282", + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "max": 200, + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 50 + } + ] + }, + "unit": "Mbits" + }, + "overrides": [] }, "gridPos": { "h": 8, @@ -47,40 +68,20 @@ "id": 10, "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "$$hashKey": "object:279", - "name": "value to text", - "value": 1 - }, - { - "$$hashKey": "object:280", - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": true, + "showThresholdMarkers": true }, - "tableColumn": "", + "pluginVersion": "7.3.7", "targets": [ { "$$hashKey": "object:187", @@ -111,7 +112,7 @@ }, { "params": [], - "type": "mean" + "type": "median" } ] ], @@ -124,40 +125,50 @@ ] } ], - "thresholds": "", "timeFrom": "1w", "timeShift": null, "title": "Average download speed / week", - "type": "singlestat", - "valueFontSize": "50%", - "valueMaps": [ - { - "$$hashKey": "object:282", - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" + "type": "gauge" }, { "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], "datasource": "InfluxDB", - "decimals": null, - "format": "Mbits", - "gauge": { - "maxValue": 30, - "minValue": 0, - "show": true, - "thresholdLabels": true, - "thresholdMarkers": true + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [ + { + "$$hashKey": "object:282", + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "max": 30, + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "green", + "value": 10 + } + ] + }, + "unit": "Mbits" + }, + "overrides": [] }, "gridPos": { "h": 8, @@ -168,40 +179,20 @@ "id": 8, "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "$$hashKey": "object:279", - "name": "value to text", - "value": 1 - }, - { - "$$hashKey": "object:280", - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": true, + "showThresholdMarkers": true }, - "tableColumn": "", + "pluginVersion": "7.3.7", "targets": [ { "$$hashKey": "object:187", @@ -232,7 +223,7 @@ }, { "params": [], - "type": "mean" + "type": "median" } ] ], @@ -245,25 +236,20 @@ ] } ], - "thresholds": "", "timeFrom": "1w", "timeShift": null, "title": "Average upload speed / week", - "type": "singlestat", - "valueFontSize": "50%", - "valueMaps": [ - { - "$$hashKey": "object:282", - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" + "type": "gauge" }, { "columns": [], "datasource": "InfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fontSize": "100%", "gridPos": { "h": 4, @@ -282,13 +268,35 @@ }, "styles": [ { + "$$hashKey": "object:589", "alias": "Time", + "align": "auto", "dateFormat": "YYYY-MM-DD HH:mm:ss", "pattern": "Time", "type": "date" }, { + "$$hashKey": "object:628", "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "Distance", + "thresholds": [], + "type": "number", + "unit": "lengthkm" + }, + { + "$$hashKey": "object:590", + "alias": "", + "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", @@ -354,7 +362,7 @@ ], "title": "Last Locations (Distance)", "transform": "table", - "type": "table" + "type": "table-old" }, { "cacheTimeout": null, @@ -367,6 +375,12 @@ ], "datasource": "InfluxDB", "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "ms", "gauge": { "maxValue": 0, @@ -483,13 +497,21 @@ "dashLength": 10, "dashes": false, "datasource": "InfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 }, + "hiddenSeries": false, "id": 4, "legend": { "alignAsTable": false, @@ -505,7 +527,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -605,6 +631,7 @@ ], "thresholds": [], "timeFrom": null, + "timeRegions": [], "timeShift": null, "title": "Upload / Download", "tooltip": { @@ -612,7 +639,6 @@ "sort": 0, "value_type": "individual" }, - "transparent": false, "type": "graph", "xaxis": { "buckets": null, @@ -652,13 +678,21 @@ "dashLength": 10, "dashes": false, "datasource": "InfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }, + "hiddenSeries": false, "id": 2, "legend": { "alignAsTable": false, @@ -674,7 +708,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -730,6 +768,7 @@ ], "thresholds": [], "timeFrom": null, + "timeRegions": [], "timeShift": null, "title": "Ping", "tooltip": { @@ -737,7 +776,6 @@ "sort": 0, "value_type": "individual" }, - "transparent": false, "type": "graph", "xaxis": { "buckets": null, @@ -774,6 +812,12 @@ { "columns": [], "datasource": "InfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fontSize": "100%", "gridPos": { "h": 8, @@ -787,29 +831,99 @@ "scroll": true, "showHeader": true, "sort": { - "col": 0, - "desc": true + "col": null, + "desc": false }, "styles": [ { - "alias": "Time", + "$$hashKey": "object:299", + "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "Time", - "type": "date" + "decimals": 2, + "mappingType": 1, + "pattern": "Distance", + "thresholds": [], + "type": "number", + "unit": "lengthkm" }, { + "$$hashKey": "object:310", "alias": "", + "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, - "pattern": "/.*/", + "mappingType": 1, + "pattern": "Download", "thresholds": [], "type": "number", + "unit": "Mbits" + }, + { + "$$hashKey": "object:321", + "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "Ping", + "thresholds": [], + "type": "number", + "unit": "ms" + }, + { + "$$hashKey": "object:332", + "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "Time", + "thresholds": [], + "type": "date", "unit": "short" + }, + { + "$$hashKey": "object:1500", + "alias": "", + "align": "auto", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "Upload", + "thresholds": [], + "type": "number", + "unit": "Mbits" } ], "targets": [ @@ -886,7 +1000,7 @@ }, { "params": [ - "Server ID" + "Server" ], "type": "alias" } @@ -957,11 +1071,11 @@ ], "title": "All Requests", "transform": "table", - "type": "table" + "type": "table-old" } ], "refresh": false, - "schemaVersion": 16, + "schemaVersion": 26, "style": "dark", "tags": [], "templating": { @@ -999,5 +1113,5 @@ "timezone": "", "title": "Speedtest results", "uid": "0A6hxROiz", - "version": 2 + "version": 1 } \ No newline at end of file diff --git a/model/comandline.go b/model/comandline.go index ff013ca..5d27c1f 100644 --- a/model/comandline.go +++ b/model/comandline.go @@ -17,31 +17,33 @@ func Parser() Settings { var keepProcessRunning bool var showExternalIp bool var saveToInfluxDb bool - var algoType string + var distanceUnit string + var includeHumanOutput bool flag.IntVar(&interval, "interval", 3600, "seconds between statistics import") - flag.StringVar(&server, "server", "", "speedtest.net server") flag.StringVar(&host, "host", "", "host where the speedetest is running") flag.StringVar(&influxHost, "influxHost", "http://influxdb:8086", "host of your influxdb instance") - flag.StringVar(&influxDB, "influxDB", "rspamd", "influxdb database") + flag.StringVar(&influxDB, "influxDB", "speetest", "influxdb database") flag.StringVar(&influxUser, "influxUser", "DEFAULT", "influxdb Username") flag.StringVar(&influxPwd, "influxPwd", "DEFAULT", "influxdb Password") - flag.StringVar(&algoType, "algoType", "max", "save and show external Ip of docker host") + flag.StringVar(&distanceUnit, "distanceUnit", "K", "Distance Unit between GeoPoints possible Values K|M|N") + flag.BoolVar(&includeHumanOutput, "includeHumanOutput", true, "Log HumanReadableOutput to Console") flag.BoolVar(&saveToInfluxDb, "saveToInfluxDb", false, "save to influxdb") - flag.BoolVar(&list, "list", false, "list servers") flag.BoolVar(&keepProcessRunning, "keepProcessRunning", false, "keep process running") flag.BoolVar(&showExternalIp, "showExternalIp", true, "save and show external Ip of docker host") + flag.StringVar(&server, "server", "", "ndt7 server") + flag.BoolVar(&list, "list", false, "list servers") + flag.Parse() log.Println("**************************************************************") log.Println("******** Parser started with following commands **************") log.Printf("** interval %v", interval) - log.Println("** SpeedtestAlgoType " + algoType) - log.Println("** server " + server) - log.Println("** host " + host) + log.Println("** Distance Unit " + distanceUnit) + log.Println("** Host " + host) if showExternalIp { log.Println("** showExternalIp: true") @@ -64,5 +66,5 @@ func Parser() Settings { log.Println("**************************************************************") log.Println("**************************************************************") - return Settings{interval, host, server, algoType, list, keepProcessRunning, showExternalIp, InfluxDbSettings{saveToInfluxDb, influxHost, influxUser, influxPwd, influxDB}} + return Settings{interval, host, server, distanceUnit, list, keepProcessRunning, showExternalIp, includeHumanOutput, InfluxDbSettings{saveToInfluxDb, influxHost, influxUser, influxPwd, influxDB}} } diff --git a/model/geoip.go b/model/geoip.go new file mode 100644 index 0000000..b781d18 --- /dev/null +++ b/model/geoip.go @@ -0,0 +1,147 @@ +package model + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math" + "net/http" +) + +type GeoIP struct { + // The right side is the name of the JSON variable + Ip string `json:"ip"` + Country string `json:"country"` + CountryName string `json:"country_name"` + RegionCode string `json:"region_code"` + Region string `json:"region"` + City string `json:"city"` + Postal string `json:"postal"` + Lat float64 `json:"latitude"` + Lon float64 `json:"longitude"` + ContinentCode string `json:"continent_code"` + InEu bool `json:"in_eu"` + Timezone string `json:"timezone"` + UtcOffset string `json:"utc_offset"` + CountryCallingCode string `json:"country_calling_code"` + Currency string `json:"currency"` + Languages string `json:"languages"` + Asn string `json:"asn"` + Org string `json:"org"` +} + +func CheckIpLocation(ip string) (GeoIP, error) { + + var ( + err error + geo GeoIP + response *http.Response + body []byte + ) + + response, err = http.Get("https://ipapi.co/" + ip + "/json/") + if err != nil { + fmt.Println(err) + } + + defer response.Body.Close() + + body, err = ioutil.ReadAll(response.Body) + if err != nil { + fmt.Println(err) + } + + err = json.Unmarshal(body, &geo) + if err != nil { + fmt.Println(err) + } + + return geo, nil +} + +func LocateUser() (GeoIP, error) { + + var ( + err error + geo GeoIP + response *http.Response + body []byte + ) + + response, err = http.Get("https://ipapi.co/json/") + if err != nil { + fmt.Println(err) + } + + defer response.Body.Close() + + body, err = ioutil.ReadAll(response.Body) + if err != nil { + fmt.Println(err) + } + + err = json.Unmarshal(body, &geo) + if err != nil { + fmt.Println(err) + } + + // Everything accessible in struct now + fmt.Println("\n==== IP Geolocation Info ====\n") + fmt.Println("IP address:\t", geo.Ip) + fmt.Println("Country Code:\t", geo.CountryName) + fmt.Println("Country Name:\t", geo.CountryName) + fmt.Println("Zip Code:\t", geo.Postal) + fmt.Println("Latitude:\t", geo.Lat) + fmt.Println("Longitude:\t", geo.Lon) + fmt.Println("Metro Code:\t", geo.City) + + return geo, nil +} + +//func DistanceBetweenUserAndIp(ip string, unit string) (float64, error) { +// geoClient, _ := LocateUser() +// time.Sleep(time.Duration(1) * time.Second) +// geoSever, _ := CheckIpLocation(ip) +// return Distance(geoSever.Lat, geoClient.Lat, geoSever.Lon, geoClient.Lon, unit), nil +//} +// +//func DistanceBetweenIps(ip1 string, ip2 string, unit string) (float64, error) { +// geoClient, _ := CheckIpLocation(ip1) +// time.Sleep(time.Duration(1) * time.Second) +// geoSever, _ := CheckIpLocation(ip2) +// return Distance(geoSever.Lat, geoClient.Lat, geoSever.Lon, geoClient.Lon, unit), nil +//} +// +//func DistanceBetweenIpAndGeoIp(ip string, geoIp GeoIP, unit string) (float64, error) { +// time.Sleep(time.Duration(1) * time.Second) +// geoSever, _ := CheckIpLocation(ip) +// return Distance(geoSever.Lat, geoIp.Lat, geoSever.Lon, geoIp.Lon, unit), nil +//} + +func Distance(lat1 float64, lng1 float64, lat2 float64, lng2 float64, unit string) float64 { + const PI float64 = 3.141592653589793 + + radlat1 := float64(PI * lat1 / 180) + radlat2 := float64(PI * lat2 / 180) + + theta := float64(lng1 - lng2) + radtheta := float64(PI * theta / 180) + + dist := math.Sin(radlat1)*math.Sin(radlat2) + math.Cos(radlat1)*math.Cos(radlat2)*math.Cos(radtheta) + + if dist > 1 { + dist = 1 + } + + dist = math.Acos(dist) + dist = dist * 180 / PI + dist = dist * 60 * 1.1515 + + if unit == "K" { + dist = dist * 1.609344 + } else if unit == "N" { + dist = dist * 0.8684 + } + + return dist +} diff --git a/model/influxdb.go b/model/influxdb.go index 9a3090a..72acff4 100644 --- a/model/influxdb.go +++ b/model/influxdb.go @@ -22,7 +22,7 @@ func SaveToInfluxDb(statistics SpeedTestStatistics, settings Settings) { Precision: "s", }) if err != nil { - log.Printf("error new batch point: %v", err) + log.Printf("error new batch point: %v", err) } // Create a point and add to batch @@ -33,9 +33,8 @@ func SaveToInfluxDb(statistics SpeedTestStatistics, settings Settings) { "upload_mbs": statistics.Up_Mbs, "ping": statistics.Ping, "distance": statistics.Server.Distance, - "serverid": statistics.Server.ID, - "location": statistics.Server.Name + ", " + statistics.Server.Country, - "sponsor": statistics.Server.Sponsor, + "serverid": statistics.Server.Name, + "location": statistics.Server.City + ", " + statistics.Server.Country, "clientProvider": statistics.Client.Provider, } @@ -45,10 +44,9 @@ func SaveToInfluxDb(statistics SpeedTestStatistics, settings Settings) { "upload_mbs": statistics.Up_Mbs, "ping": statistics.Ping, "distance": statistics.Server.Distance, - "serverid": statistics.Server.ID, - "location": statistics.Server.Name + ", " + statistics.Server.Country, + "serverid": statistics.Server.Name, + "location": statistics.Server.City + ", " + statistics.Server.Country, "external_ip": statistics.Client.ExternalIp, - "sponsor": statistics.Server.Sponsor, "clientProvider": statistics.Client.Provider, } } diff --git a/model/model.go b/model/model.go index d86807a..c3222f6 100644 --- a/model/model.go +++ b/model/model.go @@ -2,7 +2,6 @@ package model import ( "github.com/kylegrantlucas/speedtest/coords" - "github.com/kylegrantlucas/speedtest/http" ) type InfluxDbSettings struct { @@ -14,14 +13,15 @@ type InfluxDbSettings struct { } type Settings struct { - Interval int - Host string - Server string - AlgoType string - ListServers bool - KeepProcessRunning bool - ShowMyIp bool - InfluxDbSettings InfluxDbSettings + Interval int + Host string + Server string + DistanceUnit string + ListServers bool + KeepProcessRunning bool + ShowMyIp bool + IncludeHumanReadable bool + InfluxDbSettings InfluxDbSettings } type ClientInformations struct { @@ -31,9 +31,21 @@ type ClientInformations struct { } type SpeedTestStatistics struct { - Client ClientInformations - Server http.Server - Ping float64 - Down_Mbs float64 - Up_Mbs float64 + Client ClientInformations + Server Server + Ping float64 + Down_Mbs float64 + Up_Mbs float64 + DownRetransPercent float64 +} + +type Server struct { + URL string + Lat float64 + Lon float64 + Name string + Country string + City string + Distance float64 + Latency float64 } diff --git a/model/speedtest/output_humanreadable.go b/model/speedtest/output_humanreadable.go new file mode 100644 index 0000000..d51094b --- /dev/null +++ b/model/speedtest/output_humanreadable.go @@ -0,0 +1,99 @@ +package speedtest + +import ( + "errors" + "fmt" + "github.com/m-lab/ndt7-client-go/spec" + "io" + "os" +) + +// HumanReadable is a human readable emitter. It emits the events generated +// by running a ndt7 test as pleasant stdout messages. +type HumanReadable struct { + out io.Writer +} + +// NewHumanReadable returns a new human readable emitter. +func NewHumanReadable() OutputType { + return HumanReadable{os.Stdout} +} + +// NewHumanReadableWithWriter returns a new human readable emitter using the +// specified writer. +func NewHumanReadableWithWriter(w io.Writer) OutputType { + return HumanReadable{w} +} + +// OnStarting handles the start event +func (h HumanReadable) OnStarting(test spec.TestKind) error { + _, err := fmt.Fprintf(h.out, "\rstarting %s", test) + return err +} + +// OnError handles the error event +func (h HumanReadable) OnError(test spec.TestKind, err error) error { + _, failure := fmt.Fprintf(h.out, "\r%s failed: %s\n", test, err.Error()) + return failure +} + +// OnConnected handles the connected event +func (h HumanReadable) OnConnected(test spec.TestKind, fqdn string) error { + _, err := fmt.Fprintf(h.out, "\r%s in progress with %s\n", test, fqdn) + return err +} + +// OnDownloadEvent handles an event emitted by the download test +func (h HumanReadable) OnDownloadEvent(m *spec.Measurement) error { + return h.onSpeedEvent(m) +} + +// OnUploadEvent handles an event emitted during the upload test +func (h HumanReadable) OnUploadEvent(m *spec.Measurement) error { + return h.onSpeedEvent(m) +} + +func (h HumanReadable) onSpeedEvent(m *spec.Measurement) error { + // The specification recommends that we show application level + // measurements. Let's just do that in interactive mode. To this + // end, we ignore any measurement coming from the server. + if m.Origin != spec.OriginClient { + return nil + } + if m.AppInfo == nil || m.AppInfo.ElapsedTime <= 0 { + return errors.New("Missing m.AppInfo or invalid m.AppInfo.ElapsedTime") + } + elapsed := float64(m.AppInfo.ElapsedTime) / 1e06 + v := (8.0 * float64(m.AppInfo.NumBytes)) / elapsed / (1000.0 * 1000.0) + _, err := fmt.Fprintf(h.out, "\rAvg. speed : %7.1f Mbit/s", v) + return err +} + +// OnComplete handles the complete event +func (h HumanReadable) OnComplete(test spec.TestKind) error { + _, err := fmt.Fprintf(h.out, "\n%s: complete\n", test) + return err +} + +// OnSummary handles the summary event. +func (h HumanReadable) OnSummary(s *Summary) error { + const summaryFormat = `%15s: %s +%15s: %s +%15s: %7.1f %s +%15s: %7.1f %s +%15s: %7.1f %s +%15s: %7.2f %s +` + _, err := fmt.Fprintf(h.out, summaryFormat, + "Server", s.ServerFQDN, + "Client", s.ClientIP, + "Latency", s.MinRTT.Value, s.MinRTT.Unit, + "Download", s.Download.Value, s.Upload.Unit, + "Upload", s.Upload.Value, s.Upload.Unit, + "Retransmission", s.DownloadRetrans.Value, s.DownloadRetrans.Unit) + if err != nil { + return err + } + + return nil +} diff --git a/model/speedtest/output_interface.go b/model/speedtest/output_interface.go new file mode 100644 index 0000000..15d091d --- /dev/null +++ b/model/speedtest/output_interface.go @@ -0,0 +1,36 @@ +package speedtest + +import ( + "github.com/m-lab/ndt7-client-go/spec" +) + +// OutputType is a generic OutputType. When an event occurs, the +// corresponding method will be called. An error will generally +// mean that it's not possible to write the output. A common +// case where this happen is where the output is redirected to +// a file on a full hard disk. +// +// See the documentation of the main package for more details +// on the sequence in which events may occur. +type OutputType interface { + // OnStarting is emitted before attempting to start a test. + OnStarting(test spec.TestKind) error + + // OnError is emitted if a test cannot start. + OnError(test spec.TestKind, err error) error + + // OnConnected is emitted when we connected to the speedtest server. + OnConnected(test spec.TestKind, fqdn string) error + + // OnDownloadEvent is emitted during the download. + OnDownloadEvent(m *spec.Measurement) error + + // OnUploadEvent is emitted during the upload. + OnUploadEvent(m *spec.Measurement) error + + // OnComplete is always emitted when the test is over. + OnComplete(test spec.TestKind) error + + // OnSummary is emitted after the test is over. + OnSummary(s *Summary) error +} diff --git a/model/speedtest/output_silent.go b/model/speedtest/output_silent.go new file mode 100644 index 0000000..0939696 --- /dev/null +++ b/model/speedtest/output_silent.go @@ -0,0 +1,47 @@ +package speedtest + +import ( + "github.com/m-lab/ndt7-client-go/spec" +) + +type SilentOutput struct { +} + +// OnStarting handles the start event +func (h SilentOutput) OnStarting(test spec.TestKind) error { + return nil +} + +// OnError handles the error event +func (h SilentOutput) OnError(test spec.TestKind, err error) error { + return nil +} + +// OnConnected handles the connected event +func (h SilentOutput) OnConnected(test spec.TestKind, fqdn string) error { + return nil +} + +// OnDownloadEvent handles an event emitted by the download test +func (h SilentOutput) OnDownloadEvent(m *spec.Measurement) error { + return h.onSpeedEvent(m) +} + +// OnUploadEvent handles an event emitted during the upload test +func (h SilentOutput) OnUploadEvent(m *spec.Measurement) error { + return h.onSpeedEvent(m) +} + +func (h SilentOutput) onSpeedEvent(m *spec.Measurement) error { + return nil +} + +// OnComplete handles the complete event +func (h SilentOutput) OnComplete(test spec.TestKind) error { + return nil +} + +// OnSummary handles the summary event. +func (h SilentOutput) OnSummary(s *Summary) error { + return nil +} diff --git a/model/speedtest/runner.go b/model/speedtest/runner.go new file mode 100644 index 0000000..a3f680a --- /dev/null +++ b/model/speedtest/runner.go @@ -0,0 +1,62 @@ +package speedtest + +import ( + "context" + "github.com/m-lab/ndt7-client-go" + "github.com/m-lab/ndt7-client-go/spec" +) + +type TestRunner struct { + Client *ndt7.Client + Output OutputType +} + +func (r TestRunner) doRunTest( + ctx context.Context, test spec.TestKind, + start func(context.Context) (<-chan spec.Measurement, error), + emitEvent func(m *spec.Measurement) error, +) int { + ch, err := start(ctx) + if err != nil { + r.Output.OnError(test, err) + return 1 + } + err = r.Output.OnConnected(test, r.Client.FQDN) + if err != nil { + return 1 + } + for ev := range ch { + err = emitEvent(&ev) + if err != nil { + return 1 + } + } + return 0 +} + +func (r TestRunner) runTest( + ctx context.Context, test spec.TestKind, + start func(context.Context) (<-chan spec.Measurement, error), + emitEvent func(m *spec.Measurement) error, +) int { + err := r.Output.OnStarting(test) + if err != nil { + return 1 + } + code := r.doRunTest(ctx, test, start, emitEvent) + err = r.Output.OnComplete(test) + if err != nil { + return 1 + } + return code +} + +func (r TestRunner) RunDownload(ctx context.Context) int { + return r.runTest(ctx, spec.TestDownload, r.Client.StartDownload, + r.Output.OnDownloadEvent) +} + +func (r TestRunner) RunUpload(ctx context.Context) int { + return r.runTest(ctx, spec.TestUpload, r.Client.StartUpload, + r.Output.OnUploadEvent) +} diff --git a/model/speedtest/servers.go b/model/speedtest/servers.go new file mode 100644 index 0000000..bce61ef --- /dev/null +++ b/model/speedtest/servers.go @@ -0,0 +1,66 @@ +package speedtest + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +type SpeedTestServer struct { + // The right side is the name of the JSON variable + Country string `json:"country"` + City string `json:"city"` + Lat float64 `json:"latitude"` + Lon float64 `json:"longitude"` + Roundrobin bool `json:"roundrobin"` + Site string `json:"site"` + UplinkSpeed string `json:"uplink_speed"` +} + +func ListServer() ([]SpeedTestServer, error) { + var ( + err error + servers []SpeedTestServer + response *http.Response + body []byte + ) + + response, err = http.Get("https://siteinfo.mlab-oti.measurementlab.net/v1/sites/locations.json") + if err != nil { + fmt.Println(err) + } + + defer response.Body.Close() + + body, err = ioutil.ReadAll(response.Body) + if err != nil { + fmt.Println(err) + } + + err = json.Unmarshal(body, &servers) + if err != nil { + fmt.Println(err) + } + + return servers, nil +} + +func FindServerByFQDN(fqdn string) (SpeedTestServer, error) { + serverList, _ := ListServer() + for _, server := range serverList { + if strings.Contains(fqdn, "."+server.Site+".") || strings.Contains(fqdn, "-"+server.Site+"-") || strings.Contains(fqdn, "-"+server.Site+".") { + return server, nil + } + + } + serverList2, _ := ListServer() + for _, server := range serverList2 { + if strings.Contains(fqdn, "."+server.Site+".") || strings.Contains(fqdn, "-"+server.Site+"-") || strings.Contains(fqdn, "-"+server.Site+".") { + return server, nil + } + + } + return SpeedTestServer{}, nil +} diff --git a/model/speedtest/summary.go b/model/speedtest/summary.go new file mode 100644 index 0000000..6243e62 --- /dev/null +++ b/model/speedtest/summary.go @@ -0,0 +1,45 @@ +package speedtest + +// ValueUnitPair represents a {"Value": ..., "Unit": ...} pair. +type ValueUnitPair struct { + Value float64 + Unit string +} + +// Summary is a struct containing the values displayed to the user at +// the end of an speedtest test. +type Summary struct { + // ServerFQDN is the FQDN of the server used for this test. + ServerFQDN string + + // ServerIP is the (v4 or v6) IP address of the server. + ServerIP string + + // ClientIP is the (v4 or v6) IP address of the Client. + ClientIP string + + // DownloadUUID is the UUID of the download test. + DownloadUUID string + + // Download is the download speed, in Mbit/s. This is measured at the + // receiver. + Download ValueUnitPair + + // Upload is the upload speed, in Mbit/s. This is measured at the sender. + Upload ValueUnitPair + + // DownloadRetrans is the retransmission rate. This is based on the TCPInfo + // values provided by the server during a download test. + DownloadRetrans ValueUnitPair + + // RTT is the round-trip time of the latest measurement, in milliseconds. + // This is provided by the server during a download test. + MinRTT ValueUnitPair +} + +// NewSummary returns a new Summary struct for a given FQDN. +func NewSummary(FQDN string) *Summary { + return &Summary{ + ServerFQDN: FQDN, + } +} diff --git a/run.sh b/run.sh index 6b78139..f436691 100755 --- a/run.sh +++ b/run.sh @@ -1,3 +1,15 @@ #!/bin/bash -./speedtestInfluxDB -saveToInfluxDb="$INFLUXDB_USE" -interval="$INTERVAL" -host="$HOST" -server="$SPEEDTEST_SERVER" -influxHost="$INFLUXDB_URL" -influxDB="$INFLUXDB_DB" -influxUser="$INFLUXDB_USER" -influxPwd="$INFLUXDB_PWD" -list="$SPEEDTEST_LIST_SERVERS" -showExternalIp="$SHOW_EXTERNAL_IP" -keepProcessRunning="$SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING" -algoType="$SPEEDTEST_ALGO_TYPE" \ No newline at end of file +./speedtestInfluxDB -interval="$INTERVAL" \ + -saveToInfluxDb="$INFLUXDB_USE" \ + -influxHost="$INFLUXDB_URL" \ + -influxDB="$INFLUXDB_DB" \ + -influxUser="$INFLUXDB_USER" \ + -influxPwd="$INFLUXDB_PWD" \ + -host="$HOST" \ + -server="$SPEEDTEST_SERVER" \ + -list="$SPEEDTEST_LIST_SERVERS" \ + -keepProcessRunning="$SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING" \ + -distanceUnit="$SPEEDTEST_DISTANCE_UNIT" \ + -includeHumanOutput="$INCLUDE_READABLE_OUTPUT" \ + -showExternalIp="$SHOW_EXTERNAL_IP" \ No newline at end of file