├── 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 |
12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
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 |
2 |
3 |
4 |
{{ info }}
5 |
6 |
7 | {{ item.label }}
8 |
9 |
10 |
11 |
12 |
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 |
2 |
3 |
Ping Charts
4 |
6 |
8 |
9 |
10 |
11 |
12 |
13 | Name
14 | Label
15 |
16 | {{ target }}
17 |
18 |
19 |
20 |
21 |
22 | {{ row.name }}
23 | {{ row.label }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
2 |
3 |
4 | {{ averageDelay }}ms
5 |
6 |
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 |
--------------------------------------------------------------------------------