├── images
└── speedtest_dashboard.png
├── docker
└── grafana
│ └── provisioning
│ ├── dashboards
│ ├── dashboard.yml
│ └── speedtest.json
│ └── datasources
│ └── influxdb.yml
├── docker-speedtest.iml
├── .github
└── workflows
│ └── go.yml
├── run.sh
├── docker-compose.yml
├── .gitignore
├── model
├── model.go
├── speedtest
│ ├── output_silent.go
│ ├── output_interface.go
│ ├── summary.go
│ ├── runner.go
│ ├── servers.go
│ └── output_humanreadable.go
├── influxdb.go
├── comandline.go
└── geoip.go
├── Dockerfile
├── README.md
├── app.go
└── LICENSE
/images/speedtest_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuadStingray/docker-speedtest-influxdb/HEAD/images/speedtest_dashboard.png
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/docker-speedtest.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------