├── web-ui ├── test-data │ ├── 8081 │ ├── data-short │ └── data-long ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ ├── logo.png │ │ └── styles │ │ │ ├── app.css │ │ │ ├── radio.css │ │ │ └── table.css │ ├── main.js │ ├── constants.js │ ├── components │ │ ├── RadioCom.vue │ │ └── BarChart.vue │ └── App.vue ├── babel.config.js ├── jsconfig.json ├── build.sh ├── vue.config.js └── package.json ├── server ├── src │ ├── lib │ │ ├── __init__.py │ │ ├── respentity.py │ │ ├── utils.py │ │ └── database.py │ ├── test │ │ ├── flask │ │ │ ├── query.sh │ │ │ └── upload.sh │ │ ├── sqlite │ │ │ ├── delete_old_data.py │ │ │ ├── query_data.py │ │ │ └── insert_data.py │ │ └── utils │ │ │ └── load_config.py │ ├── build.source │ └── ping-charts-server.py └── requirements.txt ├── img ├── general.png ├── scroll.png └── tittle.png ├── scripts ├── release │ ├── pre-build.sh │ ├── build-client-py.sh │ ├── build-server-py.sh │ └── build-client-go.sh ├── client-update ├── server-update ├── server.sh └── client.sh ├── client ├── src │ ├── build.source │ └── ping-charts-client.py └── requirements.txt ├── updatelog.md ├── doc ├── templates │ ├── pingChartsClient.timer │ ├── pingChartsClient.service │ ├── pingChartsServer.service │ ├── client.yaml │ └── server.yaml ├── dev.md ├── configuration-zh.md ├── setup-zh.md ├── configuration.md └── setup.md ├── client-go ├── go.mod ├── go.sum └── client.go ├── .github └── workflows │ └── realeases.yaml └── README.md /web-ui/test-data/8081: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/src/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastarpen/ping-charts/HEAD/img/general.png -------------------------------------------------------------------------------- /img/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastarpen/ping-charts/HEAD/img/scroll.png -------------------------------------------------------------------------------- /img/tittle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastarpen/ping-charts/HEAD/img/tittle.png -------------------------------------------------------------------------------- /server/src/test/flask/query.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | curl http://localhost:8000/data/?min=1 5 | -------------------------------------------------------------------------------- /web-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastarpen/ping-charts/HEAD/web-ui/public/favicon.ico -------------------------------------------------------------------------------- /web-ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastarpen/ping-charts/HEAD/web-ui/src/assets/logo.png -------------------------------------------------------------------------------- /web-ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/release/pre-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # working directory should be scripts/release 4 | 5 | mkdir -p ../../dist 6 | -------------------------------------------------------------------------------- /web-ui/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /client/src/build.source: -------------------------------------------------------------------------------- 1 | pyinstaller -F ping-charts-client.py && rm build -r && mv ./dist/ping-charts-client ./ && rm ping-charts-client.spec && rmdir ./dist 2 | -------------------------------------------------------------------------------- /updatelog.md: -------------------------------------------------------------------------------- 1 | ## V1.3.0 2 | 3 | - WebUI enhancement; 4 | - Lazy load charts; 5 | - Change the precision of float data in the database; 6 | 7 | ## V1.3.1 8 | 9 | - Run DNS query before test; #7 10 | -------------------------------------------------------------------------------- /server/src/build.source: -------------------------------------------------------------------------------- 1 | pyinstaller -F --add-data 'templates:templates' --add-data 'static:static' ping-charts-server.py && rm -r ./build && mv ./dist/ping-charts-server ./ && rm ping-charts-server.spec && rmdir ./dist/ 2 | -------------------------------------------------------------------------------- /server/src/test/flask/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | curl -X POST -H "Content-Type: application/json" http://localhost:8000/upload -d '{ "serverId": 1, "targetId": 1, "delay": 121.25, "loss": 0.11, "time": "2022-01-11" }' 5 | -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2023.7.22 2 | charset-normalizer==3.2.0 3 | click==8.1.6 4 | idna==3.4 5 | PyYAML==6.0.1 6 | requests==2.31.0 7 | urllib3==2.0.4 8 | pyinstaller==5.13.0 9 | pyinstaller-hooks-contrib==2023.6 10 | -------------------------------------------------------------------------------- /doc/templates/pingChartsClient.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="Run Ping Charts client every minute" 3 | 4 | [Timer] 5 | OnCalendar=*-*-* *:*:00 6 | Unit=pingChartsClient.service 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /client-go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eastarpen/ping-charts/client-go 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | gopkg.in/yaml.v3 v3.0.1 8 | ) 9 | 10 | require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 11 | -------------------------------------------------------------------------------- /doc/templates/pingChartsClient.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Ping-Charts Client 3 | Documentation=https://github.com/eastarpen/ping-charts 4 | After=network.target 5 | 6 | [Service] 7 | WorkingDirectory=/opt/ping-charts 8 | ExecStart=/opt/ping-charts/ping-charts-client 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /doc/templates/pingChartsServer.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Ping-Charts Server 3 | Documentation=https://github.com/eastarpen/ping-charts 4 | After=network.target 5 | 6 | [Service] 7 | WorkingDirectory=/opt/ping-charts 8 | ExecStart=/opt/ping-charts/ping-charts-server 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /server/src/test/sqlite/delete_old_data.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | import sys 4 | 5 | path = "../../" # src directory 6 | sys.path.append(path) 7 | 8 | from lib import database as db 9 | 10 | 11 | if __name__ == "__main__": 12 | path = path + "data" 13 | db.init_db(path=path) 14 | db.delete_old_data() 15 | -------------------------------------------------------------------------------- /doc/templates/client.yaml: -------------------------------------------------------------------------------- 1 | name: client 2 | clientId: 1 3 | passw: password 4 | uploadUrl: url 5 | targets: 6 | - id: 1 7 | name: chinanet 8 | port: 80 9 | addr: ct.tz.cloudcpp.com 10 | - id: 2 11 | name: chinaunicom 12 | port: 80 13 | addr: cu.tz.cloudcpp.com 14 | - id: 3 15 | name: chinamobile 16 | port: 80 17 | addr: cm.tz.cloudcpp.com 18 | -------------------------------------------------------------------------------- /scripts/release/build-client-py.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # working directory should be scripts/release 5 | 6 | set -e 7 | 8 | cd ../../client/src/ || exit 9 | pip install -r ../requirements.txt 10 | 11 | 12 | # This will generate file './ping-charts-client' 13 | source build.source 14 | 15 | mv ping-charts-client "../../dist/ping-charts-client-python" 16 | -------------------------------------------------------------------------------- /server/src/test/utils/load_config.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import sys 4 | 5 | path = "../../" 6 | 7 | sys.path.append(path) 8 | 9 | from lib import utils 10 | 11 | 12 | 13 | if __name__ == "__main__": 14 | config_file = "../../data/config.yaml" 15 | targets, servers = utils.load_config(config_file) 16 | print(targets) 17 | print(servers) 18 | -------------------------------------------------------------------------------- /web-ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web-ui/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | if [ ! -d "../server/src/" ]; then 5 | echo "../server/src do not exists." 6 | exit 1 7 | fi 8 | 9 | npm install 10 | npm run build 11 | 12 | rm ../server/src/static/ -rf 13 | rm ../server/src/templates/ -rf 14 | mkdir ../server/src/templates 15 | mv ./dist/index.html ../server/src/templates/index.html 16 | mv ./dist/ ../server/src/static/ 17 | -------------------------------------------------------------------------------- /web-ui/src/constants.js: -------------------------------------------------------------------------------- 1 | export const judge_levels = { 2 | strict: 1, 3 | lenient: 2, 4 | much_lenient: 3 5 | } 6 | export const strict_level = { 7 | green: 0, 8 | yellow: 0.05, 9 | red: 1 10 | } 11 | 12 | export const lenient_level = { 13 | green: 0.05, 14 | yellow: 0.1, 15 | red: 1 16 | } 17 | 18 | export const much_lenient_level = { 19 | green: 0.1, 20 | yellow: 0.3, 21 | red: 1 22 | } 23 | 24 | export const max_ping_value = 350; 25 | -------------------------------------------------------------------------------- /server/src/test/sqlite/query_data.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | from datetime import datetime 4 | import sys 5 | 6 | lib_path = "../../" # src directory 7 | 8 | sys.path.append(lib_path) 9 | 10 | from lib import database as db 11 | 12 | if __name__ == "__main__": 13 | data_path = lib_path + 'data' 14 | db.init_db(path=data_path) 15 | timestamp = datetime(2023,1,1,0,0,0) 16 | 17 | res = db.query_entries(timestamp,1,1, ) 18 | print(res) 19 | -------------------------------------------------------------------------------- /web-ui/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | 3 | let devProxy = { 4 | proxy: { 5 | '^/data': { 6 | target: 'http://localhost:8081', 7 | changeOrigin: true, 8 | }, 9 | } 10 | } 11 | 12 | module.exports = defineConfig({ 13 | transpileDependencies: true, 14 | publicPath: process.env.dev === 'dev' ? '/' : './static', 15 | devServer: process.env.dev === 'dev' ? devProxy :{} 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/release/build-server-py.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # working directory should be scripts/release 5 | 6 | set -e 7 | 8 | WD="$(pwd)" 9 | 10 | # build static web files 11 | cd ../../web-ui/ 12 | ./build.sh 13 | 14 | cd "$WD" 15 | 16 | # build server 17 | # 18 | cd ../../server/src/ || exit 19 | pip install -r ../requirements.txt 20 | 21 | 22 | # This will generate file './ping-charts-server' 23 | source build.source 24 | 25 | mv ping-charts-server "../../dist/ping-charts-server-python" 26 | -------------------------------------------------------------------------------- /doc/templates/server.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | - name: chinanet 3 | id: 1 4 | alias: china-net 5 | - name: chinaunicom 6 | id: 2 7 | - name: chinamobile 8 | id: 3 9 | clients: 10 | - name: client 11 | label: US 12 | pass: password 13 | id: 1 14 | - name: clienta 15 | label: HK 16 | pass: password 17 | id: 2 18 | - name: clientb 19 | label: TW 20 | pass: password 21 | id: 3 22 | - name: clientd 23 | label: FR 24 | pass: password 25 | id: 4 26 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.3 2 | APScheduler==3.10.4 3 | backports.zoneinfo==0.2.1 4 | blinker==1.6.2 5 | click==8.1.6 6 | Flask==2.3.2 7 | Flask-APScheduler==1.12.4 8 | gunicorn==21.2.0 9 | importlib-metadata==6.8.0 10 | itsdangerous==2.1.2 11 | Jinja2==3.1.2 12 | MarkupSafe==2.1.3 13 | packaging==23.1 14 | pyinstaller==5.13.0 15 | pyinstaller-hooks-contrib==2023.6 16 | python-dateutil==2.8.2 17 | pytz==2023.3.post1 18 | PyYAML==6.0.1 19 | six==1.16.0 20 | tzlocal==5.0.1 21 | Werkzeug==2.3.6 22 | zipp==3.16.2 23 | -------------------------------------------------------------------------------- /web-ui/src/assets/styles/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Amarante'); 2 | 3 | html { 4 | width: 100%; 5 | } 6 | 7 | body { 8 | width: 100%; 9 | background-color: rgba(205, 247, 209, 0.6); 10 | /* background-color: #CDF7D1; */ 11 | } 12 | 13 | .top { 14 | height: 26vh; 15 | } 16 | 17 | .tittle { 18 | font-family: 'Amarante', Tahoma, sans-serif; 19 | font-weight: bold; 20 | font-size: 3.6em; 21 | line-height: 1.3em; 22 | margin: 0; 23 | color: rgba(28, 135, 39, 0.5); 24 | text-align: center; 25 | } 26 | -------------------------------------------------------------------------------- /scripts/release/build-client-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # working directory should be scripts/release 5 | 6 | set -e 7 | 8 | cd ../../client-go/ || exit 9 | 10 | # This will generate file './ping-charts-client' 11 | go build 12 | mv client-go "../dist/ping-charts-client-go" 13 | 14 | # static build 15 | # https://stackoverflow.com/questions/55450061/go-build-with-another-glibc/61812048#61812048 16 | # https://www.reddit.com/r/golang/comments/pi97sp/what_is_the_consequence_of_using_cgo_enabled0/ 17 | export CGO_ENABLED=0 18 | go build 19 | mv client-go "../dist/ping-charts-client-go-static" 20 | -------------------------------------------------------------------------------- /web-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/src/test/sqlite/insert_data.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | import datetime 4 | import random 5 | import sys 6 | 7 | path = "../../" # src directory 8 | 9 | sys.path.append(path) 10 | 11 | 12 | from lib import database as db 13 | 14 | def insert_large(start): 15 | count = 2484000 16 | ls = [] 17 | for _ in range(count): 18 | start = start + datetime.timedelta(seconds=20) 19 | serverId = random.randint(1, 10) 20 | targetId = random.randint(1, 3) 21 | delay = random.uniform(20, 200) 22 | loss = random.uniform(0, 0.33) 23 | ls.append(db.entry(serverId, targetId, start, loss, delay)) 24 | db.insert_entries(ls) 25 | 26 | if __name__ == "__main__": 27 | path = path + 'data' 28 | db.init_db(path=path) 29 | timestamp = datetime.datetime(2022,1,1,0,0,0) 30 | insert_large(timestamp) 31 | -------------------------------------------------------------------------------- /server/src/lib/respentity.py: -------------------------------------------------------------------------------- 1 | class row: 2 | def __init__(self, name: str, label: str, chartDataList: list) -> None: 3 | self.name = name 4 | self.label = label 5 | self.chartDataList = chartDataList 6 | 7 | 8 | class chartData: 9 | def __init__(self, delay: list, loss: list, time: list) -> None: 10 | self.delay = delay 11 | self.loss = loss 12 | self.time = time 13 | 14 | @staticmethod 15 | def empty(): 16 | return chartData([0], [0], [0]) 17 | 18 | 19 | class response: 20 | """ 21 | { 22 | targets: ['', '', ''] 23 | rows: [ 24 | { 25 | name: '', 26 | label: '', 27 | chartDataList: [{delay: [], time: [], loss: []}, {}, {}] 28 | }, {} , {} 29 | ] 30 | } 31 | """ 32 | 33 | def __init__(self, targets: list, rows: list) -> None: 34 | self.rows = rows 35 | self.targets = targets 36 | -------------------------------------------------------------------------------- /scripts/client-update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | green_output() { 6 | echo -e "\033[32m$1\033[0m" 7 | } 8 | 9 | red_output() { 10 | echo -e "\033[31m$1\033[0m" 11 | } 12 | 13 | file_not_exist() { 14 | if [ -e "$1" ]; then 15 | red_output "File '$1' exist." 16 | return 1; 17 | fi 18 | } 19 | 20 | if [[ $EUID -ne 0 ]]; then 21 | red_output "This script must be run with root permission." 22 | exit 1 23 | fi 24 | 25 | CONFIGFILE="/etc/systemd/system/pingChartsClient.timer" 26 | BIN="/opt/ping-charts/ping-charts-client" 27 | 28 | if file_not_exist "$CONFIGFILE" ; then 29 | red_output "File \"$CONFIGFILE\" not exist." 30 | exit 1 31 | fi 32 | 33 | systemctl stop pingChartsClient.timer 34 | green_output "Stop pingChartsClient.timer" 35 | sleep 1s 36 | 37 | 38 | wget -O "$BIN" "https://github.com/eastarpen/ping-charts/releases/latest/download/ping-charts-client-go" 39 | green_output "Downloaded ping-charts-client-go" 40 | 41 | systemctl daemon-reload 42 | systemctl start pingChartsClient.timer 43 | systemctl status pingChartsClient.timer 44 | -------------------------------------------------------------------------------- /scripts/server-update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | green_output() { 6 | echo -e "\033[32m$1\033[0m" 7 | } 8 | 9 | red_output() { 10 | echo -e "\033[31m$1\033[0m" 11 | } 12 | 13 | file_not_exist() { 14 | if [ -e "$1" ]; then 15 | red_output "File '$1' exist." 16 | return 1; 17 | fi 18 | } 19 | 20 | if [[ $EUID -ne 0 ]]; then 21 | red_output "This script must be run with root permission." 22 | exit 1 23 | fi 24 | 25 | CONFIGFILE="/etc/systemd/system/pingChartsServer.service" 26 | BIN="/opt/ping-charts/ping-charts-server" 27 | LATESTTAG="v1.2.0" 28 | 29 | if file_not_exist "$CONFIGFILE" ; then 30 | red_output "File \"$CONFIGFILE\" not exist." 31 | exit 1 32 | fi 33 | 34 | systemctl stop pingChartsServer 35 | green_output "Stop pingChartsServer" 36 | sleep 1s 37 | 38 | 39 | wget -O "$BIN" 'https://github.com/eastarpen/ping-charts/releases/latest/download/ping-charts-server-python' 40 | green_output "Downloaded ping-charts-server-python-${LATESTTAG}" 41 | 42 | systemctl daemon-reload 43 | systemctl start pingChartsServer.service 44 | systemctl status pingChartsServer.service 45 | -------------------------------------------------------------------------------- /doc/dev.md: -------------------------------------------------------------------------------- 1 | # Dev Guide 2 | 3 | This is a very simple project, so I don't want write a lot. 4 | 5 | I just put something I think which are useful here. 6 | 7 | If you have any questions, please open an issue. 8 | 9 | ## "Compile" 10 | 11 | **Server python** 12 | 13 | 1. run web-ui/build.sh to package WEBUI; 14 | 2. source or run build.source under server/src/; 15 | 3. pay attention to the relative path. 16 | 17 | **Client Golang** 18 | 19 | Under client-go, run `go build` 20 | 21 | ## Server && Client 22 | 23 | Nothing complex, just see the code. 24 | 25 | ## Web-UI 26 | 27 | - `constants.js` define some constants used to decide the charts color. 28 | 29 | `max_ping_value = 350;` define the max ping value in the charts. Change it will affect the bar height. 30 | 31 | - If you want debug it. 32 | 33 | * Set environment variable `env='env'`, see [readme](../README.md#Using source code) 34 | * Under `test-data` directory, create a json format file named 'data'(Just rename one of the data files already exist under that directory), then using `python3 -m http.server 8081` to export that file. 35 | For more information, see "vue.config.js". 36 | -------------------------------------------------------------------------------- /web-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ping-charts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.4.0", 12 | "chart.js": "^4.0.0", 13 | "core-js": "^3.8.3", 14 | "intersection-observer": "^0.12.2", 15 | "vue": "^3.2.13", 16 | "vue-chartjs": "^5.2.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.12.16", 20 | "@babel/eslint-parser": "^7.12.16", 21 | "@vue/cli-plugin-babel": "~5.0.0", 22 | "@vue/cli-plugin-eslint": "~5.0.0", 23 | "@vue/cli-service": "~5.0.0", 24 | "eslint": "^7.32.0", 25 | "eslint-plugin-vue": "^8.0.3" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/vue3-essential", 34 | "eslint:recommended" 35 | ], 36 | "parserOptions": { 37 | "parser": "@babel/eslint-parser" 38 | }, 39 | "rules": {} 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions", 44 | "not dead", 45 | "not ie 11" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/realeases.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # https://github.com/softprops/action-gh-release/issues/236#issuecomment-1150530128 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | tags: 11 | - 'v*.*.*' 12 | 13 | jobs: 14 | build-and-publish: 15 | # https://github.com/actions/setup-python/issues/544 16 | runs-on: ubuntu-20.04 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.8' 26 | - name: Setup node 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: '16' 30 | - name: Setup go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.21' 34 | 35 | - name: Build releases 36 | working-directory: ./scripts/release 37 | run: | 38 | ./pre-build.sh 39 | ./build-server-py.sh 40 | ./build-client-go.sh 41 | ./build-client-py.sh 42 | 43 | 44 | - name: Publish GitHub Releases 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | files: | 48 | ./dist/* 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /server/src/lib/utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | import datetime 4 | import logging 5 | 6 | from .respentity import chartData 7 | 8 | 9 | def load_config(path): 10 | assert os.path.exists(path), "Configuration file does not exist" 11 | 12 | with open(path, "r") as f: 13 | res = yaml.load(f, Loader=yaml.FullLoader) 14 | return res["targets"], res["clients"] 15 | 16 | 17 | def calculate_time(min: int) -> datetime.datetime: 18 | """ 19 | Ddeprecated. 20 | Using calculate_timestamp to solve timezone problem. 21 | """ 22 | return datetime.datetime.now() - datetime.timedelta(minutes=min) 23 | 24 | def calculate_timestamp(min: int) -> int: 25 | return int( 26 | (datetime.datetime.now() - datetime.timedelta(minutes=min)) 27 | .timestamp() 28 | ) 29 | 30 | def dbentries_to_chartData(entries: list) -> chartData: 31 | if not entries or len(entries) == 0: 32 | return chartData.empty() 33 | 34 | return chartData( 35 | [e.delay for e in entries], [e.loss for e in entries], [e.time for e in entries] 36 | ) 37 | 38 | 39 | def init_logging(level): 40 | logging.basicConfig( 41 | format="[%(asctime)s] [%(levelname)s] %(message)s", 42 | level=level, 43 | datefmt="%Y-%m-%d %H:%M:%S", 44 | ) 45 | -------------------------------------------------------------------------------- /client-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 7 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 12 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /scripts/server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | green_output() { 6 | echo -e "\033[32m$1\033[0m" 7 | } 8 | 9 | red_output() { 10 | echo -e "\033[31m$1\033[0m" 11 | } 12 | 13 | file_not_exist() { 14 | if [ -e "$1" ]; then 15 | red_output "File '$1' exist." 16 | return 1; 17 | fi 18 | } 19 | 20 | if [[ $EUID -ne 0 ]]; then 21 | red_output "This script must be run with root permission." 22 | exit 1 23 | fi 24 | 25 | mkdir -p /opt/ping-charts && cd /opt/ping-charts 26 | 27 | FILES=( 28 | '/opt/ping-charts/ping-charts-server' # make sure this file is executable. 29 | '/opt/ping-charts/server-update.sh' 30 | '/opt/ping-charts/server.yaml' 31 | '/etc/systemd/system/pingChartsServer.service' 32 | ) 33 | URLS=( 34 | 'https://github.com/eastarpen/ping-charts/releases/latest/download/ping-charts-server-python' 35 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/server-update' 36 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/doc/templates/server.yaml' 37 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/doc/templates/pingChartsServer.service' 38 | ) 39 | 40 | for index in "${!FILES[@]}"; do 41 | file=${FILES[$index]} 42 | if file_not_exist "$file" ; then 43 | wget -O "$file" "${URLS[$index]}" 44 | fi 45 | done 46 | 47 | chmod +x "${FILES[0]}" 48 | chmod +x "${FILES[1]}" 49 | 50 | green_output 'Ping Charts server has been downloaded.' 51 | green_output 'After configuration, run the following commands:' 52 | green_output 'sudo systemctl start pingChartsServer.service' 53 | green_output 'sudo systemctl enable pingChartsServer.service' 54 | -------------------------------------------------------------------------------- /scripts/client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | green_output() { 6 | echo -e "\033[32m$1\033[0m" 7 | } 8 | 9 | red_output() { 10 | echo -e "\033[31m$1\033[0m" 11 | } 12 | 13 | file_not_exist() { 14 | if [ -e "$1" ]; then 15 | red_output "File '$1' exist." 16 | return 1; 17 | fi 18 | } 19 | 20 | if [[ $EUID -ne 0 ]]; then 21 | red_output "This script must be run with root permission." 22 | exit 1 23 | fi 24 | 25 | mkdir -p /opt/ping-charts && cd /opt/ping-charts 26 | 27 | FILES=( 28 | '/opt/ping-charts/ping-charts-client' # make sure this file is executable 29 | '/opt/ping-charts/client-update.sh' 30 | '/opt/ping-charts/client.yaml' 31 | '/etc/systemd/system/pingChartsClient.service' 32 | '/etc/systemd/system/pingChartsClient.timer' 33 | ) 34 | URLS=( 35 | 'https://github.com/eastarpen/ping-charts/releases/latest/download/ping-charts-client-go' 36 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/client-update' 37 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/doc/templates/client.yaml' 38 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/doc/templates/pingChartsClient.service' 39 | 'https://raw.githubusercontent.com/eastarpen/ping-charts/master/doc/templates/pingChartsClient.timer' 40 | ) 41 | 42 | for index in "${!FILES[@]}"; do 43 | file=${FILES[$index]} 44 | if file_not_exist "$file" ; then 45 | wget -O "$file" "${URLS[$index]}" 46 | fi 47 | done 48 | 49 | chmod +x "${FILES[0]}" 50 | chmod +x "${FILES[1]}" 51 | 52 | green_output 'Ping Charts client has been downloaded.' 53 | green_output 'After configuration, run the following commands:' 54 | green_output 'sudo systemctl start pingChartsClient.timer' 55 | green_output 'sudo systemctl enable pingChartsClient.timer' 56 | -------------------------------------------------------------------------------- /doc/configuration-zh.md: -------------------------------------------------------------------------------- 1 | # 配置说明 2 | 3 | 确保客户端的配置与服务器配置相匹配,否则服务器将忽略客户端上传请求。 4 | 5 | ## 客户端 6 | 7 | [客户端模板](./templates/client.yaml) 8 | 9 | - 确保客户端的 "name"、"clientId" 和 "passw" 与服务器配置相同。 10 | - `uploadUrl` 为 'http(s):/service.address/upload'。 11 | 12 | 例如: 13 | 14 | | 服务地址 | uploadUrl | 15 | | ---------------------- | ------------------------------- | 16 | | http://127.0.0.1:8000 | http://127.0.0.1:8000/upload | 17 | | http://domain.com | http://domain.com/upload | 18 | | http://domain.com/ping | http://domain.com/ping/upload | 19 | 20 | - targets 必须预先在服务器配置中注册,否则该数据将被忽略。 21 | 22 | ```yaml 23 | name: client # 客户端名称,允许重复 24 | clientId: 1 # 客户端 ID,整数,不允许重复 25 | passw: password # 客户端密码,字符串 26 | uploadUrl: url # 服务器上传 URL 27 | targets: # 目标列表 28 | - id: 1 # 目标 ID,整数,不允许重复 29 | name: chinanet # 目标名称,字符串,允许重复 30 | port: 80 # 目标端口,整数 31 | addr: ct.tz.cloudcpp.com # 目标地址,可以是 IP 或域名 32 | - id: 2 33 | name: chinaunicom 34 | port: 80 35 | addr: cu.tz.cloudcpp.com 36 | ``` 37 | 38 | ## 服务器 39 | 40 | [服务器模板](./templates/server.yaml) 41 | 42 | 注意对同一个 target, 服务端和客户端的 id 和 name 必须一致. 43 | 44 | alias 用来快速更改 target 在前端的显示名称 45 | 46 | ```yaml 47 | targets: # 注册目标 48 | - name: chinanet # 目标名称,字符串,允许重复 49 | id: 1 # 目标 ID,整数,不允许重复 50 | alias: china-net # 不必填。如果存在,将显示在网页中(忽略name的值) 51 | - name: chinaunicom 52 | id: 2 53 | - name: chinamobile 54 | id: 3 55 | clients: 56 | - name: client # 客户端名称,允许重复 57 | label: US # 客户端标签,字符串,将显示在网页中 58 | pass: password # 客户端密码,字符串 59 | id: 1 # 客户端 ID,整数,不允许重复 60 | ``` 61 | -------------------------------------------------------------------------------- /doc/setup-zh.md: -------------------------------------------------------------------------------- 1 | # 快速设置指南 2 | 3 | **仅测试过 Debian 系统** 4 | 5 | 要快速轻松地使用默认配置设置此项目,你可以使用开发者提供的一键脚本。但是,建议在继续之前阅读使用文档。 6 | 7 | ## 客户端 8 | 9 | 1. 下载[客户端设置脚本](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/client.sh)并以root权限执行它。 10 | 2. 脚本将执行以下任务: 11 | - 尝试创建目录"/opt/ping-charts"。 12 | - 从[releases](https://github.com/eastarpen/ping-charts/releases)页面下载预编译的客户端。 13 | - 下载[客户端模板配置](../doc/templates/client.yaml)。 14 | - 将客户端systemd服务配置文件下载到"/etc/systemd/system/"。 15 | 16 | 执行脚本后,请按照以下步骤操作: 17 | 18 | 1. 根据你的[服务器配置](./configuration-zh.md#服务器)重写'/opt/ping-charts/client.yaml'文件。 19 | 2. 运行命令`systemctl start pingChartsClient.timer`启动客户端。这将每分钟对你的目标进行ping并将数据上传到服务器。 20 | 3. 如果需要,运行命令`systemctl enable pingChartsClient.timer`以使客户端在操作系统重新启动后自动启动。 21 | 22 | **注意事项:** 23 | - 如果你不更改服务配置文件,则客户端配置文件必须命名为'client.yaml'。 24 | 25 | ## 服务器 26 | 27 | 1. 下载[服务器设置脚本](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/server.sh)并以root权限执行它。 28 | 2. 脚本将执行以下任务: 29 | - 尝试创建目录"/opt/ping-charts"。 30 | - 从[releases](https://github.com/eastarpen/ping-charts/releases)页面下载预编译的服务器。 31 | - 下载[服务器模板配置](../doc/templates/server.yaml)。 32 | - 将服务器systemd服务配置文件下载到"/etc/systemd/system/"。 33 | 34 | 执行脚本后,请按照以下步骤操作: 35 | 36 | 1. 根据你的[客户端配置](./configuration-zh.md#客户端)重写'/opt/ping-charts/server.yaml'文件。 37 | 2. 运行命令`systemctl start pingChartsServer`启动服务器。 38 | 3. 如果需要,运行命令`systemctl enable pingChartsServer`以使服务器在操作系统重启后自动启动。 39 | 40 | **注意事项** 41 | 42 | - 如果你不更改服务配置文件,则服务器配置文件必须命名为'server.yaml'。 43 | - 服务器将在端口8000上运行。 44 | - 服务将监听来自'0.0.0.0'的请求。 45 | 46 | ## 更新 47 | 48 | 如果你使用开发者提供的一键脚本部署本项目, 你可以使用 [server-update](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/server-update) 和 [client-update](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/client-update) 来更新服务器和客户端到最新版本。 49 | 50 | ## 自定义 51 | 52 | **在进行任何更改之前,请确保你理解自己在做什么。** 53 | 54 | 如果你对默认设置不满意,可以自定义服务文件以满足你的需求。 55 | 56 | 首先,确保客户端和服务器正常运行。有关更多信息,请参阅[Usage](../README.md#Usage)。 57 | 58 | 接下来,修改服务配置文件: 59 | 60 | - 服务器:"/etc/systemd/system/pingChartsServer.service" 61 | - 客户端:"/etc/systemd/system/pingChartsClient.service"和"/etc/systemd/system/pingChartsClient.timer" 62 | -------------------------------------------------------------------------------- /doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Make sure the configuration in the client matches the server configuration. The server will ignore your upload request if the configurations do not match. 4 | 5 | ## Client 6 | 7 | [Client Template](./templates/client.yaml) 8 | 9 | - Ensure that the client "name", "clientId", and "passw" are the same as the server configuration. 10 | - The value of `uploadUrl` should be 'http(s)://service.address/upload'. 11 | 12 | For example: 13 | 14 | | Service Address | uploadUrl | 15 | | ---------------------- | ----------------------------- | 16 | | http://127.0.0.1:8000 | http://127.0.0.1:8000/upload | 17 | | http://domain.com | http://domain.com/upload | 18 | | http://domain.com/ping | http://domain.com/ping/upload | 19 | 20 | - Targets must be registered in the server configuration, or they will be ignored. 21 | 22 | 23 | ```yaml 24 | name: client # Client name (allow repeat) 25 | clientId: 1 # Client ID (integer, do not allow repeat) 26 | passw: password # Client password (string) 27 | uploadUrl: url # Server upload URL 28 | targets: # Targets list 29 | - id: 1 # Target ID (integer, do not allow repeat) 30 | name: chinanet # Target name (string, allow repeat) 31 | port: 80 # Target port (integer) 32 | addr: ct.tz.cloudcpp.com # Target address (can be IP or domain) 33 | - id: 2 34 | name: chinaunicom 35 | port: 80 36 | addr: cu.tz.cloudcpp.com 37 | ``` 38 | 39 | ## Server 40 | 41 | [Server Template](./templates/server.yaml) 42 | 43 | Notice that for one target, the name and id in server side and client side should be the same. 44 | 45 | Target alias is used to change the target's display name in web page. 46 | 47 | ```yaml 48 | targets: # Register targets 49 | - name: chinanet # Target name (string, allow repeat) 50 | id: 1 # Target ID (integer, do not allow repeat) 51 | alias: china-net # Not required. If exist, it will show in front-end(instead of the value of name) 52 | - name: chinaunicom 53 | id: 2 54 | - name: chinamobile 55 | id: 3 56 | clients: 57 | - name: client # Client name (allow repeat) 58 | label: US # Client label (string, it will show on the web page) 59 | pass: password # Client password (string) 60 | id: 1 # Client ID (integer, do not allow repeat) 61 | ``` 62 | -------------------------------------------------------------------------------- /web-ui/src/assets/styles/radio.css: -------------------------------------------------------------------------------- 1 | /* comes from https://codepen.io/havardob/pen/ExVaELV */ 2 | 3 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"); 4 | 5 | :root { 6 | --primary-color: #185ee0; 7 | --secondary-color: #e6eef9; 8 | } 9 | 10 | *, 11 | *:after, 12 | *:before { 13 | box-sizing: border-box; 14 | } 15 | 16 | .radio-container { 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | right: 0; 21 | bottom: 0; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | .tabs { 27 | display: flex; 28 | position: relative; 29 | background-color: #fff; 30 | box-shadow: 0 0 1px 0 rgba(#185ee0, 0.15), 0 6px 12px 0 rgba(#185ee0, 0.15); 31 | padding: 0.75rem; 32 | border-radius: 99px; // just a high number to create pill effect 33 | * { 34 | z-index: 2; 35 | } 36 | } 37 | 38 | input[type="radio"] { 39 | display: none; 40 | } 41 | 42 | .tab { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | height: 54px; 47 | width: 200px; 48 | font-size: 1.25rem; 49 | font-weight: 500; 50 | border-radius: 99px; // just a high number to create pill effect 51 | cursor: pointer; 52 | transition: color 0.15s ease-in; 53 | } 54 | 55 | .notification { 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | width: 2rem; 60 | height: 2rem; 61 | margin-left: 0.75rem; 62 | border-radius: 50%; 63 | background-color: var(--secondary-color); 64 | transition: 0.15s ease-in; 65 | } 66 | 67 | input[type="radio"] { 68 | &:checked { 69 | & + label { 70 | color: var(--primary-color); 71 | & > .notification { 72 | background-color: var(--primary-color); 73 | color: #fff; 74 | } 75 | } 76 | } 77 | } 78 | 79 | input[id="radio-1"] { 80 | &:checked { 81 | & ~ .glider { 82 | transform: translateX(0); 83 | } 84 | } 85 | } 86 | 87 | input[id="radio-2"] { 88 | &:checked { 89 | & ~ .glider { 90 | transform: translateX(100%); 91 | } 92 | } 93 | } 94 | 95 | input[id="radio-3"] { 96 | &:checked { 97 | & ~ .glider { 98 | transform: translateX(200%); 99 | } 100 | } 101 | } 102 | 103 | .glider { 104 | position: absolute; 105 | display: flex; 106 | height: 54px; 107 | width: 200px; 108 | background-color: var(--secondary-color); 109 | z-index: 1; 110 | border-radius: 99px; // just a high number to create pill effect 111 | transition: 0.25s ease-out; 112 | } 113 | 114 | @media (max-width: 700px) { 115 | .tabs { 116 | transform: scale(0.6); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /web-ui/src/assets/styles/table.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Thanks to 3 | * - [Create custom scrollbars using CSS](https://www.youtube.com/watch?v=lvKK2fs6h4I) 4 | * - [CUSTOM SCROLLBAR MAKER](https://codepen.io/stephenpaton-tech/full/JjRvGmY) 5 | * - [How to change the color of the bottom right square of the scrollbar in webkit](https://stackoverflow.com/questions/11667890/css-how-to-change-the-color-of-the-bottom-right-square-of-the-scrollbar-in-webk) 6 | * - [How TO - Custom Scrollbar](https://www.w3schools.com/howto/howto_css_custom_scrollbar.asp) 7 | */ 8 | 9 | * { 10 | font-family: sans-serif; 11 | } 12 | 13 | .content-table { 14 | border-collapse: collapse; 15 | margin: auto; 16 | width: 100%; 17 | font-size: 0.9em; 18 | text-align: center; 19 | border-radius: 5px 5px 0 0; 20 | } 21 | 22 | .content-table thead tr { 23 | text-transform: uppercase; 24 | background-color: #009879; 25 | color: #ffffff; 26 | font-weight: bold; 27 | } 28 | 29 | .content-table th, 30 | .content-table td { 31 | padding: 12px 15px; 32 | } 33 | 34 | .content-table td.chart { 35 | width: 50px; 36 | padding: 0px 15px; 37 | padding-top: 5px; 38 | padding-bottom: 3px; 39 | font-weight: normal; 40 | } 41 | 42 | .content-table tbody tr { 43 | font-weight: bold; 44 | border-bottom: 1px solid #dddddd; 45 | } 46 | 47 | .content-table tbody tr { 48 | background-color: #f3f3f3; 49 | } 50 | 51 | .overflow-box { 52 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); 53 | overflow: auto; 54 | width: 85vw; 55 | height: 73vh; 56 | border-radius: 5px 5px 0 0; 57 | margin: auto; 58 | } 59 | 60 | /* ===== Scrollbar CSS ===== */ 61 | /* Firefox */ 62 | .overflow-box { 63 | scrollbar-width: auto; 64 | scrollbar-color: #8f54a0 #ffffff; 65 | } 66 | 67 | /* Chrome, Edge, and Safari */ 68 | .overflow-box::-webkit-scrollbar { 69 | width: 12px; 70 | height: 12px; 71 | } 72 | 73 | .overflow-box::-webkit-scrollbar-track { 74 | border-radius: 10px; 75 | background-color: rgba(205, 247, 209, 0.6); 76 | } 77 | 78 | .overflow-box::-webkit-scrollbar-thumb { 79 | background: #009879; 80 | border-radius: 10px; 81 | border: 3px solid rgba(205, 247, 209, 0.6); 82 | } 83 | 84 | .overflow-box::-webkit-scrollbar-corner { 85 | background-color: rgba(205, 247, 209, 0.6); 86 | } 87 | 88 | thead th { 89 | background-color: #009879; 90 | position: -webkit-sticky; /* for Safari */ 91 | position: sticky; 92 | top: 0; 93 | } 94 | 95 | tbody th { 96 | position: -webkit-sticky; /* for Safari */ 97 | background-color: #f3f3f3; 98 | position: sticky; 99 | left: 0; 100 | } 101 | 102 | thead th:first-child { 103 | left: 0; 104 | z-index: 1; 105 | } 106 | 107 | tbody th { 108 | border-collapse: separate; 109 | } 110 | -------------------------------------------------------------------------------- /web-ui/src/components/RadioCom.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 119 | -------------------------------------------------------------------------------- /doc/setup.md: -------------------------------------------------------------------------------- 1 | # Quick Setup 2 | 3 | **Work fine under Debian, not test other OS** 4 | 5 | To quickly and easily set up this project with the default configuration, you can use the provided one-click scripts. However, it is recommended to read the documentation before proceeding. 6 | 7 | ## Client 8 | 9 | 1. Download the [client setup script](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/client.sh) and execute it with root permission. 10 | 2. The script will perform the following tasks: 11 | - Attempt to create the directory "/opt/ping-charts". 12 | - Download the pre-compiled client from the [releases](https://github.com/eastarpen/ping-charts/releases) page. 13 | - Download the [client template configuration](../doc/templates/client.yaml). 14 | - Download the client systemd service configuration files to "/etc/systemd/system/". 15 | 16 | After executing the script, follow these steps: 17 | 18 | 1. Rewrite the '/opt/ping-charts/client.yaml' file with your server [configuration](./configuration.md#Client). 19 | 2. Run the command `systemctl start pingChartsClient.timer` to start the client. This will ping your targets and upload data to the server once a minute. 20 | 3. If desired, run the command `systemctl enable pingChartsClient.timer` to make the client start automatically after the OS reboot. 21 | 22 | **Attention:** 23 | - If you do not change the service configuration file, the client configuration file must be named 'client.yaml'. 24 | 25 | ## Server 26 | 27 | 1. Download the [server setup script](https://raw.githubusercontent.com/eastarpen/ping-charts/master/scripts/server.sh) and execute it with root permission. 28 | 2. The script will perform the following tasks: 29 | - Attempt to create the directory "/opt/ping-charts". 30 | - Download the pre-compiled server from the [releases](https://github.com/eastarpen/ping-charts/releases) page. 31 | - Download the [server template configuration](../doc/templates/server.yaml). 32 | - Download the server systemd service configuration file to "/etc/systemd/system/". 33 | 34 | After executing the script, follow these steps: 35 | 36 | 1. Rewrite the '/opt/ping-charts/server.yaml' file with your client [configuration](./configuration.md#Server). 37 | 2. Run the command `systemctl start pingChartsServer` to start the server. 38 | 3. If desired, run the command `systemctl enable pingChartsServer` to make the server start automatically after the OS reboot. 39 | 40 | **Attention:** 41 | - If you do not change the service configuration file, the server configuration file must be named 'server.yaml'. 42 | - The server will run on port 8000. 43 | - The service will listen for requests from '0.0.0.0'. 44 | 45 | ## Update 46 | 47 | After setting up the project by one-click scripts, you can use [client-update](https://github.com/eastarpen/ping-charts/blob/master/scripts/client-update) and [server-update](https://github.com/eastarpen/ping-charts/blob/master/scripts/server-update) to update client and server to the latest version. 48 | 49 | ## Customization 50 | 51 | **Before making any changes, ensure that you understand what you are doing.** 52 | 53 | If you are not satisfied with the default settings, you can customize the service files to meet your needs. 54 | 55 | Firstly, ensure that the client and server are running correctly. For more information, refer to the [Usage](../README.md#Usage) section. 56 | 57 | Next, modify the service configuration files: 58 | 59 | - Server: "/etc/systemd/system/pingChartsServer.service" 60 | - Client: "/etc/systemd/system/pingChartsClient.service" and "/etc/systemd/system/pingChartsClient.timer" 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | # Ping Charts 7 | A simple tool to visualize delays of VPS using Python Flask and Vue. 8 | 9 | ## Screenshots 10 | 11 |
12 | General 13 |

