├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.go ├── docker-compose.yml ├── docker-speedtest.iml ├── docker └── grafana │ └── provisioning │ ├── dashboards │ ├── dashboard.yml │ └── speedtest.json │ └── datasources │ └── influxdb.yml ├── images └── speedtest_dashboard.png ├── model ├── comandline.go ├── geoip.go ├── influxdb.go ├── model.go └── speedtest │ ├── output_humanreadable.go │ ├── output_interface.go │ ├── output_silent.go │ ├── runner.go │ ├── servers.go │ └── summary.go └── run.sh /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.13 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -v . 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/dictionaries 5 | .idea/**/shelf 6 | .idea/**/dataSources/ 7 | .idea/**/dataSources.ids 8 | .idea/**/dataSources.local.xml 9 | .idea/**/sqlDataSources.xml 10 | .idea/**/dynamic.xml 11 | .idea/**/uiDesigner.xml 12 | .idea/**/dbnavigator.xml 13 | .idea/**/gradle.xml 14 | .idea/**/libraries 15 | cmake-build-debug/ 16 | cmake-build-release/ 17 | .idea/**/mongoSettings.xml 18 | *.iws 19 | out/ 20 | .idea_modules/ 21 | atlassian-ide-plugin.xml 22 | .idea/replstate.xml 23 | com_crashlytics_export_strings.xml 24 | crashlytics.properties 25 | crashlytics-build.properties 26 | fabric.properties 27 | .idea/httpRequests 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | Icon 32 | ._* 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | *.exe 46 | *.exe~ 47 | *.dll 48 | *.so 49 | *.dylib 50 | *.test 51 | *.out 52 | vendor 53 | .idea 54 | go.mod 55 | go.sum -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build-env 2 | 3 | # Set go bin which doesn't appear to be set already. 4 | ENV GOBIN /go/bin 5 | 6 | RUN apk update && apk upgrade && \ 7 | apk add --no-cache bash git openssh 8 | 9 | # build directories 10 | ADD . /go/src/quadstingray/speedtest-influxdb 11 | WORKDIR /go/src/quadstingray/speedtest-influxdb 12 | 13 | RUN export GO111MODULE=on 14 | RUN go mod init 15 | RUN go mod tidy 16 | 17 | # Build my app 18 | RUN go build -o speedtestInfluxDB *.go 19 | 20 | # final stage 21 | FROM alpine 22 | WORKDIR /app 23 | 24 | MAINTAINER QuadStingray 25 | 26 | ENV INTERVAL=3600 \ 27 | INFLUXDB_USE="true" \ 28 | INFLUXDB_DB="speedtest" \ 29 | INFLUXDB_URL="http://influxdb:8086" \ 30 | INFLUXDB_USER="DEFAULT" \ 31 | INFLUXDB_PWD="DEFAULT" \ 32 | HOST="local" \ 33 | SPEEDTEST_SERVER="" \ 34 | SPEEDTEST_LIST_SERVERS="false" \ 35 | SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING="true" \ 36 | SPEEDTEST_DISTANCE_UNIT="K" \ 37 | INCLUDE_READABLE_OUTPUT="false" \ 38 | RETRY_ZERO_VALUE="false" \ 39 | RETRY_INTERVAL=300 \ 40 | SHOW_EXTERNAL_IP="false" 41 | 42 | RUN apk add ca-certificates 43 | COPY --from=build-env /go/src/quadstingray/speedtest-influxdb/speedtestInfluxDB /app/speedtestInfluxDB 44 | ADD run.sh run.sh 45 | CMD sh run.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # speedtest-influxdb:1.1.0 2 | 3 | - [Introduction](#introduction) 4 | - [Contributing](#contributing) 5 | - [Issues](#issues) 6 | - [Getting started](#getting-started) 7 | - [Installation](#installation) 8 | - [Quickstart](#quickstart) 9 | - [Environment Variables](#environment-variables) 10 | - [Grafana](#grafana) 11 | 12 | # Introduction 13 | 14 | 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 15 | by https://github.com/frdmn/docker-speedtest 16 | 17 | ## Contributing 18 | 19 | If you find this image helpfull, so you can see here how you can help: 20 | 21 | - Create an new branch and send a pull request with your features and bug fixes 22 | - Help users resolve their [issues](https://github.com/QuadStingray/docker-speedtest-influxdb/issues). 23 | 24 | ## Issues 25 | 26 | Before reporting your issue please try updating Docker to the latest version and check if it resolves the issue. Refer to the 27 | Docker [installation guide](https://docs.docker.com/installation) for instructions. 28 | 29 | If that recommendations do not help then [report your issue](https://github.com/QuadStingray/docker-speedtest-influxdb/issues/new) along with the following information: 30 | 31 | - Output of the `docker version` and `docker info` commands 32 | - The `docker run` command or `docker-compose.yml` used to start the image. Mask out the sensitive bits. 33 | 34 | # Getting started 35 | 36 | ## Installation 37 | 38 | Automated builds of the image are available on 39 | [Dockerhub](https://hub.docker.com/r/quadstingray/speedtest-influxdb/) 40 | 41 | ```bash 42 | docker pull quadstingray/speedtest-influxdb 43 | 44 | ``` 45 | 46 | Alternatively you can build the image yourself. 47 | 48 | ```bash 49 | docker build . --tag 'quadstingray/speedtest-influxdb:dev'; 50 | ``` 51 | 52 | ## Quickstart 53 | 54 | ```bash 55 | docker run -e "HOST=local" quadstingray/speedtest-influxdb:1.1.0 56 | ``` 57 | 58 | *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/)* 59 | 60 | ## Environment Variables 61 | 62 | | Variable | Default Value | Informations | 63 | |:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------| 64 | | INTERVAL | 3600 | Seconds between import of statistics | 65 | | RETRY_INTERVAL | 300 | Seconds between retry of statistics import | 66 | | HOST | local | host where the speedtest is running for grafana filter | 67 | | [SPEEDTEST_SERVER](#environment-variable-speedtest_server) | '' | ndt 7 server. Empty string, means speedtest return server for test | 68 | | INCLUDE_READABLE_OUTPUT | false | Log Speedtest Output to Console | 69 | | RETRY_ZERO_VALUE | false | Retry Speedtest at Zero Values returned | 70 | | SPEEDTEST_DISTANCE_UNIT | K | Unit for Distance Calculation K = Kilometers, N = Nautical Miles other Values = Miles | 71 | | SPEEDTEST_LIST_SERVERS | 'false' | list all available ndt7 servers at the console | 72 | | SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING | 'true' | keep docker container running after listing all ndt7 servers | 73 | | SHOW_EXTERNAL_IP | 'false' | You can activate logging your external Ip to InfluxDb to monitor IP changes. | 74 | | INFLUXDB_USE | 'true' | You can deactivate save speedtest results to influx | 75 | | INFLUXDB_URL | http://influxdb:8086 | Url of your InfluxDb installation | 76 | | INFLUXDB_DB | speedtest | Database at your InfluxDb installation | 77 | | INFLUXDB_USER | DEFAULT | optional user for insert to your InfluxDb | 78 | | INFLUXDB_PWD | DEFAULT | optional password for insert to your InfluxDb | 79 | 80 | ### Removed Variables 81 | 82 | * SPEEDTEST_ALGO_TYPE 83 | 84 | ### Environment Variable: SPEEDTEST_SERVER 85 | 86 | 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 87 | 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. 88 | 89 | ``` 90 | ... 91 | 2021/02/02 09:16:09 County: AU | Location: Sydney | ServerId: syd03 | UplinkSpeed: 10g | Roundrobin: true 92 | 2021/02/02 09:16:09 County: AU | Location: Sydney | ServerId: syd02 | UplinkSpeed: 10g | Roundrobin: true 93 | 2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru01 | UplinkSpeed: 10g | Roundrobin: true 94 | 2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru03 | UplinkSpeed: 10g | Roundrobin: true 95 | 2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru05 | UplinkSpeed: 10g | Roundrobin: true 96 | 2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru04 | UplinkSpeed: 10g | Roundrobin: true 97 | 2021/02/02 09:16:09 County: BE | Location: Brussels | ServerId: bru02 | UplinkSpeed: 10g | Roundrobin: true 98 | 99 | ... 100 | ``` 101 | 102 | ## Grafana 103 | 104 | 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) 105 | 106 | ![](https://raw.githubusercontent.com/QuadStingray/docker-speedtest-influxdb/master/images/speedtest_dashboard.png) 107 | 108 | 109 | ## Todo: 110 | * Code Clean Up 111 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/m-lab/ndt7-client-go" 7 | "github.com/m-lab/ndt7-client-go/spec" 8 | "log" 9 | "net" 10 | "os" 11 | "quadstingray/speedtest-influxdb/model" 12 | "quadstingray/speedtest-influxdb/model/speedtest" 13 | "sort" 14 | "time" 15 | ) 16 | 17 | const ( 18 | clientName = "speedtest-influxdb" 19 | clientVersion = "1.1.0" 20 | defaultTimeout = 60 * time.Second 21 | ) 22 | 23 | func main() { 24 | settings := model.Parser() 25 | 26 | if settings.ListServers { 27 | listServers() 28 | if settings.KeepProcessRunning { 29 | for true { 30 | time.Sleep(time.Duration(1) * time.Second) 31 | } 32 | } 33 | os.Exit(0) 34 | } 35 | 36 | for true { 37 | 38 | if settings.IncludeHumanReadable { 39 | log.Printf("speed test started") 40 | } 41 | 42 | stats, err := runTest(settings) 43 | 44 | if err != nil { 45 | time.Sleep(time.Duration(settings.RetryInterval) * time.Second) 46 | } else { 47 | if !settings.RetryZeroValue || (stats.Down_Mbs != 0 || stats.Up_Mbs != 0) { 48 | if settings.InfluxDbSettings.Use_Influx { 49 | go model.SaveToInfluxDb(stats, settings) 50 | } 51 | if settings.IncludeHumanReadable { 52 | log.Printf("sleep for %v seconds", settings.Interval) 53 | } 54 | time.Sleep(time.Duration(settings.Interval) * time.Second) 55 | } else { 56 | time.Sleep(time.Duration(settings.RetryInterval) * time.Second) 57 | } 58 | } 59 | 60 | } 61 | } 62 | 63 | func listServers() { 64 | 65 | allServers, err := speedtest.ListServer() 66 | if err != nil { 67 | log.Printf("error creating client: %v", err) 68 | } 69 | 70 | sort.Slice(allServers, func(i, j int) bool { 71 | return allServers[i].Country < allServers[j].Country 72 | }) 73 | 74 | for _, v := range allServers { 75 | log.Printf("County: %v | Location: %v | ServerId: %v | UplinkSpeed: %v | Roundrobin: %v", v.Country, v.City, v.Site, v.UplinkSpeed, v.Roundrobin) 76 | } 77 | } 78 | 79 | func runTest(settings model.Settings) (model.SpeedTestStatistics, error) { 80 | //geoClient2, _ := model.LocateUser() 81 | //log.Printf("%v", geoClient2) 82 | 83 | ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) 84 | defer cancel() 85 | 86 | var output speedtest.OutputType 87 | 88 | if settings.IncludeHumanReadable { 89 | output = speedtest.NewHumanReadable() 90 | } else { 91 | output = speedtest.SilentOutput{} 92 | } 93 | 94 | var r = speedtest.TestRunner{ 95 | ndt7.NewClient(clientName, clientVersion), 96 | output, 97 | } 98 | 99 | if settings.Server != "" { 100 | r.Client.Server = "ndt-mlab3-" + settings.Server + ".mlab-oti.measurement-lab.org" 101 | } 102 | 103 | var code int 104 | code += r.RunDownload(ctx) 105 | code += r.RunUpload(ctx) 106 | 107 | if code != 0 { 108 | code = 0 109 | log.Printf("No Connection to Server %v restart with search new NDT7 Sever", r.Client.Server) 110 | r.Client.Server = "" 111 | code += r.RunDownload(ctx) 112 | code += r.RunUpload(ctx) 113 | if code != 0 { 114 | return model.SpeedTestStatistics{ 115 | Client: model.ClientInformations{}, 116 | Server: model.Server{}, 117 | Ping: 0, 118 | Down_Mbs: 0, 119 | Up_Mbs: 0, 120 | DownRetransPercent: 0, 121 | }, errors.New("server not reachable") 122 | } 123 | } 124 | 125 | s := makeSummary(r.Client.FQDN, r.Client.Results()) 126 | r.Output.OnSummary(s) 127 | 128 | geoClient, _ := model.CheckIpLocation(s.ClientIP) 129 | time.Sleep(time.Duration(1) * time.Second) 130 | geoSever, _ := speedtest.FindServerByFQDN(s.ServerFQDN) 131 | 132 | var distance float64 133 | if geoSever.Lat == 0 && geoSever.Lon == 0 || geoClient.Lat == 0 && geoClient.Lon == 0 { 134 | distance = 0 135 | } else { 136 | distance = model.Distance(geoSever.Lat, geoSever.Lon, geoClient.Lat, geoClient.Lon, settings.DistanceUnit) 137 | } 138 | 139 | return model.SpeedTestStatistics{ 140 | model.ClientInformations{ 141 | ExternalIp: s.ClientIP, 142 | Provider: geoClient.Org, 143 | Coordinate: model.Coordinate{ 144 | geoClient.Lat, 145 | geoClient.Lon, 146 | }, 147 | }, 148 | model.Server{ 149 | URL: s.ServerFQDN, 150 | Lat: geoSever.Lat, 151 | Lon: geoSever.Lon, 152 | Name: s.ServerFQDN, 153 | Country: geoSever.Country, 154 | City: geoSever.City, 155 | Distance: distance, 156 | Latency: 0, 157 | }, 158 | s.MinRTT.Value, 159 | s.Download.Value, 160 | s.Upload.Value, 161 | s.DownloadRetrans.Value, 162 | }, nil 163 | } 164 | 165 | func makeSummary(FQDN string, results map[spec.TestKind]*ndt7.LatestMeasurements) *speedtest.Summary { 166 | 167 | s := speedtest.NewSummary(FQDN) 168 | 169 | if results[spec.TestDownload] != nil && 170 | results[spec.TestDownload].ConnectionInfo != nil { 171 | // Get UUID, ClientIP and ServerIP from ConnectionInfo. 172 | s.DownloadUUID = results[spec.TestDownload].ConnectionInfo.UUID 173 | 174 | clientIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Client) 175 | if err == nil { 176 | s.ClientIP = clientIP 177 | } 178 | 179 | serverIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Server) 180 | if err == nil { 181 | s.ServerIP = serverIP 182 | } 183 | } 184 | 185 | if dl, ok := results[spec.TestDownload]; ok { 186 | if dl.Client.AppInfo != nil && dl.Client.AppInfo.ElapsedTime > 0 { 187 | elapsed := float64(dl.Client.AppInfo.ElapsedTime) / 1e06 188 | s.Download = speedtest.ValueUnitPair{ 189 | Value: (8.0 * float64(dl.Client.AppInfo.NumBytes)) / 190 | elapsed / (1000.0 * 1000.0), 191 | Unit: "Mbit/s", 192 | } 193 | } 194 | if dl.Server.TCPInfo != nil { 195 | if dl.Server.TCPInfo.BytesSent > 0 { 196 | s.DownloadRetrans = speedtest.ValueUnitPair{ 197 | Value: float64(dl.Server.TCPInfo.BytesRetrans) / float64(dl.Server.TCPInfo.BytesSent) * 100, 198 | Unit: "%", 199 | } 200 | } 201 | s.MinRTT = speedtest.ValueUnitPair{ 202 | Value: float64(dl.Server.TCPInfo.MinRTT) / 1000, 203 | Unit: "ms", 204 | } 205 | } 206 | } 207 | // Upload comes from the client-side Measurement during the upload test. 208 | if ul, ok := results[spec.TestUpload]; ok { 209 | if ul.Client.AppInfo != nil && ul.Client.AppInfo.ElapsedTime > 0 { 210 | elapsed := float64(ul.Client.AppInfo.ElapsedTime) / 1e06 211 | s.Upload = speedtest.ValueUnitPair{ 212 | Value: (8.0 * float64(ul.Client.AppInfo.NumBytes)) / 213 | elapsed / (1000.0 * 1000.0), 214 | Unit: "Mbit/s", 215 | } 216 | } 217 | } 218 | 219 | return s 220 | } 221 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | influxdb: 5 | restart: always 6 | image: influxdb:1.7.10 7 | #volumes: 8 | #- "./data/influxdb/:/var/lib/influxdb" 9 | environment: 10 | - "INFLUXDB_ADMIN_USER=admin" 11 | - "INFLUXDB_ADMIN_PASSWORD=password" 12 | - "INFLUXDB_DB=speedtest" 13 | speedtest-influxdb: 14 | restart: always 15 | # image: quadstingray/speedtest-influxdb:0.8.0 16 | build: . 17 | links: 18 | - "influxdb:influxdb" 19 | environment: 20 | - "INTERVAL=120" 21 | # - "SPEEDTEST_LIST_SERVERS=true" 22 | grafana: 23 | restart: always 24 | image: grafana/grafana:7.3.7 25 | volumes: 26 | - "./docker/grafana/provisioning:/etc/grafana/provisioning" 27 | ports: 28 | - "3000:3000" 29 | links: 30 | - "influxdb:influxdb" 31 | environment: 32 | - "GF_SERVER_ROOT_URL=http://localhost" 33 | - "GF_SECURITY_ADMIN_PASSWORD=admin" 34 | - "GF_AUTH_ANONYMOUS_ENABLED=true" -------------------------------------------------------------------------------- /docker-speedtest.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/provisioning/dashboards 12 | -------------------------------------------------------------------------------- /docker/grafana/provisioning/dashboards/speedtest.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "$$hashKey": "object:947", 6 | "builtIn": 1, 7 | "datasource": "-- Grafana --", 8 | "enable": true, 9 | "hide": true, 10 | "iconColor": "rgba(0, 211, 255, 1)", 11 | "name": "Annotations & Alerts", 12 | "type": "dashboard" 13 | } 14 | ] 15 | }, 16 | "description": "Display ndt7 speedtest results (ping, upload and download speed)", 17 | "editable": true, 18 | "gnetId": null, 19 | "graphTooltip": 2, 20 | "links": [], 21 | "panels": [ 22 | { 23 | "cacheTimeout": null, 24 | "datasource": "InfluxDB", 25 | "fieldConfig": { 26 | "defaults": { 27 | "custom": {}, 28 | "mappings": [ 29 | { 30 | "$$hashKey": "object:282", 31 | "id": 0, 32 | "op": "=", 33 | "text": "N/A", 34 | "type": 1, 35 | "value": "null" 36 | } 37 | ], 38 | "max": 200, 39 | "min": 0, 40 | "nullValueMode": "connected", 41 | "thresholds": { 42 | "mode": "absolute", 43 | "steps": [ 44 | { 45 | "color": "dark-red", 46 | "value": null 47 | }, 48 | { 49 | "color": "yellow", 50 | "value": 20 51 | }, 52 | { 53 | "color": "green", 54 | "value": 50 55 | } 56 | ] 57 | }, 58 | "unit": "Mbits" 59 | }, 60 | "overrides": [] 61 | }, 62 | "gridPos": { 63 | "h": 8, 64 | "w": 8, 65 | "x": 0, 66 | "y": 0 67 | }, 68 | "id": 10, 69 | "interval": null, 70 | "links": [], 71 | "maxDataPoints": 100, 72 | "options": { 73 | "orientation": "horizontal", 74 | "reduceOptions": { 75 | "calcs": [ 76 | "mean" 77 | ], 78 | "fields": "", 79 | "values": false 80 | }, 81 | "showThresholdLabels": true, 82 | "showThresholdMarkers": true 83 | }, 84 | "pluginVersion": "7.3.7", 85 | "targets": [ 86 | { 87 | "$$hashKey": "object:187", 88 | "alias": "Download", 89 | "groupBy": [ 90 | { 91 | "params": [ 92 | "1h" 93 | ], 94 | "type": "time" 95 | } 96 | ], 97 | "hide": false, 98 | "measurement": "speedtest", 99 | "orderByTime": "ASC", 100 | "policy": "default", 101 | "query": "SELECT mean(\"value\") FROM \"download\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", 102 | "rawQuery": false, 103 | "refId": "A", 104 | "resultFormat": "time_series", 105 | "select": [ 106 | [ 107 | { 108 | "params": [ 109 | "download_mbs" 110 | ], 111 | "type": "field" 112 | }, 113 | { 114 | "params": [], 115 | "type": "median" 116 | } 117 | ] 118 | ], 119 | "tags": [ 120 | { 121 | "key": "host", 122 | "operator": "=", 123 | "value": "local" 124 | } 125 | ] 126 | } 127 | ], 128 | "timeFrom": "1w", 129 | "timeShift": null, 130 | "title": "Average download speed / week", 131 | "type": "gauge" 132 | }, 133 | { 134 | "cacheTimeout": null, 135 | "datasource": "InfluxDB", 136 | "fieldConfig": { 137 | "defaults": { 138 | "custom": {}, 139 | "mappings": [ 140 | { 141 | "$$hashKey": "object:282", 142 | "id": 0, 143 | "op": "=", 144 | "text": "N/A", 145 | "type": 1, 146 | "value": "null" 147 | } 148 | ], 149 | "max": 30, 150 | "min": 0, 151 | "nullValueMode": "connected", 152 | "thresholds": { 153 | "mode": "absolute", 154 | "steps": [ 155 | { 156 | "color": "red", 157 | "value": null 158 | }, 159 | { 160 | "color": "yellow", 161 | "value": 5 162 | }, 163 | { 164 | "color": "green", 165 | "value": 10 166 | } 167 | ] 168 | }, 169 | "unit": "Mbits" 170 | }, 171 | "overrides": [] 172 | }, 173 | "gridPos": { 174 | "h": 8, 175 | "w": 8, 176 | "x": 8, 177 | "y": 0 178 | }, 179 | "id": 8, 180 | "interval": null, 181 | "links": [], 182 | "maxDataPoints": 100, 183 | "options": { 184 | "orientation": "horizontal", 185 | "reduceOptions": { 186 | "calcs": [ 187 | "mean" 188 | ], 189 | "fields": "", 190 | "values": false 191 | }, 192 | "showThresholdLabels": true, 193 | "showThresholdMarkers": true 194 | }, 195 | "pluginVersion": "7.3.7", 196 | "targets": [ 197 | { 198 | "$$hashKey": "object:187", 199 | "alias": "Upload", 200 | "groupBy": [ 201 | { 202 | "params": [ 203 | "1h" 204 | ], 205 | "type": "time" 206 | } 207 | ], 208 | "hide": false, 209 | "measurement": "speedtest", 210 | "orderByTime": "ASC", 211 | "policy": "default", 212 | "query": "SELECT mean(\"value\") FROM \"download\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", 213 | "rawQuery": false, 214 | "refId": "A", 215 | "resultFormat": "time_series", 216 | "select": [ 217 | [ 218 | { 219 | "params": [ 220 | "upload_mbs" 221 | ], 222 | "type": "field" 223 | }, 224 | { 225 | "params": [], 226 | "type": "median" 227 | } 228 | ] 229 | ], 230 | "tags": [ 231 | { 232 | "key": "host", 233 | "operator": "=", 234 | "value": "local" 235 | } 236 | ] 237 | } 238 | ], 239 | "timeFrom": "1w", 240 | "timeShift": null, 241 | "title": "Average upload speed / week", 242 | "type": "gauge" 243 | }, 244 | { 245 | "columns": [], 246 | "datasource": "InfluxDB", 247 | "fieldConfig": { 248 | "defaults": { 249 | "custom": {} 250 | }, 251 | "overrides": [] 252 | }, 253 | "fontSize": "100%", 254 | "gridPos": { 255 | "h": 4, 256 | "w": 8, 257 | "x": 16, 258 | "y": 0 259 | }, 260 | "id": 12, 261 | "links": [], 262 | "pageSize": null, 263 | "scroll": true, 264 | "showHeader": true, 265 | "sort": { 266 | "col": 0, 267 | "desc": true 268 | }, 269 | "styles": [ 270 | { 271 | "$$hashKey": "object:589", 272 | "alias": "Time", 273 | "align": "auto", 274 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 275 | "pattern": "Time", 276 | "type": "date" 277 | }, 278 | { 279 | "$$hashKey": "object:628", 280 | "alias": "", 281 | "align": "auto", 282 | "colorMode": null, 283 | "colors": [ 284 | "rgba(245, 54, 54, 0.9)", 285 | "rgba(237, 129, 40, 0.89)", 286 | "rgba(50, 172, 45, 0.97)" 287 | ], 288 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 289 | "decimals": 2, 290 | "mappingType": 1, 291 | "pattern": "Distance", 292 | "thresholds": [], 293 | "type": "number", 294 | "unit": "lengthkm" 295 | }, 296 | { 297 | "$$hashKey": "object:590", 298 | "alias": "", 299 | "align": "auto", 300 | "colorMode": null, 301 | "colors": [ 302 | "rgba(245, 54, 54, 0.9)", 303 | "rgba(237, 129, 40, 0.89)", 304 | "rgba(50, 172, 45, 0.97)" 305 | ], 306 | "decimals": 2, 307 | "pattern": "/.*/", 308 | "thresholds": [], 309 | "type": "number", 310 | "unit": "short" 311 | } 312 | ], 313 | "targets": [ 314 | { 315 | "$$hashKey": "object:1112", 316 | "groupBy": [], 317 | "hide": false, 318 | "limit": "3", 319 | "measurement": "speedtest", 320 | "orderByTime": "DESC", 321 | "policy": "default", 322 | "refId": "A", 323 | "resultFormat": "table", 324 | "select": [ 325 | [ 326 | { 327 | "params": [ 328 | "location" 329 | ], 330 | "type": "field" 331 | }, 332 | { 333 | "params": [ 334 | "Location" 335 | ], 336 | "type": "alias" 337 | } 338 | ], 339 | [ 340 | { 341 | "params": [ 342 | "distance" 343 | ], 344 | "type": "field" 345 | }, 346 | { 347 | "params": [ 348 | "Distance" 349 | ], 350 | "type": "alias" 351 | } 352 | ] 353 | ], 354 | "tags": [ 355 | { 356 | "key": "host", 357 | "operator": "=", 358 | "value": "local" 359 | } 360 | ] 361 | } 362 | ], 363 | "title": "Last Locations (Distance)", 364 | "transform": "table", 365 | "type": "table-old" 366 | }, 367 | { 368 | "cacheTimeout": null, 369 | "colorBackground": false, 370 | "colorValue": false, 371 | "colors": [ 372 | "#299c46", 373 | "rgba(237, 129, 40, 0.89)", 374 | "#d44a3a" 375 | ], 376 | "datasource": "InfluxDB", 377 | "decimals": null, 378 | "fieldConfig": { 379 | "defaults": { 380 | "custom": {} 381 | }, 382 | "overrides": [] 383 | }, 384 | "format": "ms", 385 | "gauge": { 386 | "maxValue": 0, 387 | "minValue": 0, 388 | "show": false, 389 | "thresholdLabels": true, 390 | "thresholdMarkers": true 391 | }, 392 | "gridPos": { 393 | "h": 4, 394 | "w": 8, 395 | "x": 16, 396 | "y": 4 397 | }, 398 | "id": 6, 399 | "interval": null, 400 | "links": [], 401 | "mappingType": 1, 402 | "mappingTypes": [ 403 | { 404 | "$$hashKey": "object:279", 405 | "name": "value to text", 406 | "value": 1 407 | }, 408 | { 409 | "$$hashKey": "object:280", 410 | "name": "range to text", 411 | "value": 2 412 | } 413 | ], 414 | "maxDataPoints": 100, 415 | "nullPointMode": "connected", 416 | "nullText": null, 417 | "postfix": "", 418 | "postfixFontSize": "50%", 419 | "prefix": "", 420 | "prefixFontSize": "50%", 421 | "rangeMaps": [ 422 | { 423 | "from": "null", 424 | "text": "N/A", 425 | "to": "null" 426 | } 427 | ], 428 | "sparkline": { 429 | "fillColor": "rgba(31, 118, 189, 0.18)", 430 | "full": false, 431 | "lineColor": "rgb(31, 120, 193)", 432 | "show": false 433 | }, 434 | "tableColumn": "", 435 | "targets": [ 436 | { 437 | "$$hashKey": "object:187", 438 | "alias": "Ping", 439 | "groupBy": [ 440 | { 441 | "params": [ 442 | "1h" 443 | ], 444 | "type": "time" 445 | } 446 | ], 447 | "hide": false, 448 | "measurement": "speedtest", 449 | "orderByTime": "ASC", 450 | "policy": "default", 451 | "query": "SELECT mean(\"value\") FROM \"download\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", 452 | "rawQuery": false, 453 | "refId": "A", 454 | "resultFormat": "time_series", 455 | "select": [ 456 | [ 457 | { 458 | "params": [ 459 | "ping" 460 | ], 461 | "type": "field" 462 | }, 463 | { 464 | "params": [], 465 | "type": "mean" 466 | } 467 | ] 468 | ], 469 | "tags": [ 470 | { 471 | "key": "host", 472 | "operator": "=", 473 | "value": "local" 474 | } 475 | ] 476 | } 477 | ], 478 | "thresholds": "", 479 | "timeFrom": "1w", 480 | "timeShift": null, 481 | "title": "Average ping latency / week", 482 | "type": "singlestat", 483 | "valueFontSize": "150%", 484 | "valueMaps": [ 485 | { 486 | "$$hashKey": "object:282", 487 | "op": "=", 488 | "text": "N/A", 489 | "value": "null" 490 | } 491 | ], 492 | "valueName": "avg" 493 | }, 494 | { 495 | "aliasColors": {}, 496 | "bars": false, 497 | "dashLength": 10, 498 | "dashes": false, 499 | "datasource": "InfluxDB", 500 | "fieldConfig": { 501 | "defaults": { 502 | "custom": {} 503 | }, 504 | "overrides": [] 505 | }, 506 | "fill": 1, 507 | "fillGradient": 0, 508 | "gridPos": { 509 | "h": 8, 510 | "w": 24, 511 | "x": 0, 512 | "y": 8 513 | }, 514 | "hiddenSeries": false, 515 | "id": 4, 516 | "legend": { 517 | "alignAsTable": false, 518 | "avg": false, 519 | "current": true, 520 | "max": false, 521 | "min": false, 522 | "show": true, 523 | "total": false, 524 | "values": true 525 | }, 526 | "lines": true, 527 | "linewidth": 2, 528 | "links": [], 529 | "nullPointMode": "connected", 530 | "options": { 531 | "alertThreshold": true 532 | }, 533 | "percentage": false, 534 | "pluginVersion": "7.3.7", 535 | "pointradius": 5, 536 | "points": false, 537 | "renderer": "flot", 538 | "seriesOverrides": [], 539 | "spaceLength": 10, 540 | "stack": false, 541 | "steppedLine": false, 542 | "targets": [ 543 | { 544 | "$$hashKey": "object:615", 545 | "alias": "Download", 546 | "groupBy": [ 547 | { 548 | "params": [ 549 | "$__interval" 550 | ], 551 | "type": "time" 552 | }, 553 | { 554 | "params": [ 555 | "null" 556 | ], 557 | "type": "fill" 558 | } 559 | ], 560 | "measurement": "speedtest", 561 | "orderByTime": "ASC", 562 | "policy": "default", 563 | "refId": "C", 564 | "resultFormat": "time_series", 565 | "select": [ 566 | [ 567 | { 568 | "params": [ 569 | "download_mbs" 570 | ], 571 | "type": "field" 572 | }, 573 | { 574 | "params": [], 575 | "type": "mean" 576 | } 577 | ] 578 | ], 579 | "tags": [ 580 | { 581 | "key": "host", 582 | "operator": "=", 583 | "value": "local" 584 | } 585 | ] 586 | }, 587 | { 588 | "$$hashKey": "object:648", 589 | "alias": "Upload", 590 | "groupBy": [ 591 | { 592 | "params": [ 593 | "$__interval" 594 | ], 595 | "type": "time" 596 | }, 597 | { 598 | "params": [ 599 | "null" 600 | ], 601 | "type": "fill" 602 | } 603 | ], 604 | "measurement": "speedtest", 605 | "orderByTime": "ASC", 606 | "policy": "default", 607 | "refId": "A", 608 | "resultFormat": "time_series", 609 | "select": [ 610 | [ 611 | { 612 | "params": [ 613 | "upload_mbs" 614 | ], 615 | "type": "field" 616 | }, 617 | { 618 | "params": [], 619 | "type": "mean" 620 | } 621 | ] 622 | ], 623 | "tags": [ 624 | { 625 | "key": "host", 626 | "operator": "=", 627 | "value": "local" 628 | } 629 | ] 630 | } 631 | ], 632 | "thresholds": [], 633 | "timeFrom": null, 634 | "timeRegions": [], 635 | "timeShift": null, 636 | "title": "Upload / Download", 637 | "tooltip": { 638 | "shared": true, 639 | "sort": 0, 640 | "value_type": "individual" 641 | }, 642 | "type": "graph", 643 | "xaxis": { 644 | "buckets": null, 645 | "mode": "time", 646 | "name": null, 647 | "show": true, 648 | "values": [] 649 | }, 650 | "yaxes": [ 651 | { 652 | "$$hashKey": "object:687", 653 | "format": "Mbits", 654 | "label": null, 655 | "logBase": 1, 656 | "max": null, 657 | "min": null, 658 | "show": true 659 | }, 660 | { 661 | "$$hashKey": "object:688", 662 | "format": "Mbits", 663 | "label": null, 664 | "logBase": 1, 665 | "max": null, 666 | "min": null, 667 | "show": false 668 | } 669 | ], 670 | "yaxis": { 671 | "align": false, 672 | "alignLevel": null 673 | } 674 | }, 675 | { 676 | "aliasColors": {}, 677 | "bars": false, 678 | "dashLength": 10, 679 | "dashes": false, 680 | "datasource": "InfluxDB", 681 | "fieldConfig": { 682 | "defaults": { 683 | "custom": {} 684 | }, 685 | "overrides": [] 686 | }, 687 | "fill": 1, 688 | "fillGradient": 0, 689 | "gridPos": { 690 | "h": 8, 691 | "w": 24, 692 | "x": 0, 693 | "y": 16 694 | }, 695 | "hiddenSeries": false, 696 | "id": 2, 697 | "legend": { 698 | "alignAsTable": false, 699 | "avg": false, 700 | "current": false, 701 | "max": false, 702 | "min": false, 703 | "show": true, 704 | "total": false, 705 | "values": false 706 | }, 707 | "lines": true, 708 | "linewidth": 2, 709 | "links": [], 710 | "nullPointMode": "connected", 711 | "options": { 712 | "alertThreshold": true 713 | }, 714 | "percentage": false, 715 | "pluginVersion": "7.3.7", 716 | "pointradius": 5, 717 | "points": false, 718 | "renderer": "flot", 719 | "seriesOverrides": [], 720 | "spaceLength": 10, 721 | "stack": false, 722 | "steppedLine": false, 723 | "targets": [ 724 | { 725 | "$$hashKey": "object:804", 726 | "alias": "Ping", 727 | "groupBy": [ 728 | { 729 | "params": [ 730 | "$__interval" 731 | ], 732 | "type": "time" 733 | }, 734 | { 735 | "params": [ 736 | "null" 737 | ], 738 | "type": "fill" 739 | } 740 | ], 741 | "measurement": "speedtest", 742 | "orderByTime": "ASC", 743 | "policy": "default", 744 | "refId": "B", 745 | "resultFormat": "time_series", 746 | "select": [ 747 | [ 748 | { 749 | "params": [ 750 | "ping" 751 | ], 752 | "type": "field" 753 | }, 754 | { 755 | "params": [], 756 | "type": "mean" 757 | } 758 | ] 759 | ], 760 | "tags": [ 761 | { 762 | "key": "host", 763 | "operator": "=", 764 | "value": "local" 765 | } 766 | ] 767 | } 768 | ], 769 | "thresholds": [], 770 | "timeFrom": null, 771 | "timeRegions": [], 772 | "timeShift": null, 773 | "title": "Ping", 774 | "tooltip": { 775 | "shared": true, 776 | "sort": 0, 777 | "value_type": "individual" 778 | }, 779 | "type": "graph", 780 | "xaxis": { 781 | "buckets": null, 782 | "mode": "time", 783 | "name": null, 784 | "show": true, 785 | "values": [] 786 | }, 787 | "yaxes": [ 788 | { 789 | "$$hashKey": "object:318", 790 | "format": "ms", 791 | "label": null, 792 | "logBase": 1, 793 | "max": null, 794 | "min": null, 795 | "show": true 796 | }, 797 | { 798 | "$$hashKey": "object:319", 799 | "format": "Mbits", 800 | "label": null, 801 | "logBase": 1, 802 | "max": null, 803 | "min": null, 804 | "show": false 805 | } 806 | ], 807 | "yaxis": { 808 | "align": false, 809 | "alignLevel": null 810 | } 811 | }, 812 | { 813 | "columns": [], 814 | "datasource": "InfluxDB", 815 | "fieldConfig": { 816 | "defaults": { 817 | "custom": {} 818 | }, 819 | "overrides": [] 820 | }, 821 | "fontSize": "100%", 822 | "gridPos": { 823 | "h": 8, 824 | "w": 24, 825 | "x": 0, 826 | "y": 24 827 | }, 828 | "id": 14, 829 | "links": [], 830 | "pageSize": null, 831 | "scroll": true, 832 | "showHeader": true, 833 | "sort": { 834 | "col": null, 835 | "desc": false 836 | }, 837 | "styles": [ 838 | { 839 | "$$hashKey": "object:299", 840 | "alias": "", 841 | "align": "auto", 842 | "colorMode": null, 843 | "colors": [ 844 | "rgba(245, 54, 54, 0.9)", 845 | "rgba(237, 129, 40, 0.89)", 846 | "rgba(50, 172, 45, 0.97)" 847 | ], 848 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 849 | "decimals": 2, 850 | "mappingType": 1, 851 | "pattern": "Distance", 852 | "thresholds": [], 853 | "type": "number", 854 | "unit": "lengthkm" 855 | }, 856 | { 857 | "$$hashKey": "object:310", 858 | "alias": "", 859 | "align": "auto", 860 | "colorMode": null, 861 | "colors": [ 862 | "rgba(245, 54, 54, 0.9)", 863 | "rgba(237, 129, 40, 0.89)", 864 | "rgba(50, 172, 45, 0.97)" 865 | ], 866 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 867 | "decimals": 2, 868 | "mappingType": 1, 869 | "pattern": "Download", 870 | "thresholds": [], 871 | "type": "number", 872 | "unit": "Mbits" 873 | }, 874 | { 875 | "$$hashKey": "object:321", 876 | "alias": "", 877 | "align": "auto", 878 | "colorMode": null, 879 | "colors": [ 880 | "rgba(245, 54, 54, 0.9)", 881 | "rgba(237, 129, 40, 0.89)", 882 | "rgba(50, 172, 45, 0.97)" 883 | ], 884 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 885 | "decimals": 2, 886 | "mappingType": 1, 887 | "pattern": "Ping", 888 | "thresholds": [], 889 | "type": "number", 890 | "unit": "ms" 891 | }, 892 | { 893 | "$$hashKey": "object:332", 894 | "alias": "", 895 | "align": "auto", 896 | "colorMode": null, 897 | "colors": [ 898 | "rgba(245, 54, 54, 0.9)", 899 | "rgba(237, 129, 40, 0.89)", 900 | "rgba(50, 172, 45, 0.97)" 901 | ], 902 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 903 | "decimals": 2, 904 | "mappingType": 1, 905 | "pattern": "Time", 906 | "thresholds": [], 907 | "type": "date", 908 | "unit": "short" 909 | }, 910 | { 911 | "$$hashKey": "object:1500", 912 | "alias": "", 913 | "align": "auto", 914 | "colorMode": null, 915 | "colors": [ 916 | "rgba(245, 54, 54, 0.9)", 917 | "rgba(237, 129, 40, 0.89)", 918 | "rgba(50, 172, 45, 0.97)" 919 | ], 920 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 921 | "decimals": 2, 922 | "mappingType": 1, 923 | "pattern": "Upload", 924 | "thresholds": [], 925 | "type": "number", 926 | "unit": "Mbits" 927 | } 928 | ], 929 | "targets": [ 930 | { 931 | "$$hashKey": "object:269", 932 | "groupBy": [ 933 | { 934 | "params": [ 935 | "$__interval" 936 | ], 937 | "type": "time" 938 | }, 939 | { 940 | "params": [ 941 | "none" 942 | ], 943 | "type": "fill" 944 | } 945 | ], 946 | "measurement": "speedtest", 947 | "orderByTime": "ASC", 948 | "policy": "default", 949 | "query": "SELECT last(\"serverid\") AS \"Server ID\", last(\"location\") AS \"Location\", last(\"distance\") AS \"Distance\",last(\"ping\") AS \"Ping\", last(\"download_mbs\") AS \"Download\", last(\"upload_mbs\") AS \"Upload\" FROM \"speedtest\" WHERE (\"host\" = 'local') AND $timeFilter GROUP BY time($__interval) fill(null)", 950 | "rawQuery": false, 951 | "refId": "A", 952 | "resultFormat": "table", 953 | "select": [ 954 | [ 955 | { 956 | "params": [ 957 | "location" 958 | ], 959 | "type": "field" 960 | }, 961 | { 962 | "params": [], 963 | "type": "last" 964 | }, 965 | { 966 | "params": [ 967 | "Location" 968 | ], 969 | "type": "alias" 970 | } 971 | ], 972 | [ 973 | { 974 | "params": [ 975 | "distance" 976 | ], 977 | "type": "field" 978 | }, 979 | { 980 | "params": [], 981 | "type": "last" 982 | }, 983 | { 984 | "params": [ 985 | "Distance" 986 | ], 987 | "type": "alias" 988 | } 989 | ], 990 | [ 991 | { 992 | "params": [ 993 | "serverid" 994 | ], 995 | "type": "field" 996 | }, 997 | { 998 | "params": [], 999 | "type": "last" 1000 | }, 1001 | { 1002 | "params": [ 1003 | "Server" 1004 | ], 1005 | "type": "alias" 1006 | } 1007 | ], 1008 | [ 1009 | { 1010 | "params": [ 1011 | "ping" 1012 | ], 1013 | "type": "field" 1014 | }, 1015 | { 1016 | "params": [], 1017 | "type": "last" 1018 | }, 1019 | { 1020 | "params": [ 1021 | "Ping" 1022 | ], 1023 | "type": "alias" 1024 | } 1025 | ], 1026 | [ 1027 | { 1028 | "params": [ 1029 | "download_mbs" 1030 | ], 1031 | "type": "field" 1032 | }, 1033 | { 1034 | "params": [], 1035 | "type": "last" 1036 | }, 1037 | { 1038 | "params": [ 1039 | "Download" 1040 | ], 1041 | "type": "alias" 1042 | } 1043 | ], 1044 | [ 1045 | { 1046 | "params": [ 1047 | "upload_mbs" 1048 | ], 1049 | "type": "field" 1050 | }, 1051 | { 1052 | "params": [], 1053 | "type": "last" 1054 | }, 1055 | { 1056 | "params": [ 1057 | "Upload" 1058 | ], 1059 | "type": "alias" 1060 | } 1061 | ] 1062 | ], 1063 | "tags": [ 1064 | { 1065 | "key": "host", 1066 | "operator": "=", 1067 | "value": "local" 1068 | } 1069 | ] 1070 | } 1071 | ], 1072 | "title": "All Requests", 1073 | "transform": "table", 1074 | "type": "table-old" 1075 | } 1076 | ], 1077 | "refresh": false, 1078 | "schemaVersion": 26, 1079 | "style": "dark", 1080 | "tags": [], 1081 | "templating": { 1082 | "list": [] 1083 | }, 1084 | "time": { 1085 | "from": "now-7d", 1086 | "to": "now" 1087 | }, 1088 | "timepicker": { 1089 | "refresh_intervals": [ 1090 | "5s", 1091 | "10s", 1092 | "30s", 1093 | "1m", 1094 | "5m", 1095 | "15m", 1096 | "30m", 1097 | "1h", 1098 | "2h", 1099 | "1d" 1100 | ], 1101 | "time_options": [ 1102 | "5m", 1103 | "15m", 1104 | "1h", 1105 | "6h", 1106 | "12h", 1107 | "24h", 1108 | "2d", 1109 | "7d", 1110 | "30d" 1111 | ] 1112 | }, 1113 | "timezone": "", 1114 | "title": "Speedtest results", 1115 | "uid": "0A6hxROiz", 1116 | "version": 1 1117 | } -------------------------------------------------------------------------------- /docker/grafana/provisioning/datasources/influxdb.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: InfluxDB 5 | 6 | datasources: 7 | - name: InfluxDB 8 | type: influxdb 9 | access: proxy 10 | database: speedtest 11 | user: admin 12 | password: password 13 | url: http://influxdb:8086 14 | -------------------------------------------------------------------------------- /images/speedtest_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuadStingray/docker-speedtest-influxdb/58406e06304fbf45088a124d8a4589f41e5c8123/images/speedtest_dashboard.png -------------------------------------------------------------------------------- /model/comandline.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | ) 7 | 8 | func Parser() Settings { 9 | var interval int 10 | var retryInterval int 11 | 12 | var server string 13 | var host string 14 | var influxHost string 15 | var influxDB string 16 | var influxPwd string 17 | var influxUser string 18 | 19 | var list bool 20 | var keepProcessRunning bool 21 | var showExternalIp bool 22 | var saveToInfluxDb bool 23 | var distanceUnit string 24 | var includeHumanOutput bool 25 | var retryZeroValue bool 26 | 27 | flag.IntVar(&interval, "interval", 3600, "seconds between statistics import") 28 | flag.IntVar(&retryInterval, "retryInterval", 300, "seconds between statistics retry") 29 | 30 | flag.StringVar(&host, "host", "", "host where the speedetest is running") 31 | flag.StringVar(&influxHost, "influxHost", "http://influxdb:8086", "host of your influxdb instance") 32 | flag.StringVar(&influxDB, "influxDB", "speetest", "influxdb database") 33 | flag.StringVar(&influxUser, "influxUser", "DEFAULT", "influxdb Username") 34 | flag.StringVar(&influxPwd, "influxPwd", "DEFAULT", "influxdb Password") 35 | flag.StringVar(&distanceUnit, "distanceUnit", "K", "Distance Unit between GeoPoints possible Values K|M|N") 36 | 37 | flag.BoolVar(&includeHumanOutput, "includeHumanOutput", true, "Log HumanReadableOutput to Console") 38 | flag.BoolVar(&saveToInfluxDb, "saveToInfluxDb", false, "save to influxdb") 39 | flag.BoolVar(&keepProcessRunning, "keepProcessRunning", false, "keep process running") 40 | flag.BoolVar(&showExternalIp, "showExternalIp", true, "save and show external Ip of docker host") 41 | flag.BoolVar(&retryZeroValue, "retryZeroValue", false, "retry speedtest at zero values returned") 42 | 43 | flag.StringVar(&server, "server", "", "ndt7 server") 44 | flag.BoolVar(&list, "list", false, "list servers") 45 | 46 | flag.Parse() 47 | 48 | log.Println("**************************************************************") 49 | log.Println("******** Parser started with following commands **************") 50 | log.Printf("** interval %v", interval) 51 | log.Println("** Distance Unit " + distanceUnit) 52 | log.Println("** Host " + host) 53 | 54 | if showExternalIp { 55 | log.Println("** showExternalIp: true") 56 | } else { 57 | 58 | log.Println("** showExternalIp: false") 59 | } 60 | 61 | if saveToInfluxDb { 62 | log.Println("** influxHost " + influxHost) 63 | log.Println("** influxDB " + influxDB) 64 | log.Println("** influxUser " + influxUser) 65 | 66 | if influxPwd == "DEFAULT" { 67 | log.Println("** influxPwd DEFAULT") 68 | } else { 69 | log.Println("** influxPwd is not Default") 70 | } 71 | } 72 | 73 | log.Println("**************************************************************") 74 | log.Println("**************************************************************") 75 | return Settings{interval, host, server, distanceUnit, list, keepProcessRunning, showExternalIp, includeHumanOutput, InfluxDbSettings{saveToInfluxDb, influxHost, influxUser, influxPwd, influxDB}, retryZeroValue, retryInterval} 76 | } 77 | -------------------------------------------------------------------------------- /model/geoip.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "math" 8 | "net/http" 9 | ) 10 | 11 | type GeoIP struct { 12 | // The right side is the name of the JSON variable 13 | Ip string `json:"ip"` 14 | Country string `json:"country"` 15 | CountryName string `json:"country_name"` 16 | RegionCode string `json:"region_code"` 17 | Region string `json:"region"` 18 | City string `json:"city"` 19 | Postal string `json:"postal"` 20 | Lat float64 `json:"latitude"` 21 | Lon float64 `json:"longitude"` 22 | ContinentCode string `json:"continent_code"` 23 | InEu bool `json:"in_eu"` 24 | Timezone string `json:"timezone"` 25 | UtcOffset string `json:"utc_offset"` 26 | CountryCallingCode string `json:"country_calling_code"` 27 | Currency string `json:"currency"` 28 | Languages string `json:"languages"` 29 | Asn string `json:"asn"` 30 | Org string `json:"org"` 31 | } 32 | 33 | func CheckIpLocation(ip string) (GeoIP, error) { 34 | 35 | var ( 36 | err error 37 | geo GeoIP 38 | response *http.Response 39 | body []byte 40 | ) 41 | 42 | response, err = http.Get("https://ipapi.co/" + ip + "/json/") 43 | if err != nil { 44 | fmt.Println(err) 45 | } else { 46 | defer response.Body.Close() 47 | body, err = ioutil.ReadAll(response.Body) 48 | if err != nil { 49 | fmt.Println(err) 50 | } else { 51 | err = json.Unmarshal(body, &geo) 52 | if err != nil { 53 | fmt.Println(err) 54 | } 55 | } 56 | } 57 | 58 | return geo, err 59 | } 60 | 61 | func LocateUser() (GeoIP, error) { 62 | 63 | var ( 64 | err error 65 | geo GeoIP 66 | response *http.Response 67 | body []byte 68 | ) 69 | 70 | response, err = http.Get("https://ipapi.co/json/") 71 | if err != nil { 72 | fmt.Println(err) 73 | } else { 74 | defer response.Body.Close() 75 | body, err = ioutil.ReadAll(response.Body) 76 | if err != nil { 77 | fmt.Println(err) 78 | } else { 79 | err = json.Unmarshal(body, &geo) 80 | if err != nil { 81 | fmt.Println(err) 82 | } 83 | } 84 | } 85 | 86 | // Everything accessible in struct now 87 | fmt.Println("\n==== IP Geolocation Info ====\n") 88 | fmt.Println("IP address:\t", geo.Ip) 89 | fmt.Println("Country Code:\t", geo.CountryName) 90 | fmt.Println("Country Name:\t", geo.CountryName) 91 | fmt.Println("Zip Code:\t", geo.Postal) 92 | fmt.Println("Latitude:\t", geo.Lat) 93 | fmt.Println("Longitude:\t", geo.Lon) 94 | fmt.Println("Metro Code:\t", geo.City) 95 | 96 | return geo, err 97 | } 98 | 99 | //func DistanceBetweenUserAndIp(ip string, unit string) (float64, error) { 100 | // geoClient, _ := LocateUser() 101 | // time.Sleep(time.Duration(1) * time.Second) 102 | // geoSever, _ := CheckIpLocation(ip) 103 | // return Distance(geoSever.Lat, geoClient.Lat, geoSever.Lon, geoClient.Lon, unit), nil 104 | //} 105 | // 106 | //func DistanceBetweenIps(ip1 string, ip2 string, unit string) (float64, error) { 107 | // geoClient, _ := CheckIpLocation(ip1) 108 | // time.Sleep(time.Duration(1) * time.Second) 109 | // geoSever, _ := CheckIpLocation(ip2) 110 | // return Distance(geoSever.Lat, geoClient.Lat, geoSever.Lon, geoClient.Lon, unit), nil 111 | //} 112 | // 113 | //func DistanceBetweenIpAndGeoIp(ip string, geoIp GeoIP, unit string) (float64, error) { 114 | // time.Sleep(time.Duration(1) * time.Second) 115 | // geoSever, _ := CheckIpLocation(ip) 116 | // return Distance(geoSever.Lat, geoIp.Lat, geoSever.Lon, geoIp.Lon, unit), nil 117 | //} 118 | 119 | func Distance(lat1 float64, lng1 float64, lat2 float64, lng2 float64, unit string) float64 { 120 | const PI float64 = 3.141592653589793 121 | 122 | radlat1 := float64(PI * lat1 / 180) 123 | radlat2 := float64(PI * lat2 / 180) 124 | 125 | theta := float64(lng1 - lng2) 126 | radtheta := float64(PI * theta / 180) 127 | 128 | dist := math.Sin(radlat1)*math.Sin(radlat2) + math.Cos(radlat1)*math.Cos(radlat2)*math.Cos(radtheta) 129 | 130 | if dist > 1 { 131 | dist = 1 132 | } 133 | 134 | dist = math.Acos(dist) 135 | dist = dist * 180 / PI 136 | dist = dist * 60 * 1.1515 137 | 138 | if unit == "K" { 139 | dist = dist * 1.609344 140 | } else if unit == "N" { 141 | dist = dist * 0.8684 142 | } 143 | 144 | return dist 145 | } 146 | -------------------------------------------------------------------------------- /model/influxdb.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/influxdata/influxdb/client/v2" 5 | "log" 6 | "time" 7 | ) 8 | 9 | func SaveToInfluxDb(statistics SpeedTestStatistics, settings Settings) { 10 | c, err := client.NewHTTPClient(client.HTTPConfig{ 11 | Addr: settings.InfluxDbSettings.Db_Url, 12 | Username: settings.InfluxDbSettings.Username, 13 | Password: settings.InfluxDbSettings.Password, 14 | }) 15 | if err != nil { 16 | log.Printf("error creating http client: %v", err) 17 | } 18 | 19 | // Create a new point batch 20 | bp, err := client.NewBatchPoints(client.BatchPointsConfig{ 21 | Database: settings.InfluxDbSettings.Db, 22 | Precision: "s", 23 | }) 24 | if err != nil { 25 | log.Printf("error new batch point: %v", err) 26 | } 27 | 28 | // Create a point and add to batch 29 | tags := map[string]string{"host": settings.Host} 30 | 31 | fields := map[string]interface{}{ 32 | "download_mbs": statistics.Down_Mbs, 33 | "upload_mbs": statistics.Up_Mbs, 34 | "ping": statistics.Ping, 35 | "distance": statistics.Server.Distance, 36 | "serverid": statistics.Server.Name, 37 | "location": statistics.Server.City + ", " + statistics.Server.Country, 38 | "clientProvider": statistics.Client.Provider, 39 | } 40 | 41 | if settings.ShowMyIp { 42 | fields = map[string]interface{}{ 43 | "download_mbs": statistics.Down_Mbs, 44 | "upload_mbs": statistics.Up_Mbs, 45 | "ping": statistics.Ping, 46 | "distance": statistics.Server.Distance, 47 | "serverid": statistics.Server.Name, 48 | "location": statistics.Server.City + ", " + statistics.Server.Country, 49 | "external_ip": statistics.Client.ExternalIp, 50 | "clientProvider": statistics.Client.Provider, 51 | } 52 | } 53 | 54 | pt, err := client.NewPoint("speedtest", tags, fields, time.Now()) 55 | if err != nil { 56 | log.Printf("error creating messure point: %v", err) 57 | } 58 | bp.AddPoint(pt) 59 | 60 | err = c.Write(bp) 61 | if err != nil { 62 | log.Printf("could not write to influx Db. check connection to %v and Db %s with user %v with pwd %s (error: %s)", settings.InfluxDbSettings.Db_Url, settings.InfluxDbSettings.Db, settings.InfluxDbSettings.Username, settings.InfluxDbSettings.Password, err) 63 | time.Sleep(time.Duration(10) * time.Second) 64 | SaveToInfluxDb(statistics, settings) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type InfluxDbSettings struct { 4 | Use_Influx bool 5 | Db_Url string 6 | Username string 7 | Password string 8 | Db string 9 | } 10 | 11 | type Settings struct { 12 | Interval int 13 | Host string 14 | Server string 15 | DistanceUnit string 16 | ListServers bool 17 | KeepProcessRunning bool 18 | ShowMyIp bool 19 | IncludeHumanReadable bool 20 | InfluxDbSettings InfluxDbSettings 21 | RetryZeroValue bool 22 | RetryInterval int 23 | } 24 | 25 | type ClientInformations struct { 26 | ExternalIp string 27 | Provider string 28 | Coordinate Coordinate 29 | } 30 | 31 | type SpeedTestStatistics struct { 32 | Client ClientInformations 33 | Server Server 34 | Ping float64 35 | Down_Mbs float64 36 | Up_Mbs float64 37 | DownRetransPercent float64 38 | } 39 | 40 | type Server struct { 41 | URL string 42 | Lat float64 43 | Lon float64 44 | Name string 45 | Country string 46 | City string 47 | Distance float64 48 | Latency float64 49 | } 50 | 51 | type Coordinate struct { 52 | Lat float64 53 | Lon float64 54 | } 55 | -------------------------------------------------------------------------------- /model/speedtest/output_humanreadable.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/m-lab/ndt7-client-go/spec" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // HumanReadable is a human readable emitter. It emits the events generated 12 | // by running a ndt7 test as pleasant stdout messages. 13 | type HumanReadable struct { 14 | out io.Writer 15 | } 16 | 17 | // NewHumanReadable returns a new human readable emitter. 18 | func NewHumanReadable() OutputType { 19 | return HumanReadable{os.Stdout} 20 | } 21 | 22 | // NewHumanReadableWithWriter returns a new human readable emitter using the 23 | // specified writer. 24 | func NewHumanReadableWithWriter(w io.Writer) OutputType { 25 | return HumanReadable{w} 26 | } 27 | 28 | // OnStarting handles the start event 29 | func (h HumanReadable) OnStarting(test spec.TestKind) error { 30 | _, err := fmt.Fprintf(h.out, "\rstarting %s", test) 31 | return err 32 | } 33 | 34 | // OnError handles the error event 35 | func (h HumanReadable) OnError(test spec.TestKind, err error) error { 36 | _, failure := fmt.Fprintf(h.out, "\r%s failed: %s\n", test, err.Error()) 37 | return failure 38 | } 39 | 40 | // OnConnected handles the connected event 41 | func (h HumanReadable) OnConnected(test spec.TestKind, fqdn string) error { 42 | _, err := fmt.Fprintf(h.out, "\r%s in progress with %s\n", test, fqdn) 43 | return err 44 | } 45 | 46 | // OnDownloadEvent handles an event emitted by the download test 47 | func (h HumanReadable) OnDownloadEvent(m *spec.Measurement) error { 48 | return h.onSpeedEvent(m) 49 | } 50 | 51 | // OnUploadEvent handles an event emitted during the upload test 52 | func (h HumanReadable) OnUploadEvent(m *spec.Measurement) error { 53 | return h.onSpeedEvent(m) 54 | } 55 | 56 | func (h HumanReadable) onSpeedEvent(m *spec.Measurement) error { 57 | // The specification recommends that we show application level 58 | // measurements. Let's just do that in interactive mode. To this 59 | // end, we ignore any measurement coming from the server. 60 | if m.Origin != spec.OriginClient { 61 | return nil 62 | } 63 | if m.AppInfo == nil || m.AppInfo.ElapsedTime <= 0 { 64 | return errors.New("Missing m.AppInfo or invalid m.AppInfo.ElapsedTime") 65 | } 66 | elapsed := float64(m.AppInfo.ElapsedTime) / 1e06 67 | v := (8.0 * float64(m.AppInfo.NumBytes)) / elapsed / (1000.0 * 1000.0) 68 | _, err := fmt.Fprintf(h.out, "\rAvg. speed : %7.1f Mbit/s", v) 69 | return err 70 | } 71 | 72 | // OnComplete handles the complete event 73 | func (h HumanReadable) OnComplete(test spec.TestKind) error { 74 | _, err := fmt.Fprintf(h.out, "\n%s: complete\n", test) 75 | return err 76 | } 77 | 78 | // OnSummary handles the summary event. 79 | func (h HumanReadable) OnSummary(s *Summary) error { 80 | const summaryFormat = `%15s: %s 81 | %15s: %s 82 | %15s: %7.1f %s 83 | %15s: %7.1f %s 84 | %15s: %7.1f %s 85 | %15s: %7.2f %s 86 | ` 87 | _, err := fmt.Fprintf(h.out, summaryFormat, 88 | "Server", s.ServerFQDN, 89 | "Client", s.ClientIP, 90 | "Latency", s.MinRTT.Value, s.MinRTT.Unit, 91 | "Download", s.Download.Value, s.Upload.Unit, 92 | "Upload", s.Upload.Value, s.Upload.Unit, 93 | "Retransmission", s.DownloadRetrans.Value, s.DownloadRetrans.Unit) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /model/speedtest/output_interface.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "github.com/m-lab/ndt7-client-go/spec" 5 | ) 6 | 7 | // OutputType is a generic OutputType. When an event occurs, the 8 | // corresponding method will be called. An error will generally 9 | // mean that it's not possible to write the output. A common 10 | // case where this happen is where the output is redirected to 11 | // a file on a full hard disk. 12 | // 13 | // See the documentation of the main package for more details 14 | // on the sequence in which events may occur. 15 | type OutputType interface { 16 | // OnStarting is emitted before attempting to start a test. 17 | OnStarting(test spec.TestKind) error 18 | 19 | // OnError is emitted if a test cannot start. 20 | OnError(test spec.TestKind, err error) error 21 | 22 | // OnConnected is emitted when we connected to the speedtest server. 23 | OnConnected(test spec.TestKind, fqdn string) error 24 | 25 | // OnDownloadEvent is emitted during the download. 26 | OnDownloadEvent(m *spec.Measurement) error 27 | 28 | // OnUploadEvent is emitted during the upload. 29 | OnUploadEvent(m *spec.Measurement) error 30 | 31 | // OnComplete is always emitted when the test is over. 32 | OnComplete(test spec.TestKind) error 33 | 34 | // OnSummary is emitted after the test is over. 35 | OnSummary(s *Summary) error 36 | } 37 | -------------------------------------------------------------------------------- /model/speedtest/output_silent.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "github.com/m-lab/ndt7-client-go/spec" 5 | ) 6 | 7 | type SilentOutput struct { 8 | } 9 | 10 | // OnStarting handles the start event 11 | func (h SilentOutput) OnStarting(test spec.TestKind) error { 12 | return nil 13 | } 14 | 15 | // OnError handles the error event 16 | func (h SilentOutput) OnError(test spec.TestKind, err error) error { 17 | return nil 18 | } 19 | 20 | // OnConnected handles the connected event 21 | func (h SilentOutput) OnConnected(test spec.TestKind, fqdn string) error { 22 | return nil 23 | } 24 | 25 | // OnDownloadEvent handles an event emitted by the download test 26 | func (h SilentOutput) OnDownloadEvent(m *spec.Measurement) error { 27 | return h.onSpeedEvent(m) 28 | } 29 | 30 | // OnUploadEvent handles an event emitted during the upload test 31 | func (h SilentOutput) OnUploadEvent(m *spec.Measurement) error { 32 | return h.onSpeedEvent(m) 33 | } 34 | 35 | func (h SilentOutput) onSpeedEvent(m *spec.Measurement) error { 36 | return nil 37 | } 38 | 39 | // OnComplete handles the complete event 40 | func (h SilentOutput) OnComplete(test spec.TestKind) error { 41 | return nil 42 | } 43 | 44 | // OnSummary handles the summary event. 45 | func (h SilentOutput) OnSummary(s *Summary) error { 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /model/speedtest/runner.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "context" 5 | "github.com/m-lab/ndt7-client-go" 6 | "github.com/m-lab/ndt7-client-go/spec" 7 | ) 8 | 9 | type TestRunner struct { 10 | Client *ndt7.Client 11 | Output OutputType 12 | } 13 | 14 | func (r TestRunner) doRunTest( 15 | ctx context.Context, test spec.TestKind, 16 | start func(context.Context) (<-chan spec.Measurement, error), 17 | emitEvent func(m *spec.Measurement) error, 18 | ) int { 19 | ch, err := start(ctx) 20 | if err != nil { 21 | r.Output.OnError(test, err) 22 | return 1 23 | } 24 | err = r.Output.OnConnected(test, r.Client.FQDN) 25 | if err != nil { 26 | return 1 27 | } 28 | for ev := range ch { 29 | err = emitEvent(&ev) 30 | if err != nil { 31 | return 1 32 | } 33 | } 34 | return 0 35 | } 36 | 37 | func (r TestRunner) runTest( 38 | ctx context.Context, test spec.TestKind, 39 | start func(context.Context) (<-chan spec.Measurement, error), 40 | emitEvent func(m *spec.Measurement) error, 41 | ) int { 42 | err := r.Output.OnStarting(test) 43 | if err != nil { 44 | return 1 45 | } 46 | code := r.doRunTest(ctx, test, start, emitEvent) 47 | err = r.Output.OnComplete(test) 48 | if err != nil { 49 | return 1 50 | } 51 | return code 52 | } 53 | 54 | func (r TestRunner) RunDownload(ctx context.Context) int { 55 | return r.runTest(ctx, spec.TestDownload, r.Client.StartDownload, 56 | r.Output.OnDownloadEvent) 57 | } 58 | 59 | func (r TestRunner) RunUpload(ctx context.Context) int { 60 | return r.runTest(ctx, spec.TestUpload, r.Client.StartUpload, 61 | r.Output.OnUploadEvent) 62 | } 63 | -------------------------------------------------------------------------------- /model/speedtest/servers.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type SpeedTestServer struct { 12 | // The right side is the name of the JSON variable 13 | Country string `json:"country"` 14 | City string `json:"city"` 15 | Lat float64 `json:"latitude"` 16 | Lon float64 `json:"longitude"` 17 | Roundrobin bool `json:"roundrobin"` 18 | Site string `json:"site"` 19 | UplinkSpeed string `json:"uplink_speed"` 20 | } 21 | 22 | func ListServer() ([]SpeedTestServer, error) { 23 | var ( 24 | err error 25 | servers []SpeedTestServer 26 | response *http.Response 27 | body []byte 28 | ) 29 | 30 | response, err = http.Get("https://siteinfo.mlab-oti.measurementlab.net/v1/sites/locations.json") 31 | if err != nil { 32 | fmt.Println(err) 33 | } 34 | 35 | defer response.Body.Close() 36 | 37 | body, err = ioutil.ReadAll(response.Body) 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | 42 | err = json.Unmarshal(body, &servers) 43 | if err != nil { 44 | fmt.Println(err) 45 | } 46 | 47 | return servers, nil 48 | } 49 | 50 | func FindServerByFQDN(fqdn string) (SpeedTestServer, error) { 51 | serverList, _ := ListServer() 52 | for _, server := range serverList { 53 | if strings.Contains(fqdn, "."+server.Site+".") || strings.Contains(fqdn, "-"+server.Site+"-") || strings.Contains(fqdn, "-"+server.Site+".") { 54 | return server, nil 55 | } 56 | 57 | } 58 | serverList2, _ := ListServer() 59 | for _, server := range serverList2 { 60 | if strings.Contains(fqdn, "."+server.Site+".") || strings.Contains(fqdn, "-"+server.Site+"-") || strings.Contains(fqdn, "-"+server.Site+".") { 61 | return server, nil 62 | } 63 | 64 | } 65 | return SpeedTestServer{}, nil 66 | } 67 | -------------------------------------------------------------------------------- /model/speedtest/summary.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | // ValueUnitPair represents a {"Value": ..., "Unit": ...} pair. 4 | type ValueUnitPair struct { 5 | Value float64 6 | Unit string 7 | } 8 | 9 | // Summary is a struct containing the values displayed to the user at 10 | // the end of an speedtest test. 11 | type Summary struct { 12 | // ServerFQDN is the FQDN of the server used for this test. 13 | ServerFQDN string 14 | 15 | // ServerIP is the (v4 or v6) IP address of the server. 16 | ServerIP string 17 | 18 | // ClientIP is the (v4 or v6) IP address of the Client. 19 | ClientIP string 20 | 21 | // DownloadUUID is the UUID of the download test. 22 | DownloadUUID string 23 | 24 | // Download is the download speed, in Mbit/s. This is measured at the 25 | // receiver. 26 | Download ValueUnitPair 27 | 28 | // Upload is the upload speed, in Mbit/s. This is measured at the sender. 29 | Upload ValueUnitPair 30 | 31 | // DownloadRetrans is the retransmission rate. This is based on the TCPInfo 32 | // values provided by the server during a download test. 33 | DownloadRetrans ValueUnitPair 34 | 35 | // RTT is the round-trip time of the latest measurement, in milliseconds. 36 | // This is provided by the server during a download test. 37 | MinRTT ValueUnitPair 38 | } 39 | 40 | // NewSummary returns a new Summary struct for a given FQDN. 41 | func NewSummary(FQDN string) *Summary { 42 | return &Summary{ 43 | ServerFQDN: FQDN, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./speedtestInfluxDB -interval="$INTERVAL" \ 4 | -saveToInfluxDb="$INFLUXDB_USE" \ 5 | -influxHost="$INFLUXDB_URL" \ 6 | -influxDB="$INFLUXDB_DB" \ 7 | -influxUser="$INFLUXDB_USER" \ 8 | -influxPwd="$INFLUXDB_PWD" \ 9 | -host="$HOST" \ 10 | -server="$SPEEDTEST_SERVER" \ 11 | -list="$SPEEDTEST_LIST_SERVERS" \ 12 | -keepProcessRunning="$SPEEDTEST_LIST_KEEP_CONTAINER_RUNNING" \ 13 | -distanceUnit="$SPEEDTEST_DISTANCE_UNIT" \ 14 | -includeHumanOutput="$INCLUDE_READABLE_OUTPUT" \ 15 | -showExternalIp="$SHOW_EXTERNAL_IP" \ 16 | -retryInterval="$RETRY_INTERVAL" \ 17 | -retryZeroValue="$RETRY_ZERO_VALUE" --------------------------------------------------------------------------------