14 | 15 |

16 |
17 | 18 |
19 | Scroll 20 |

21 | 22 |

23 |
24 | 25 | ## Usage 26 | 27 | For those who do not care about the code and just want to simply run this project, see [setup](./doc/setup.md). 28 | 29 | For Chinese users: [一键脚本使用说明](./doc/setup-zh.md). 30 | 31 | ### Configuration 32 | 33 | See [here](./doc/configuration.md). 34 | 35 | For Chinese users: [配置说明](./doc/configuration-zh.md). 36 | 37 | ### Using compiled executable file 38 | 39 | **Work fine under Debian, donot test other OS** 40 | 41 | Just prepare the configuration files and download the executable file from [releases](https://github.com/eastarpen/ping-charts/releases) page, then execute it. 42 | 43 | Run `ping-charts-client --help` and `ping-charts-server --help` to see available CLI options. 44 | 45 | ### Using source code 46 | 47 | **Server && Client** 48 | 49 | - Install dependencies in "requirements.ext" under client/ and server/. 50 | - Prepare the configuration file (both server side and client side). 51 | - Run the command `python ping-charts-client.py --help` or `python ping-charts-server.py --help`. 52 | 53 | **Web-UI** 54 | 55 | ~~The web static files have been packed into 'static/' and 'templates/' under 'server/src/'. You do not need to run the Web-UI separately in general.~~ 56 | 57 | Using command `npm run build` to build static files. 58 | 59 | If you want to know how releases pack the static files, see [build.sh](./web-ui/build.sh). 60 | 61 | Only when you want to change the UI elements (which means you will directly change the source code), you should run the web-ui using Node.js. 62 | 63 | Add the 'dev' environment variable to tell Vue to use the development mode. 64 | 65 | ```shell 66 | npm install 67 | export dev='dev' # see vue.config.js 68 | npm run serve 69 | ``` 70 | 71 | ### Nginx sub-location 72 | 73 | If you want to use the project under Nginx as a 'sub-location', you can refer to the following configuration. 74 | 75 | Make sure you understand what you are doing when making any changes. 76 | 77 | ``` 78 | # ping charts 79 | location /ping/ { 80 | proxy_redirect off; 81 | proxy_pass http://127.0.0.1:5555; // server port 82 | proxy_http_version 1.1; 83 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 84 | proxy_set_header X-Forwarded-Proto $scheme; 85 | proxy_set_header X-Forwarded-Host $host; 86 | proxy_set_header X-Forwarded-Prefix /ping; // same as the location set above 87 | rewrite ^/ping/(.*) /$1 break; // watch out 88 | } 89 | ``` 90 | 91 | ## Dev 92 | 93 | PRs are welcomed. 94 | 95 | **TODO** 96 | 97 | I want to keep implementing these functions if many people need. 98 | 99 | - [ ] Delete old data (see test/../delete_old_data.py) 100 | - [ ] Auto manage clientId and targetId 101 | - [ ] Scroll table border radius 102 | 103 | [Some information](./doc/dev.md). 104 | 105 | ## Thanks 106 | 107 | This project is inspired by [ping0.cc](https://ping0.cc/vpsmon/) 108 | 109 | - [Title style comes from here](https://codepen.io/jakestuts/pen/AEMqEM) 110 | - [Table style](https://dcode.domenade.com/tutorials/how-to-style-html-tables-with-css) 111 | - [Fixed column table](https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css) 112 | -------------------------------------------------------------------------------- /web-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 37 | 38 | 107 | -------------------------------------------------------------------------------- /server/src/lib/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | import math 4 | import logging 5 | 6 | from .utils import calculate_timestamp 7 | 8 | db_con, db_path = None, None 9 | 10 | MAX_ENTRIES = 250 11 | TABLE_NAME = "pingdata" 12 | CREATE_TABLE_SQL = """CREATE TABLE %s ( 13 | id INTEGER PRIMARY KEY, 14 | clientId INTEGER NOT NULL, 15 | targetId INTEGER NOT NULL, 16 | time INTEGER NOT NULL, 17 | loss FLOAT NOT NULL, 18 | delay FLOAT NOT NULL);""" % ( 19 | TABLE_NAME 20 | ) 21 | 22 | INSERT_ENTRY_SQL = """ INSERT INTO %s 23 | ('clientId', 'targetId', 'time', 'loss', 'delay') 24 | VALUES(?,?,?,?,?);""" % ( 25 | TABLE_NAME 26 | ) 27 | 28 | QUERY_ENTRY_SQL = """SELECT clientId, targetId, time, loss, delay FROM %s 29 | WHERE time > ? AND clientId = ? AND targetId = ?;""" % ( 30 | TABLE_NAME 31 | ) 32 | 33 | DELETE_OLD_DATA_SQL = """DELETE FROM %s where time < ?;""" % (TABLE_NAME) 34 | 35 | 36 | class entry: 37 | def __init__( 38 | self, 39 | clientId: int, 40 | targetId: int, 41 | time: int, 42 | loss: float, 43 | delay: float, 44 | ) -> None: 45 | self.clientId = clientId 46 | self.targetId = targetId 47 | self.time = time 48 | self.loss = round(loss, 2) 49 | self.delay = round(delay, 2) 50 | 51 | 52 | def init_db(dbname="pingcharts.db", path="./", createDir=False) -> None: 53 | if createDir: 54 | os.makedirs(path, exist_ok=True) 55 | 56 | if not os.path.exists(path): 57 | raise FileNotFoundError("Path does not exist: %s" % path) 58 | 59 | global db_path 60 | db_path = os.path.join(path, dbname) 61 | 62 | try: 63 | with get_connection() as con: 64 | # init tables 65 | res = con.execute(f"SELECT name FROM sqlite_master WHERE name='{TABLE_NAME}'") 66 | table_not_exist = res.fetchone() is None 67 | 68 | if table_not_exist: 69 | logging.info("Create table [%s]" % TABLE_NAME) 70 | con.execute(CREATE_TABLE_SQL) 71 | 72 | except sqlite3.Error: 73 | logging.error("Error while init db.") 74 | 75 | 76 | def get_connection(): 77 | 78 | global db_con, db_path 79 | 80 | assert db_path 81 | 82 | if not db_con: 83 | # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/ 84 | db_con = sqlite3.connect(db_path, check_same_thread=False) 85 | return db_con 86 | 87 | 88 | def insert_entries( 89 | entries: list, 90 | ): 91 | assert entries and len(entries) > 0 and isinstance(entries[0], entry) 92 | 93 | try: 94 | with get_connection() as con: 95 | data = [(e.clientId, e.targetId, e.time, e.loss, e.delay) for e in entries] 96 | con.executemany(INSERT_ENTRY_SQL, data) 97 | except sqlite3.Error: 98 | logging.error("Error while insert entries.") 99 | 100 | 101 | def insert_entry( 102 | entry: entry, 103 | ): 104 | insert_entries([entry]) 105 | 106 | 107 | def query_entries(timestamp: float, clientId: int, targetId: int): 108 | res = [] 109 | try: 110 | with get_connection() as con: 111 | records = con.execute(QUERY_ENTRY_SQL, (timestamp, clientId, targetId)).fetchall() 112 | step = math.ceil(len(records) / MAX_ENTRIES) 113 | res = [entry(*r) for idx, r in enumerate(records) if idx % step == 0] 114 | except sqlite3.Error: 115 | logging.error("Error while query entries.") 116 | return res 117 | 118 | 119 | def delete_old_data(day = 7): 120 | time = calculate_timestamp(60 * 24 * day) # mins 121 | try: 122 | with get_connection() as con: 123 | con.execute(DELETE_OLD_DATA_SQL, (time,)) 124 | except sqlite3.Error: 125 | logging.error("Error while delete old data.") 126 | -------------------------------------------------------------------------------- /client/src/ping-charts-client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import requests 4 | import click 5 | import socket 6 | import ipaddress 7 | import time 8 | import statistics 9 | import yaml 10 | 11 | 12 | VERSION = "v1.3.1" 13 | 14 | CONFIG_KEYS = [ 15 | "name", 16 | "clientId", 17 | "passw", 18 | "uploadUrl", 19 | "targets", 20 | ] 21 | TARGET_KEYS = [ 22 | "id", 23 | "name", 24 | "port", 25 | "addr", 26 | ] 27 | 28 | clientId = None 29 | passw = None 30 | name = None 31 | tars = None 32 | 33 | 34 | def load_config(config_file: str): 35 | assert os.path.exists(config_file), "config file not found" 36 | 37 | conf = None 38 | 39 | with open(config_file) as file: 40 | conf = yaml.safe_load(file) 41 | 42 | # check keys 43 | for key in CONFIG_KEYS: 44 | if key not in conf: 45 | raise Exception("config file missing key: " + key) 46 | 47 | for tar in conf["targets"]: 48 | for key in TARGET_KEYS: 49 | if key not in tar: 50 | raise Exception("target missing key: " + key) 51 | return ( 52 | conf["clientId"], 53 | conf["passw"], 54 | conf["name"], 55 | conf["uploadUrl"], 56 | conf["targets"], 57 | ) 58 | 59 | 60 | def get_ip_address(host): 61 | try: 62 | # Check if host is already a valid IP address 63 | ipaddress.ip_address(host) 64 | return host 65 | except ValueError: 66 | # If not, perform a DNS lookup 67 | try: 68 | return socket.gethostbyname(host) 69 | except socket.gaierror: 70 | raise Exception(f"Unable to resolve domain or invalid IP address: {host}") 71 | 72 | def tcping(host, port=80, count=10, timeout=0.5): 73 | delays = [] 74 | lost_packets = 0 75 | ip = get_ip_address(host) 76 | 77 | for _ in range(count): 78 | try: 79 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 80 | sock.settimeout(timeout) 81 | 82 | start_time = time.time() 83 | sock.connect((ip, port)) 84 | end_time = time.time() 85 | 86 | delay = end_time - start_time 87 | delays.append(delay) 88 | 89 | sock.close() 90 | except socket.timeout: 91 | lost_packets += 1 92 | 93 | avg_delay = statistics.mean(delays) * 1000 if delays else 0 94 | packet_loss = lost_packets / count 95 | 96 | return avg_delay, packet_loss 97 | 98 | 99 | def send_request(data: dict, upload_url: str): 100 | json_data = json.dumps(data) 101 | response = requests.post( 102 | upload_url, data=json_data, headers={"Content-Type": "application/json"} 103 | ) 104 | print(f"Server response: {response.text}, status code: {response.status_code}") 105 | 106 | 107 | def generate_data(count: int, timeout: float): 108 | d = { 109 | "clientId": clientId, 110 | "passw": passw, 111 | "name": name, 112 | } 113 | data = [] 114 | for tar in tars: 115 | res = { 116 | "name": tar["name"], 117 | "id": tar["id"], 118 | "time": int(time.time()), 119 | } 120 | delay, loss = tcping(tar["addr"], tar["port"], count, timeout) 121 | res["delay"] = delay 122 | res["loss"] = loss 123 | data.append(res) 124 | d["data"] = data 125 | return d 126 | 127 | 128 | @click.command() 129 | @click.option( 130 | "--config", 131 | "-c", 132 | default="./client.yaml", 133 | help="yaml config file, default value './client.yaml'", 134 | ) 135 | @click.option( 136 | "--timeout", 137 | "-t", 138 | default=0.5, 139 | help="time that judged as package loss, 0.5s defaulted. The unit is seconds.", 140 | ) 141 | @click.option( 142 | "--package", 143 | "-p", 144 | default=10, 145 | help="how many packages to send in a test, 10 defaulted", 146 | ) 147 | @click.option( 148 | "--version", 149 | is_flag=True, 150 | default=False, 151 | help="Print version and exit", 152 | ) 153 | def client(config, timeout, package, version): 154 | """Ping Charts client(python version).\nA simple tool to visualize vps latency based on TCP.""" 155 | if version: 156 | print("Ping Charts client-py " + VERSION) 157 | return 158 | global clientId, passw, name, tars 159 | clientId, passw, name, upload_url, tars = load_config(config) 160 | data = generate_data(package, timeout) 161 | send_request(data, upload_url) 162 | 163 | 164 | if __name__ == "__main__": 165 | client() 166 | -------------------------------------------------------------------------------- /web-ui/src/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 126 | -------------------------------------------------------------------------------- /server/src/ping-charts-server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | from flask_apscheduler import APScheduler 3 | import json 4 | import click 5 | import os 6 | import logging 7 | 8 | from lib import database as db 9 | from lib import utils, respentity 10 | 11 | 12 | VERSION = "v1.3.1" 13 | 14 | targets, clients = None, None 15 | targetList = None 16 | 17 | 18 | app = Flask(__name__) 19 | scheduler = APScheduler() 20 | 21 | 22 | def autoDelete(day: int): 23 | scheduler.add_job( 24 | id="Scheduled Task", 25 | func=lambda: db.delete_old_data(day), 26 | trigger="interval", 27 | seconds=24 * 60 * 60, 28 | ) 29 | scheduler.start() 30 | 31 | 32 | @app.route("/data", methods=["GET"]) 33 | def getChartData(): 34 | min = request.args.get("min") 35 | 36 | if not min or not min.isdigit(): 37 | logging.info("Request param min error") 38 | return "Bad Request", 400 39 | 40 | time = utils.calculate_timestamp(int(min)) 41 | rows = [] 42 | for client_dict in clients: 43 | clientDataList = [] 44 | for target_dict in targets: 45 | targetId, clientId = target_dict["id"], client_dict["id"] 46 | entries = db.query_entries(time, clientId, targetId) 47 | clientDataList.append(utils.dbentries_to_chartData(entries)) 48 | rows.append( 49 | respentity.row(client_dict["name"], client_dict["label"], clientDataList) 50 | ) 51 | 52 | resp = respentity.response(targetList, rows) 53 | resp_json = json.dumps(resp.__dict__, default=lambda o: o.__dict__) 54 | return resp_json 55 | 56 | 57 | def check_reqeust(d: dict): 58 | # check keys 59 | if ( 60 | not d 61 | or "clientId" not in d 62 | or "name" not in d 63 | or "passw" not in d 64 | or "data" not in d 65 | ): 66 | return "Request data format error", 400 67 | for client_dict in clients: 68 | # check client name and password and id 69 | if client_dict["id"] != d["clientId"]: 70 | continue 71 | if client_dict["name"] != d["name"] or client_dict["pass"] != d["passw"]: 72 | return "Auth Error", 403 73 | res = [] 74 | for tar in targets: 75 | entry = None 76 | for data in d["data"]: 77 | try: 78 | if tar["id"] != data["id"] or tar["name"] != data["name"]: 79 | continue 80 | entry = db.entry( 81 | d["clientId"], 82 | tar["id"], 83 | data["time"], 84 | data["loss"], 85 | data["delay"], 86 | ) 87 | break 88 | except KeyError: 89 | return ("Target data format error", 400) 90 | if entry: 91 | res.append(entry) 92 | return res, 200 93 | return "Client not found", 400 94 | 95 | 96 | @app.route("/upload", methods=["POST"]) 97 | def uploadData(): 98 | dataDict = request.get_json() 99 | if dataDict: 100 | name = dataDict.get("name", "UNKNOW_NAME") 101 | id = dataDict.get("clientId", "UNKNOW_ID") 102 | data, status_code = check_reqeust(dataDict) 103 | if status_code != 200: 104 | logging.info(f"{name} {id} {data}") 105 | return data, status_code 106 | try: 107 | db.insert_entries(data) 108 | except Exception: 109 | logging.error(f"{name} {id} Insert error") 110 | return "Server Error", 500 111 | return "Success", 200 112 | 113 | 114 | @app.get("/") 115 | def root(): 116 | return render_template("index.html") 117 | 118 | 119 | @click.command() 120 | @click.option( 121 | "--config", 122 | "-c", 123 | default="./server.yaml", 124 | help="YAML config file path, default './server.yaml'", 125 | ) 126 | @click.option( 127 | "--data", 128 | "-d", 129 | default="./data", 130 | help="Direcotry to store data, './data/' defaulted", 131 | ) 132 | @click.option( 133 | "--port", 134 | "-p", 135 | default=8000, 136 | help="Service port, 8000 defaulted", 137 | ) 138 | @click.option( 139 | "--delete", 140 | default=7, 141 | help="How long ago should the data be deleted, in days. 7 defaulted.", 142 | ) 143 | @click.option( 144 | "--local", 145 | "-l", 146 | help="Listen 127.0.0.1 only", 147 | is_flag=True, 148 | ) 149 | @click.option( 150 | "--debug", 151 | help="Set Log level to DEBUG", 152 | is_flag=True, 153 | ) 154 | @click.option( 155 | "--version", 156 | help="Print version and exit", 157 | default=False, 158 | is_flag=True, 159 | ) 160 | def server( 161 | config: str, 162 | data: str, 163 | port: int, 164 | delete: int, 165 | local: bool, 166 | debug: bool, 167 | version: bool, 168 | ): 169 | """Ping Charts server.\nA simple tool to visualize vps latency based on TCP.""" 170 | if version: 171 | print("Ping Charts Server-py ", VERSION) 172 | return 173 | # init logging 174 | logging_level = logging.DEBUG if debug else logging.INFO 175 | utils.init_logging(logging_level) 176 | 177 | if not os.path.exists(config): 178 | logging.error(f'Config file "{config}" not exist') 179 | return 180 | 181 | global targets, clients, targetList, app 182 | targets, clients = utils.load_config(config) 183 | 184 | targetList = [e["alias"] if "alias" in e else e["name"] for e in targets] 185 | 186 | host = "127.0.0.1" if local else "0.0.0.0" 187 | 188 | db.init_db(path=data, createDir=True) 189 | 190 | logging.debug( 191 | f"listen on {host}:{port}, logging level: {logging_level}, data dir: {data}" 192 | ) 193 | 194 | # auto delete old data 195 | autoDelete(delete) 196 | 197 | app.run( 198 | port=port, 199 | host=host, 200 | ) 201 | 202 | 203 | if __name__ == "__main__": 204 | server() 205 | -------------------------------------------------------------------------------- /client-go/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "github.com/sirupsen/logrus" 9 | "gopkg.in/yaml.v3" 10 | "io" 11 | "net" 12 | "net/http" 13 | "os" 14 | "reflect" 15 | "time" 16 | ) 17 | 18 | const VERSION = "v1.3.1" 19 | 20 | var ( 21 | configFile = flag.String("c", "client.yaml", "Client config file.") 22 | timeout = flag.Int("t", 500, "Timeout, in milliseconds.") 23 | pack = flag.Int("p", 10, "How many packets to send in one test.") 24 | showVersion = flag.Bool("version", false, "Show version.") 25 | clientID int 26 | passw string 27 | name string 28 | log *logrus.Logger 29 | 30 | tars []Target 31 | configKeys = []string{ 32 | "name", 33 | "clientId", 34 | "passw", 35 | "uploadUrl", 36 | "targets", 37 | } 38 | targetKeys = []string{ 39 | "id", 40 | "name", 41 | "port", 42 | "addr", 43 | } 44 | ) 45 | 46 | type Config struct { 47 | Name string `yaml:"name" validate:"required"` 48 | ClientID int `yaml:"clientId" validate:"required"` 49 | Passw string `yaml:"passw" validate:"required"` 50 | UploadURL string `yaml:"uploadUrl" validate:"required"` 51 | Targets []Target `yaml:"targets" validate:"required"` 52 | } 53 | 54 | type Target struct { 55 | ID int `yaml:"id" validate:"required"` 56 | Name string `yaml:"name" validate:"required"` 57 | Port int `yaml:"port" validate:"required"` 58 | Addr string `yaml:"addr" validate:"required"` 59 | } 60 | 61 | type Data struct { 62 | ClientID int `json:"clientId"` 63 | Passw string `json:"passw"` 64 | Name string `json:"name"` 65 | Data []Metrics `json:"data"` 66 | } 67 | 68 | type Metrics struct { 69 | Name string `json:"name"` 70 | ID int `json:"id"` 71 | Time int64 `json:"time"` 72 | Delay float64 `json:"delay"` 73 | Loss float64 `json:"loss"` 74 | } 75 | 76 | func loadConfig(configFile string) (Config, error) { 77 | conf := Config{} 78 | 79 | data, err := os.ReadFile(configFile) 80 | 81 | if err != nil { 82 | return conf, err 83 | } 84 | 85 | if err := yaml.Unmarshal(data, &conf); err != nil { 86 | return conf, err 87 | } 88 | 89 | // check keys 90 | if err := validateStruct(conf, "Config"); err != nil { 91 | return conf, err 92 | } 93 | 94 | for _, value := range conf.Targets { 95 | if err := validateStruct(value, "Target"); err != nil { 96 | return conf, err 97 | } 98 | } 99 | 100 | return conf, nil 101 | } 102 | 103 | // getIPAddress takes a string parameter 'host', which can be an IP address or domain. 104 | // If 'host' is a domain, it runs a DNS query and returns the corresponding IP address. 105 | // If 'host' is an IP address, it returns it directly. 106 | // If the IP of the domain does not exist, it raises an error. 107 | func getIPAddress(host string) (string, error) { 108 | // Check if host is an IP address. 109 | if ip := net.ParseIP(host); ip != nil { 110 | return host, nil 111 | } 112 | 113 | // If not an IP address, assume it's a domain and do a DNS lookup. 114 | IPs, err := net.LookupIP(host) 115 | if err != nil { 116 | return "", fmt.Errorf("unable to resolve domain or invalid IP address: %s", host) 117 | } 118 | 119 | // Return the first resolved IP address as a string. 120 | return IPs[0].String(), nil 121 | } 122 | 123 | func tcping(host string, port int, count int, timeout time.Duration) (float64, float64, error) { 124 | delays := make([]float64, 0) 125 | lostPackets := 0 126 | 127 | ip, err := getIPAddress(host) 128 | if err != nil { 129 | return 0, 0, err 130 | } 131 | 132 | for i := 0; i < count; i++ { 133 | startTime := time.Now() 134 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), timeout) 135 | if err != nil { 136 | lostPackets++ 137 | continue 138 | } 139 | defer conn.Close() 140 | 141 | endTime := time.Now() 142 | delay := endTime.Sub(startTime).Seconds() 143 | delays = append(delays, delay) 144 | } 145 | 146 | avgDelay := calculateAvgDelay(delays) 147 | packetLoss := float64(lostPackets) / float64(count) 148 | 149 | return avgDelay, packetLoss, nil 150 | } 151 | 152 | func calculateAvgDelay(delays []float64) float64 { 153 | if len(delays) == 0 { 154 | return 0 155 | } 156 | sum := 0.0 157 | for _, delay := range delays { 158 | sum += delay 159 | } 160 | 161 | return (sum / float64(len(delays))) * 1000 162 | } 163 | 164 | func sendRequest(data Data, uploadURL string) error { 165 | jsonData, err := json.Marshal(data) 166 | if err != nil { 167 | return fmt.Errorf("Failed to marshal data to JSON: %v", err) 168 | } 169 | 170 | resp, err := http.Post(uploadURL, "application/json", bytes.NewBuffer(jsonData)) 171 | if err != nil { 172 | return fmt.Errorf("%v", err) 173 | } 174 | defer resp.Body.Close() 175 | 176 | respData, err := io.ReadAll(resp.Body) 177 | if err != nil { 178 | return fmt.Errorf("Can not parse server response: %v", err) 179 | } 180 | log.Infof("Server response: %s", string(respData)) 181 | 182 | if resp.StatusCode != http.StatusOK { 183 | return fmt.Errorf("Request failed with status code: %d", resp.StatusCode) 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func generateData(count int, timeout time.Duration) (Data, error) { 190 | data := Data{ 191 | ClientID: clientID, 192 | Passw: passw, 193 | Name: name, 194 | Data: make([]Metrics, 0), 195 | } 196 | 197 | for _, tar := range tars { 198 | res := Metrics{ 199 | Name: tar.Name, 200 | ID: tar.ID, 201 | Time: time.Now().Unix(), 202 | } 203 | 204 | delay, loss, err := tcping(tar.Addr, tar.Port, count, timeout) 205 | if err != nil { 206 | return data, err 207 | } 208 | res.Delay = delay 209 | res.Loss = loss 210 | 211 | data.Data = append(data.Data, res) 212 | } 213 | 214 | return data, nil 215 | } 216 | 217 | func validateStruct(s interface{}, name string) error { 218 | value := reflect.ValueOf(s) 219 | for i := 0; i < value.NumField(); i++ { 220 | field := value.Field(i) 221 | tag := value.Type().Field(i).Tag.Get("validate") 222 | if tag == "required" && field.IsZero() { 223 | yamlTag := value.Type().Field(i).Tag.Get("yaml") 224 | return fmt.Errorf("%s lack required field: \"%s\".", name, yamlTag) 225 | } 226 | } 227 | return nil 228 | } 229 | 230 | func main() { 231 | flag.Parse() 232 | 233 | // show version 234 | if *showVersion { 235 | fmt.Println("Ping Charts Client-go " + VERSION) 236 | fmt.Println("A simple tool to visualize vps latency based on TCP.") 237 | return 238 | } 239 | 240 | // TODO logging format 241 | // set up logging 242 | log = logrus.New() 243 | log.SetFormatter(&logrus.TextFormatter{ 244 | FullTimestamp: true, 245 | TimestampFormat: "2006-01-02 15:04:05", 246 | }) 247 | 248 | 249 | conf, err := loadConfig(*configFile) 250 | if err != nil { 251 | log.Error(err) 252 | return 253 | } 254 | 255 | clientID = conf.ClientID 256 | passw = conf.Passw 257 | name = conf.Name 258 | tars = conf.Targets 259 | 260 | data, err := generateData(10, 500*time.Millisecond) 261 | if err != nil { 262 | log.Error(err) 263 | return 264 | } 265 | err = sendRequest(data, conf.UploadURL) 266 | if err != nil { 267 | log.Error(err) 268 | return 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /web-ui/test-data/data-short: -------------------------------------------------------------------------------- 1 | { 2 | "rows":[ 3 | { 4 | "name":"mva", 5 | "label":"US", 6 | "chartDataList":[ 7 | { 8 | "delay":[ 148.4159363640679, 149.49872493743896, 144.3725347518921, 144.99189853668213, 150.65691471099854, 146.89202308654785, 144.73109775119357, 141.12334781222873, 145.2308177947998, 143.79332065582275, 158.9658498764038, 149.0121603012085, 148.93803596496582, 150.5099058151245, 151.82547569274902, 151.70354843139648, 164.56499099731445, 146.23863697052002, 153.9275884628296, 702.1387418111166, 144.77226734161377, 142.14828279283313, 149.44934844970703, 146.2186574935913, 148.65972995758057, 147.1461534500122, 152.78820991516113, 148.81210327148438, 143.37650934855142, 149.7118208143446, 150.7516860961914, 146.25113010406494, 142.38648414611816, 143.35550202263724, 148.0121374130249, 144.0770387649536, 144.6545124053955, 148.43389987945557, 140.10622766282825, 143.44453811645508, 144.4328784942627, 142.84358024597168, 145.39730548858643 ], 9 | "loss":[ 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0 ], 10 | "time":[ "2023-08-04 03:17:03", "2023-08-04 03:18:10", "2023-08-04 03:19:16", "2023-08-04 03:20:23", "2023-08-04 03:21:29", "2023-08-04 03:22:36", "2023-08-04 03:23:42", "2023-08-04 03:30:49", "2023-08-04 03:31:56", "2023-08-04 03:33:03", "2023-08-04 03:34:10", "2023-08-04 03:35:18", "2023-08-04 03:36:30", "2023-08-04 03:37:52", "2023-08-04 03:38:59", "2023-08-04 03:40:05", "2023-08-04 03:41:13", "2023-08-04 03:48:13", "2023-08-04 03:49:19", "2023-08-04 03:50:26", "2023-08-04 03:51:44", "2023-08-04 03:52:56", "2023-08-04 03:54:02", "2023-08-04 03:55:09", "2023-08-04 03:56:21", "2023-08-04 03:57:28", "2023-08-04 03:58:35", "2023-08-04 03:59:42", "2023-08-04 04:00:49", "2023-08-04 04:01:56", "2023-08-04 04:03:03", "2023-08-04 04:04:11", "2023-08-04 04:05:18", "2023-08-04 04:06:24", "2023-08-04 04:07:31", "2023-08-04 04:08:38", "2023-08-04 04:09:45", "2023-08-04 04:10:51", "2023-08-04 04:11:58", "2023-08-04 04:13:05", "2023-08-04 04:14:11", "2023-08-04 04:15:18", "2023-08-04 04:16:24" ] 11 | }, 12 | { 13 | "delay":[ 166.81373119354248, 163.02108764648438, 165.76919555664062, 170.06723880767822, 164.2078161239624, 170.34568786621094, 164.3610954284668, 167.35951900482178, 175.46727657318115, 171.06709480285645, 193.67599487304688, 669.2214488983154, 1835.5801105499268, 169.58637237548828, 165.65791765848795, 170.0634002685547, 677.4886846542358, 175.59309005737305, 170.89438438415527, 674.4474172592163, 168.60275268554688, 164.7831916809082, 168.24307441711426, 666.3554668426514, 165.11704921722412, 165.4531717300415, 175.6770372390747, 171.02131843566895, 166.5233850479126, 170.3695297241211, 168.98701190948486, 167.25234985351562, 165.87250232696533, 170.3160524368286, 170.3601360321045, 166.70053005218506, 168.97056102752686, 166.4522409439087, 169.1849946975708, 165.5790090560913, 167.25084781646729, 168.31870079040527, 164.8949384689331 ], 14 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 15 | "time":[ "2023-08-04 03:17:05", "2023-08-04 03:18:11", "2023-08-04 03:19:18", "2023-08-04 03:20:24", "2023-08-04 03:21:31", "2023-08-04 03:22:37", "2023-08-04 03:23:44", "2023-08-04 03:30:51", "2023-08-04 03:31:57", "2023-08-04 03:33:04", "2023-08-04 03:34:12", "2023-08-04 03:35:20", "2023-08-04 03:36:32", "2023-08-04 03:37:54", "2023-08-04 03:39:00", "2023-08-04 03:40:07", "2023-08-04 03:41:14", "2023-08-04 03:48:14", "2023-08-04 03:49:21", "2023-08-04 03:50:33", "2023-08-04 03:51:45", "2023-08-04 03:52:57", "2023-08-04 03:54:04", "2023-08-04 03:55:11", "2023-08-04 03:56:23", "2023-08-04 03:57:30", "2023-08-04 03:58:37", "2023-08-04 03:59:44", "2023-08-04 04:00:51", "2023-08-04 04:01:58", "2023-08-04 04:03:05", "2023-08-04 04:04:12", "2023-08-04 04:05:19", "2023-08-04 04:06:26", "2023-08-04 04:07:33", "2023-08-04 04:08:40", "2023-08-04 04:09:46", "2023-08-04 04:10:53", "2023-08-04 04:11:59", "2023-08-04 04:13:06", "2023-08-04 04:14:13", "2023-08-04 04:15:19", "2023-08-04 04:16:26" ] 16 | }, 17 | { 18 | "delay":[ 231.01942539215088, 234.1773509979248, 232.37769603729248, 223.20265769958496, 209.00111198425293, 228.62367630004883, 219.75958347320557, 227.14216709136963, 225.9751425849067, 245.84457874298096, 272.4731922149658, 241.2816286087036, 234.8088502883911, 233.08653831481934, 236.23576164245605, 259.84630584716797, 263.41280937194824, 230.23428916931152, 251.44176483154297, 223.0548858642578, 797.0458401574028, 227.28779315948486, 233.85034667121042, 253.1954050064087, 258.7684790293376, 264.1437292098999, 218.50159433152942, 243.8448190689087, 241.09601974487305, 244.67576874627005, 234.9262237548828, 240.9356435139974, 244.8103666305542, 209.89933013916016, 253.05507183074948, 234.3067169189453, 238.49256038665771, 220.56009769439697, 236.05334758758545, 242.52619743347168, 223.7908124923706, 226.89027786254883, 242.44844913482666 ], 19 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.1, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 20 | "time":[ "2023-08-04 03:17:07", "2023-08-04 03:18:13", "2023-08-04 03:19:19", "2023-08-04 03:20:26", "2023-08-04 03:21:32", "2023-08-04 03:22:39", "2023-08-04 03:23:46", "2023-08-04 03:30:52", "2023-08-04 03:31:59", "2023-08-04 03:33:06", "2023-08-04 03:34:14", "2023-08-04 03:35:26", "2023-08-04 03:36:49", "2023-08-04 03:37:55", "2023-08-04 03:39:02", "2023-08-04 03:40:09", "2023-08-04 03:41:21", "2023-08-04 03:48:16", "2023-08-04 03:49:23", "2023-08-04 03:50:40", "2023-08-04 03:51:47", "2023-08-04 03:52:59", "2023-08-04 03:54:05", "2023-08-04 03:55:17", "2023-08-04 03:56:24", "2023-08-04 03:57:31", "2023-08-04 03:58:38", "2023-08-04 03:59:45", "2023-08-04 04:00:52", "2023-08-04 04:01:59", "2023-08-04 04:03:06", "2023-08-04 04:04:14", "2023-08-04 04:05:21", "2023-08-04 04:06:28", "2023-08-04 04:07:34", "2023-08-04 04:08:41", "2023-08-04 04:09:48", "2023-08-04 04:10:54", "2023-08-04 04:12:01", "2023-08-04 04:13:08", "2023-08-04 04:14:14", "2023-08-04 04:15:21", "2023-08-04 04:16:27" ] 21 | } 22 | ] 23 | }, 24 | { 25 | "name":"wapa", 26 | "label":"HK", 27 | "chartDataList":[ 28 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 29 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 30 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 31 | ] 32 | }, 33 | { 34 | "name":"wapb", 35 | "label":"TW", 36 | "chartDataList":[ 37 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 38 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 39 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 40 | ] 41 | }, 42 | { 43 | "name":"thxa", 44 | "label":"FR", 45 | "chartDataList":[ 46 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 47 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 48 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 49 | ] 50 | } 51 | ], 52 | "targets":[ 53 | "chinanet", 54 | "chinaunicom", 55 | "chinamobile" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /web-ui/test-data/data-long: -------------------------------------------------------------------------------- 1 | { 2 | "rows":[ 3 | { 4 | "name":"mva", 5 | "label":"US", 6 | "chartDataList":[ 7 | { 8 | "delay":[ 148.4159363640679, 149.49872493743896, 144.3725347518921, 144.99189853668213, 150.65691471099854, 146.89202308654785, 144.73109775119357, 141.12334781222873, 145.2308177947998, 143.79332065582275, 158.9658498764038, 149.0121603012085, 148.93803596496582, 150.5099058151245, 151.82547569274902, 151.70354843139648, 164.56499099731445, 146.23863697052002, 153.9275884628296, 702.1387418111166, 144.77226734161377, 142.14828279283313, 149.44934844970703, 146.2186574935913, 148.65972995758057, 147.1461534500122, 152.78820991516113, 148.81210327148438, 143.37650934855142, 149.7118208143446, 150.7516860961914, 146.25113010406494, 142.38648414611816, 143.35550202263724, 148.0121374130249, 144.0770387649536, 144.6545124053955, 148.43389987945557, 140.10622766282825, 143.44453811645508, 144.4328784942627, 142.84358024597168, 145.39730548858643 ], 9 | "loss":[ 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0 ], 10 | "time":[ "2023-08-04 03:17:03", "2023-08-04 03:18:10", "2023-08-04 03:19:16", "2023-08-04 03:20:23", "2023-08-04 03:21:29", "2023-08-04 03:22:36", "2023-08-04 03:23:42", "2023-08-04 03:30:49", "2023-08-04 03:31:56", "2023-08-04 03:33:03", "2023-08-04 03:34:10", "2023-08-04 03:35:18", "2023-08-04 03:36:30", "2023-08-04 03:37:52", "2023-08-04 03:38:59", "2023-08-04 03:40:05", "2023-08-04 03:41:13", "2023-08-04 03:48:13", "2023-08-04 03:49:19", "2023-08-04 03:50:26", "2023-08-04 03:51:44", "2023-08-04 03:52:56", "2023-08-04 03:54:02", "2023-08-04 03:55:09", "2023-08-04 03:56:21", "2023-08-04 03:57:28", "2023-08-04 03:58:35", "2023-08-04 03:59:42", "2023-08-04 04:00:49", "2023-08-04 04:01:56", "2023-08-04 04:03:03", "2023-08-04 04:04:11", "2023-08-04 04:05:18", "2023-08-04 04:06:24", "2023-08-04 04:07:31", "2023-08-04 04:08:38", "2023-08-04 04:09:45", "2023-08-04 04:10:51", "2023-08-04 04:11:58", "2023-08-04 04:13:05", "2023-08-04 04:14:11", "2023-08-04 04:15:18", "2023-08-04 04:16:24" ] 11 | }, 12 | { 13 | "delay":[ 166.81373119354248, 163.02108764648438, 165.76919555664062, 170.06723880767822, 164.2078161239624, 170.34568786621094, 164.3610954284668, 167.35951900482178, 175.46727657318115, 171.06709480285645, 193.67599487304688, 669.2214488983154, 1835.5801105499268, 169.58637237548828, 165.65791765848795, 170.0634002685547, 677.4886846542358, 175.59309005737305, 170.89438438415527, 674.4474172592163, 168.60275268554688, 164.7831916809082, 168.24307441711426, 666.3554668426514, 165.11704921722412, 165.4531717300415, 175.6770372390747, 171.02131843566895, 166.5233850479126, 170.3695297241211, 168.98701190948486, 167.25234985351562, 165.87250232696533, 170.3160524368286, 170.3601360321045, 166.70053005218506, 168.97056102752686, 166.4522409439087, 169.1849946975708, 165.5790090560913, 167.25084781646729, 168.31870079040527, 164.8949384689331 ], 14 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 15 | "time":[ "2023-08-04 03:17:05", "2023-08-04 03:18:11", "2023-08-04 03:19:18", "2023-08-04 03:20:24", "2023-08-04 03:21:31", "2023-08-04 03:22:37", "2023-08-04 03:23:44", "2023-08-04 03:30:51", "2023-08-04 03:31:57", "2023-08-04 03:33:04", "2023-08-04 03:34:12", "2023-08-04 03:35:20", "2023-08-04 03:36:32", "2023-08-04 03:37:54", "2023-08-04 03:39:00", "2023-08-04 03:40:07", "2023-08-04 03:41:14", "2023-08-04 03:48:14", "2023-08-04 03:49:21", "2023-08-04 03:50:33", "2023-08-04 03:51:45", "2023-08-04 03:52:57", "2023-08-04 03:54:04", "2023-08-04 03:55:11", "2023-08-04 03:56:23", "2023-08-04 03:57:30", "2023-08-04 03:58:37", "2023-08-04 03:59:44", "2023-08-04 04:00:51", "2023-08-04 04:01:58", "2023-08-04 04:03:05", "2023-08-04 04:04:12", "2023-08-04 04:05:19", "2023-08-04 04:06:26", "2023-08-04 04:07:33", "2023-08-04 04:08:40", "2023-08-04 04:09:46", "2023-08-04 04:10:53", "2023-08-04 04:11:59", "2023-08-04 04:13:06", "2023-08-04 04:14:13", "2023-08-04 04:15:19", "2023-08-04 04:16:26" ] 16 | }, 17 | { 18 | "delay":[ 231.01942539215088, 234.1773509979248, 232.37769603729248, 223.20265769958496, 209.00111198425293, 228.62367630004883, 219.75958347320557, 227.14216709136963, 225.9751425849067, 245.84457874298096, 272.4731922149658, 241.2816286087036, 234.8088502883911, 233.08653831481934, 236.23576164245605, 259.84630584716797, 263.41280937194824, 230.23428916931152, 251.44176483154297, 223.0548858642578, 797.0458401574028, 227.28779315948486, 233.85034667121042, 253.1954050064087, 258.7684790293376, 264.1437292098999, 218.50159433152942, 243.8448190689087, 241.09601974487305, 244.67576874627005, 234.9262237548828, 240.9356435139974, 244.8103666305542, 209.89933013916016, 253.05507183074948, 234.3067169189453, 238.49256038665771, 220.56009769439697, 236.05334758758545, 242.52619743347168, 223.7908124923706, 226.89027786254883, 242.44844913482666 ], 19 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.1, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 20 | "time":[ "2023-08-04 03:17:07", "2023-08-04 03:18:13", "2023-08-04 03:19:19", "2023-08-04 03:20:26", "2023-08-04 03:21:32", "2023-08-04 03:22:39", "2023-08-04 03:23:46", "2023-08-04 03:30:52", "2023-08-04 03:31:59", "2023-08-04 03:33:06", "2023-08-04 03:34:14", "2023-08-04 03:35:26", "2023-08-04 03:36:49", "2023-08-04 03:37:55", "2023-08-04 03:39:02", "2023-08-04 03:40:09", "2023-08-04 03:41:21", "2023-08-04 03:48:16", "2023-08-04 03:49:23", "2023-08-04 03:50:40", "2023-08-04 03:51:47", "2023-08-04 03:52:59", "2023-08-04 03:54:05", "2023-08-04 03:55:17", "2023-08-04 03:56:24", "2023-08-04 03:57:31", "2023-08-04 03:58:38", "2023-08-04 03:59:45", "2023-08-04 04:00:52", "2023-08-04 04:01:59", "2023-08-04 04:03:06", "2023-08-04 04:04:14", "2023-08-04 04:05:21", "2023-08-04 04:06:28", "2023-08-04 04:07:34", "2023-08-04 04:08:41", "2023-08-04 04:09:48", "2023-08-04 04:10:54", "2023-08-04 04:12:01", "2023-08-04 04:13:08", "2023-08-04 04:14:14", "2023-08-04 04:15:21", "2023-08-04 04:16:27" ] 21 | }, 22 | { 23 | "delay":[ 148.4159363640679, 149.49872493743896, 144.3725347518921, 144.99189853668213, 150.65691471099854, 146.89202308654785, 144.73109775119357, 141.12334781222873, 145.2308177947998, 143.79332065582275, 158.9658498764038, 149.0121603012085, 148.93803596496582, 150.5099058151245, 151.82547569274902, 151.70354843139648, 164.56499099731445, 146.23863697052002, 153.9275884628296, 702.1387418111166, 144.77226734161377, 142.14828279283313, 149.44934844970703, 146.2186574935913, 148.65972995758057, 147.1461534500122, 152.78820991516113, 148.81210327148438, 143.37650934855142, 149.7118208143446, 150.7516860961914, 146.25113010406494, 142.38648414611816, 143.35550202263724, 148.0121374130249, 144.0770387649536, 144.6545124053955, 148.43389987945557, 140.10622766282825, 143.44453811645508, 144.4328784942627, 142.84358024597168, 145.39730548858643 ], 24 | "loss":[ 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0 ], 25 | "time":[ "2023-08-04 03:17:03", "2023-08-04 03:18:10", "2023-08-04 03:19:16", "2023-08-04 03:20:23", "2023-08-04 03:21:29", "2023-08-04 03:22:36", "2023-08-04 03:23:42", "2023-08-04 03:30:49", "2023-08-04 03:31:56", "2023-08-04 03:33:03", "2023-08-04 03:34:10", "2023-08-04 03:35:18", "2023-08-04 03:36:30", "2023-08-04 03:37:52", "2023-08-04 03:38:59", "2023-08-04 03:40:05", "2023-08-04 03:41:13", "2023-08-04 03:48:13", "2023-08-04 03:49:19", "2023-08-04 03:50:26", "2023-08-04 03:51:44", "2023-08-04 03:52:56", "2023-08-04 03:54:02", "2023-08-04 03:55:09", "2023-08-04 03:56:21", "2023-08-04 03:57:28", "2023-08-04 03:58:35", "2023-08-04 03:59:42", "2023-08-04 04:00:49", "2023-08-04 04:01:56", "2023-08-04 04:03:03", "2023-08-04 04:04:11", "2023-08-04 04:05:18", "2023-08-04 04:06:24", "2023-08-04 04:07:31", "2023-08-04 04:08:38", "2023-08-04 04:09:45", "2023-08-04 04:10:51", "2023-08-04 04:11:58", "2023-08-04 04:13:05", "2023-08-04 04:14:11", "2023-08-04 04:15:18", "2023-08-04 04:16:24" ] 26 | }, 27 | { 28 | "delay":[ 166.81373119354248, 163.02108764648438, 165.76919555664062, 170.06723880767822, 164.2078161239624, 170.34568786621094, 164.3610954284668, 167.35951900482178, 175.46727657318115, 171.06709480285645, 193.67599487304688, 669.2214488983154, 1835.5801105499268, 169.58637237548828, 165.65791765848795, 170.0634002685547, 677.4886846542358, 175.59309005737305, 170.89438438415527, 674.4474172592163, 168.60275268554688, 164.7831916809082, 168.24307441711426, 666.3554668426514, 165.11704921722412, 165.4531717300415, 175.6770372390747, 171.02131843566895, 166.5233850479126, 170.3695297241211, 168.98701190948486, 167.25234985351562, 165.87250232696533, 170.3160524368286, 170.3601360321045, 166.70053005218506, 168.97056102752686, 166.4522409439087, 169.1849946975708, 165.5790090560913, 167.25084781646729, 168.31870079040527, 164.8949384689331 ], 29 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 30 | "time":[ "2023-08-04 03:17:05", "2023-08-04 03:18:11", "2023-08-04 03:19:18", "2023-08-04 03:20:24", "2023-08-04 03:21:31", "2023-08-04 03:22:37", "2023-08-04 03:23:44", "2023-08-04 03:30:51", "2023-08-04 03:31:57", "2023-08-04 03:33:04", "2023-08-04 03:34:12", "2023-08-04 03:35:20", "2023-08-04 03:36:32", "2023-08-04 03:37:54", "2023-08-04 03:39:00", "2023-08-04 03:40:07", "2023-08-04 03:41:14", "2023-08-04 03:48:14", "2023-08-04 03:49:21", "2023-08-04 03:50:33", "2023-08-04 03:51:45", "2023-08-04 03:52:57", "2023-08-04 03:54:04", "2023-08-04 03:55:11", "2023-08-04 03:56:23", "2023-08-04 03:57:30", "2023-08-04 03:58:37", "2023-08-04 03:59:44", "2023-08-04 04:00:51", "2023-08-04 04:01:58", "2023-08-04 04:03:05", "2023-08-04 04:04:12", "2023-08-04 04:05:19", "2023-08-04 04:06:26", "2023-08-04 04:07:33", "2023-08-04 04:08:40", "2023-08-04 04:09:46", "2023-08-04 04:10:53", "2023-08-04 04:11:59", "2023-08-04 04:13:06", "2023-08-04 04:14:13", "2023-08-04 04:15:19", "2023-08-04 04:16:26" ] 31 | }, 32 | { 33 | "delay":[ 231.01942539215088, 234.1773509979248, 232.37769603729248, 223.20265769958496, 209.00111198425293, 228.62367630004883, 219.75958347320557, 227.14216709136963, 225.9751425849067, 245.84457874298096, 272.4731922149658, 241.2816286087036, 234.8088502883911, 233.08653831481934, 236.23576164245605, 259.84630584716797, 263.41280937194824, 230.23428916931152, 251.44176483154297, 223.0548858642578, 797.0458401574028, 227.28779315948486, 233.85034667121042, 253.1954050064087, 258.7684790293376, 264.1437292098999, 218.50159433152942, 243.8448190689087, 241.09601974487305, 244.67576874627005, 234.9262237548828, 240.9356435139974, 244.8103666305542, 209.89933013916016, 253.05507183074948, 234.3067169189453, 238.49256038665771, 220.56009769439697, 236.05334758758545, 242.52619743347168, 223.7908124923706, 226.89027786254883, 242.44844913482666 ], 34 | "loss":[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.1, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], 35 | "time":[ "2023-08-04 03:17:07", "2023-08-04 03:18:13", "2023-08-04 03:19:19", "2023-08-04 03:20:26", "2023-08-04 03:21:32", "2023-08-04 03:22:39", "2023-08-04 03:23:46", "2023-08-04 03:30:52", "2023-08-04 03:31:59", "2023-08-04 03:33:06", "2023-08-04 03:34:14", "2023-08-04 03:35:26", "2023-08-04 03:36:49", "2023-08-04 03:37:55", "2023-08-04 03:39:02", "2023-08-04 03:40:09", "2023-08-04 03:41:21", "2023-08-04 03:48:16", "2023-08-04 03:49:23", "2023-08-04 03:50:40", "2023-08-04 03:51:47", "2023-08-04 03:52:59", "2023-08-04 03:54:05", "2023-08-04 03:55:17", "2023-08-04 03:56:24", "2023-08-04 03:57:31", "2023-08-04 03:58:38", "2023-08-04 03:59:45", "2023-08-04 04:00:52", "2023-08-04 04:01:59", "2023-08-04 04:03:06", "2023-08-04 04:04:14", "2023-08-04 04:05:21", "2023-08-04 04:06:28", "2023-08-04 04:07:34", "2023-08-04 04:08:41", "2023-08-04 04:09:48", "2023-08-04 04:10:54", "2023-08-04 04:12:01", "2023-08-04 04:13:08", "2023-08-04 04:14:14", "2023-08-04 04:15:21", "2023-08-04 04:16:27" ] 36 | } 37 | ] 38 | }, 39 | { 40 | "name":"wapa", 41 | "label":"HK", 42 | "chartDataList":[ 43 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 44 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 45 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 46 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 47 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 48 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 49 | ] 50 | }, 51 | { 52 | "name":"wapb", 53 | "label":"TW", 54 | "chartDataList":[ 55 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 56 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 57 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 58 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 59 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 60 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 61 | ] 62 | }, 63 | { 64 | "name":"thxa", 65 | "label":"FR", 66 | "chartDataList":[ 67 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 68 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 69 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 70 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 71 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] }, 72 | { "delay":[ 0 ], "loss":[ 0 ], "time":[ 0 ] } 73 | ] 74 | } 75 | ], 76 | "targets":[ 77 | "chinanet", 78 | "chinaunicom", 79 | "chinamobile", 80 | "chinanet", 81 | "chinaunicom", 82 | "chinamobile" 83 | ] 84 | } 85 | --------------------------------------------------------------------------------