├── .env.sample ├── .gitignore ├── README.md ├── docker-compose.yml ├── grafana ├── dashboards │ ├── dashboard.yml │ └── network-speedtest.json └── datasources │ └── influxdb.yml ├── influxdb └── .gitkeep ├── screenshots └── Network-Speedtest.jpg └── speedtest ├── Dockerfile └── src ├── index.js ├── package-lock.json └── package.json /.env.sample: -------------------------------------------------------------------------------- 1 | CRON_CONFIG='*/2 * * * *' # Interval between speedtests, this sample set up to every 2 mins. 2 | SPEEDTEST_HOST='Home' # Display name of the client 3 | # SPEEDTEST_SERVER_ID=none # Optionally set specific speedtest.net server ID, otherwise use the closest 4 | 5 | GRAFANA_PORT=3000 # Port to bind Grafana webinterface on the host system 6 | GF_AUTH_ANONYMOUS_ENABLED=false 7 | 8 | INFLUXDB_DB=network_speed # Database to save speedtest results 9 | INFLUXDB_ADMIN_USER=root # Username for InfluxDB authentication 10 | INFLUXDB_ADMIN_PASSWORD=Secret123 # Password for InfluxDB authentication 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | influxdb/* 2 | !influxdb/.gitkeep 3 | 4 | **/node_modules 5 | **/data 6 | **/@eaDir 7 | 8 | .env 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Speed Monitor Docker 2 | 3 | A network speed monitoring stack that built with InfluxDB, Grafana and Speedtest CLI 4 | 5 | ## Prerequisite 6 | - Docker & Docker-compose installed 7 | - Git installed 8 | 9 | ## Getting started 10 | - Clone this repo 11 | - Update `.env` file following the guide below 12 | - Run `docker-compose up -d` 13 | - Visit `http://localhost:` to view the statistics. The default credentials are `admin:admin`, Grafana will ask you to change your password the first time 14 | 15 | ## Update env file 16 | - Run `cp .env.sample .env` and change the environment variables as you wish in `.env` 17 | - Get `SPEEDTEST_SERVER_ID` value from [this link](https://sparanoid.com/lab/speedtest-list/) 18 | - Config `CRON_CONFIG` following [node-cron syntax](https://www.npmjs.com/package/node-cron). I recommend to start with 1 minute to see the first few test quickly, then finally increase it to your desired time. Personally, I use `1 * * * *` (each hour) as I think it would be more than enough. Every time you change the `.env` file, run `docker-compose up -d --build` to rebuild the image. 19 | 20 | ## Showcase 21 | ![](./screenshots/Network-Speedtest.jpg) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | influxdb: 5 | image: influxdb:1.8.4 6 | restart: always 7 | ports: 8 | - 8086:8086 9 | volumes: 10 | - ./influxdb:/var/lib/influxdb 11 | environment: 12 | - INFLUXDB_ADMIN_USER=${INFLUXDB_ADMIN_USER} 13 | - INFLUXDB_ADMIN_PASSWORD=${INFLUXDB_ADMIN_PASSWORD} 14 | - INFLUXDB_DB=${INFLUXDB_DB} 15 | 16 | grafana: 17 | restart: always 18 | image: grafana/grafana:7.5.4 19 | volumes: 20 | - ./grafana:/etc/grafana/provisioning 21 | ports: 22 | - ${GRAFANA_PORT}:3000 23 | environment: 24 | - GF_AUTH_ANONYMOUS_ENABLED=${GF_AUTH_ANONYMOUS_ENABLED} 25 | depends_on: 26 | - influxdb 27 | 28 | speedtest: 29 | restart: always 30 | network_mode: host 31 | build: ./speedtest 32 | image: turbothinh/network-speedtest:latest 33 | environment: 34 | - CRON_CONFIG=${CRON_CONFIG} 35 | - SPEEDTEST_HOST=${SPEEDTEST_HOST} 36 | - SPEEDTEST_SERVER_ID=${SPEEDTEST_SERVER_ID} 37 | - INFLUXDB_DB=${INFLUXDB_DB} 38 | - INFLUXDB_ADMIN_USER=${INFLUXDB_ADMIN_USER} 39 | - INFLUXDB_ADMIN_PASSWORD=${INFLUXDB_ADMIN_PASSWORD} 40 | depends_on: 41 | - influxdb 42 | # Only enable this volume in development, otherwise host volume will override docker work folder 43 | # volumes: 44 | # - ./speedtest/src:/src 45 | -------------------------------------------------------------------------------- /grafana/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | allowUiUpdates: true 11 | options: 12 | path: /etc/grafana/provisioning/dashboards 13 | -------------------------------------------------------------------------------- /grafana/dashboards/network-speedtest.json: -------------------------------------------------------------------------------- 1 | { 2 | "timezone": "Europe/Copenhagen", 3 | "title": "Network Speedtest", 4 | "uid": "pZrFG2uGz", 5 | "version": 2, 6 | "description": "", 7 | "editable": true, 8 | "gnetId": null, 9 | "graphTooltip": 2, 10 | "links": [], 11 | "panels": [ 12 | { 13 | "cacheTimeout": null, 14 | "datasource": "InfluxDB", 15 | "fieldConfig": { 16 | "defaults": { 17 | "mappings": [], 18 | "min": 0, 19 | "thresholds": { 20 | "mode": "absolute", 21 | "steps": [ 22 | { 23 | "color": "red", 24 | "value": null 25 | }, 26 | { 27 | "color": "yellow", 28 | "value": 200 29 | }, 30 | { 31 | "color": "green", 32 | "value": 500 33 | } 34 | ] 35 | }, 36 | "unit": "Mbits" 37 | }, 38 | "overrides": [] 39 | }, 40 | "gridPos": { 41 | "h": 9, 42 | "w": 6, 43 | "x": 0, 44 | "y": 0 45 | }, 46 | "hideTimeOverride": false, 47 | "id": 10, 48 | "links": [], 49 | "options": { 50 | "orientation": "horizontal", 51 | "reduceOptions": { 52 | "calcs": [ 53 | "mean" 54 | ], 55 | "fields": "", 56 | "values": false 57 | }, 58 | "showThresholdLabels": true, 59 | "showThresholdMarkers": true, 60 | "text": {} 61 | }, 62 | "pluginVersion": "7.5.4", 63 | "repeat": null, 64 | "repeatDirection": "h", 65 | "targets": [ 66 | { 67 | "alias": "Download", 68 | "groupBy": [ 69 | { 70 | "params": [ 71 | "$__interval" 72 | ], 73 | "type": "time" 74 | }, 75 | { 76 | "params": [ 77 | "null" 78 | ], 79 | "type": "fill" 80 | } 81 | ], 82 | "hide": false, 83 | "measurement": "download", 84 | "orderByTime": "ASC", 85 | "policy": "default", 86 | "query": "SELECT mean(\"value\") FROM \"download\" WHERE (\"location\" = Home) AND $timeFilter GROUP BY time($__interval)", 87 | "rawQuery": false, 88 | "refId": "A", 89 | "resultFormat": "time_series", 90 | "select": [ 91 | [ 92 | { 93 | "params": [ 94 | "value" 95 | ], 96 | "type": "field" 97 | }, 98 | { 99 | "params": [], 100 | "type": "mean" 101 | } 102 | ] 103 | ], 104 | "tags": [ 105 | { 106 | "key": "host", 107 | "operator": "=", 108 | "value": "Home" 109 | } 110 | ] 111 | } 112 | ], 113 | "timeFrom": "1w", 114 | "timeShift": null, 115 | "title": "Download avarage", 116 | "type": "gauge" 117 | }, 118 | { 119 | "cacheTimeout": null, 120 | "datasource": "InfluxDB", 121 | "fieldConfig": { 122 | "defaults": { 123 | "mappings": [], 124 | "min": 0, 125 | "thresholds": { 126 | "mode": "absolute", 127 | "steps": [ 128 | { 129 | "color": "red", 130 | "value": null 131 | }, 132 | { 133 | "color": "yellow", 134 | "value": 200 135 | }, 136 | { 137 | "color": "green", 138 | "value": 350 139 | } 140 | ] 141 | }, 142 | "unit": "Mbits" 143 | }, 144 | "overrides": [] 145 | }, 146 | "gridPos": { 147 | "h": 9, 148 | "w": 6, 149 | "x": 6, 150 | "y": 0 151 | }, 152 | "id": 8, 153 | "links": [], 154 | "options": { 155 | "orientation": "horizontal", 156 | "reduceOptions": { 157 | "calcs": [ 158 | "mean" 159 | ], 160 | "fields": "", 161 | "values": false 162 | }, 163 | "showThresholdLabels": true, 164 | "showThresholdMarkers": true, 165 | "text": {} 166 | }, 167 | "pluginVersion": "7.5.4", 168 | "targets": [ 169 | { 170 | "alias": "Upload", 171 | "groupBy": [ 172 | { 173 | "params": [ 174 | "$__interval" 175 | ], 176 | "type": "time" 177 | }, 178 | { 179 | "params": [ 180 | "null" 181 | ], 182 | "type": "fill" 183 | } 184 | ], 185 | "hide": false, 186 | "measurement": "upload", 187 | "orderByTime": "ASC", 188 | "policy": "default", 189 | "query": "SELECT mean(\"value\") FROM \"upload\" WHERE (\"location\" = Home) AND $timeFilter GROUP BY time($__interval)", 190 | "rawQuery": false, 191 | "refId": "A", 192 | "resultFormat": "time_series", 193 | "select": [ 194 | [ 195 | { 196 | "params": [ 197 | "value" 198 | ], 199 | "type": "field" 200 | }, 201 | { 202 | "params": [], 203 | "type": "mean" 204 | } 205 | ] 206 | ], 207 | "tags": [ 208 | { 209 | "key": "host", 210 | "operator": "=", 211 | "value": "Home" 212 | } 213 | ] 214 | } 215 | ], 216 | "timeFrom": "1w", 217 | "timeShift": null, 218 | "title": "Upload avarage", 219 | "type": "gauge" 220 | }, 221 | { 222 | "cacheTimeout": null, 223 | "datasource": "InfluxDB", 224 | "fieldConfig": { 225 | "defaults": { 226 | "color": { 227 | "mode": "thresholds" 228 | }, 229 | "mappings": [], 230 | "thresholds": { 231 | "mode": "absolute", 232 | "steps": [ 233 | { 234 | "color": "green", 235 | "value": null 236 | }, 237 | { 238 | "color": "red", 239 | "value": 80 240 | } 241 | ] 242 | } 243 | }, 244 | "overrides": [] 245 | }, 246 | "gridPos": { 247 | "h": 9, 248 | "w": 6, 249 | "x": 12, 250 | "y": 0 251 | }, 252 | "id": 6, 253 | "links": [], 254 | "options": { 255 | "colorMode": "value", 256 | "graphMode": "area", 257 | "justifyMode": "auto", 258 | "orientation": "auto", 259 | "reduceOptions": { 260 | "calcs": [ 261 | "lastNotNull" 262 | ], 263 | "fields": "", 264 | "values": false 265 | }, 266 | "text": {}, 267 | "textMode": "auto" 268 | }, 269 | "pluginVersion": "7.5.4", 270 | "targets": [ 271 | { 272 | "alias": "Ping", 273 | "groupBy": [ 274 | { 275 | "params": [ 276 | "$__interval" 277 | ], 278 | "type": "time" 279 | }, 280 | { 281 | "params": [ 282 | "null" 283 | ], 284 | "type": "fill" 285 | } 286 | ], 287 | "hide": false, 288 | "measurement": "ping", 289 | "orderByTime": "ASC", 290 | "policy": "default", 291 | "query": "SELECT mean(\"value\") FROM \"download\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", 292 | "rawQuery": false, 293 | "refId": "A", 294 | "resultFormat": "time_series", 295 | "select": [ 296 | [ 297 | { 298 | "params": [ 299 | "value" 300 | ], 301 | "type": "field" 302 | }, 303 | { 304 | "params": [], 305 | "type": "mean" 306 | } 307 | ] 308 | ], 309 | "tags": [ 310 | { 311 | "key": "host", 312 | "operator": "=", 313 | "value": "Home" 314 | } 315 | ] 316 | } 317 | ], 318 | "timeFrom": "1w", 319 | "timeShift": null, 320 | "title": "Average ping latency", 321 | "type": "stat" 322 | }, 323 | { 324 | "datasource": "InfluxDB", 325 | "fieldConfig": { 326 | "defaults": { 327 | "color": { 328 | "mode": "thresholds" 329 | }, 330 | "custom": { 331 | "align": "center", 332 | "displayMode": "color-text", 333 | "filterable": false 334 | }, 335 | "mappings": [], 336 | "thresholds": { 337 | "mode": "absolute", 338 | "steps": [ 339 | { 340 | "color": "green", 341 | "value": null 342 | } 343 | ] 344 | } 345 | }, 346 | "overrides": [] 347 | }, 348 | "gridPos": { 349 | "h": 9, 350 | "w": 6, 351 | "x": 18, 352 | "y": 0 353 | }, 354 | "id": 14, 355 | "options": { 356 | "showHeader": false 357 | }, 358 | "pluginVersion": "7.5.4", 359 | "targets": [ 360 | { 361 | "alias": "Public IP", 362 | "groupBy": [ 363 | { 364 | "params": [ 365 | "$__interval" 366 | ], 367 | "type": "time" 368 | }, 369 | { 370 | "params": [ 371 | "null" 372 | ], 373 | "type": "fill" 374 | } 375 | ], 376 | "measurement": "myExternalIp", 377 | "orderByTime": "ASC", 378 | "policy": "default", 379 | "query": "SELECT last(value) FROM \"isp\" ,\"myExternalIp\" WHERE (\"host\" = 'Home') AND $timeFilter", 380 | "queryType": "randomWalk", 381 | "rawQuery": true, 382 | "refId": "A", 383 | "resultFormat": "table", 384 | "select": [ 385 | [ 386 | { 387 | "params": [ 388 | "value" 389 | ], 390 | "type": "field" 391 | }, 392 | { 393 | "params": [], 394 | "type": "mean" 395 | } 396 | ] 397 | ], 398 | "tags": [ 399 | { 400 | "key": "host", 401 | "operator": "=", 402 | "value": "Home" 403 | } 404 | ] 405 | } 406 | ], 407 | "timeFrom": null, 408 | "timeShift": null, 409 | "title": "Internet Info", 410 | "transformations": [ 411 | { 412 | "id": "organize", 413 | "options": { 414 | "excludeByName": { 415 | "Time": true, 416 | "last": false 417 | }, 418 | "indexByName": {}, 419 | "renameByName": { 420 | "Time": "" 421 | } 422 | } 423 | } 424 | ], 425 | "type": "table" 426 | }, 427 | { 428 | "aliasColors": {}, 429 | "bars": false, 430 | "dashLength": 10, 431 | "dashes": false, 432 | "datasource": "InfluxDB", 433 | "fieldConfig": { 434 | "defaults": { 435 | "unit": "Mbits" 436 | }, 437 | "overrides": [] 438 | }, 439 | "fill": 1, 440 | "fillGradient": 0, 441 | "gridPos": { 442 | "h": 10, 443 | "w": 12, 444 | "x": 0, 445 | "y": 9 446 | }, 447 | "hiddenSeries": false, 448 | "id": 4, 449 | "legend": { 450 | "alignAsTable": false, 451 | "avg": false, 452 | "current": false, 453 | "max": false, 454 | "min": false, 455 | "show": true, 456 | "total": false, 457 | "values": false 458 | }, 459 | "lines": true, 460 | "linewidth": 2, 461 | "links": [], 462 | "nullPointMode": "connected", 463 | "options": { 464 | "alertThreshold": true 465 | }, 466 | "percentage": false, 467 | "pluginVersion": "7.5.4", 468 | "pointradius": 5, 469 | "points": false, 470 | "renderer": "flot", 471 | "seriesOverrides": [], 472 | "spaceLength": 10, 473 | "stack": false, 474 | "steppedLine": false, 475 | "targets": [ 476 | { 477 | "alias": "Download", 478 | "groupBy": [ 479 | { 480 | "params": [ 481 | "$__interval" 482 | ], 483 | "type": "time" 484 | }, 485 | { 486 | "params": [ 487 | "null" 488 | ], 489 | "type": "fill" 490 | } 491 | ], 492 | "measurement": "download", 493 | "orderByTime": "ASC", 494 | "policy": "default", 495 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE (\"location\" = Home) AND $timeFilter GROUP BY time($__interval) fill(null)", 496 | "rawQuery": false, 497 | "refId": "A", 498 | "resultFormat": "time_series", 499 | "select": [ 500 | [ 501 | { 502 | "params": [ 503 | "value" 504 | ], 505 | "type": "field" 506 | }, 507 | { 508 | "params": [], 509 | "type": "mean" 510 | } 511 | ] 512 | ], 513 | "tags": [ 514 | { 515 | "key": "host", 516 | "operator": "=", 517 | "value": "Home" 518 | } 519 | ] 520 | }, 521 | { 522 | "alias": "Upload", 523 | "groupBy": [ 524 | { 525 | "params": [ 526 | "$__interval" 527 | ], 528 | "type": "time" 529 | }, 530 | { 531 | "params": [ 532 | "null" 533 | ], 534 | "type": "fill" 535 | } 536 | ], 537 | "measurement": "upload", 538 | "orderByTime": "ASC", 539 | "policy": "default", 540 | "query": "SELECT mean(\"value\") FROM \"upload\" WHERE \"host\" =~ /^[[host]]$/ and $timeFilter GROUP BY time($interval) fill(null)", 541 | "rawQuery": false, 542 | "refId": "B", 543 | "resultFormat": "time_series", 544 | "select": [ 545 | [ 546 | { 547 | "params": [ 548 | "value" 549 | ], 550 | "type": "field" 551 | }, 552 | { 553 | "params": [], 554 | "type": "mean" 555 | } 556 | ] 557 | ], 558 | "tags": [ 559 | { 560 | "key": "host", 561 | "operator": "=", 562 | "value": "Home" 563 | } 564 | ] 565 | } 566 | ], 567 | "thresholds": [ 568 | { 569 | "colorMode": "critical", 570 | "fill": false, 571 | "line": true, 572 | "op": "gt", 573 | "value": 200, 574 | "yaxis": "left" 575 | }, 576 | { 577 | "colorMode": "custom", 578 | "fill": false, 579 | "fillColor": "#73BF69", 580 | "line": true, 581 | "lineColor": "#73BF69", 582 | "op": "gt", 583 | "value": 700, 584 | "yaxis": "left" 585 | } 586 | ], 587 | "timeFrom": null, 588 | "timeRegions": [], 589 | "timeShift": null, 590 | "title": "Upload / Download", 591 | "tooltip": { 592 | "shared": true, 593 | "sort": 0, 594 | "value_type": "individual" 595 | }, 596 | "type": "graph", 597 | "xaxis": { 598 | "buckets": null, 599 | "mode": "time", 600 | "name": null, 601 | "show": true, 602 | "values": [] 603 | }, 604 | "yaxes": [ 605 | { 606 | "format": "Mbits", 607 | "label": null, 608 | "logBase": 1, 609 | "max": null, 610 | "min": null, 611 | "show": true 612 | }, 613 | { 614 | "format": "Mbits", 615 | "label": null, 616 | "logBase": 1, 617 | "max": null, 618 | "min": null, 619 | "show": false 620 | } 621 | ], 622 | "yaxis": { 623 | "align": false, 624 | "alignLevel": null 625 | } 626 | }, 627 | { 628 | "datasource": "InfluxDB", 629 | "description": "", 630 | "fieldConfig": { 631 | "defaults": { 632 | "color": { 633 | "mode": "palette-classic" 634 | }, 635 | "custom": { 636 | "axisLabel": "", 637 | "axisPlacement": "left", 638 | "barAlignment": 0, 639 | "drawStyle": "line", 640 | "fillOpacity": 40, 641 | "gradientMode": "none", 642 | "hideFrom": { 643 | "graph": false, 644 | "legend": false, 645 | "tooltip": false 646 | }, 647 | "lineInterpolation": "linear", 648 | "lineStyle": { 649 | "fill": "solid" 650 | }, 651 | "lineWidth": 1, 652 | "pointSize": 2, 653 | "scaleDistribution": { 654 | "type": "linear" 655 | }, 656 | "showPoints": "always", 657 | "spanNulls": true 658 | }, 659 | "mappings": [], 660 | "thresholds": { 661 | "mode": "absolute", 662 | "steps": [ 663 | { 664 | "color": "green", 665 | "value": null 666 | }, 667 | { 668 | "color": "red", 669 | "value": 5 670 | } 671 | ] 672 | }, 673 | "unit": "ms" 674 | }, 675 | "overrides": [] 676 | }, 677 | "gridPos": { 678 | "h": 10, 679 | "w": 12, 680 | "x": 12, 681 | "y": 9 682 | }, 683 | "id": 2, 684 | "links": [], 685 | "options": { 686 | "graph": {}, 687 | "legend": { 688 | "calcs": [], 689 | "displayMode": "list", 690 | "placement": "bottom" 691 | }, 692 | "tooltipOptions": { 693 | "mode": "single" 694 | } 695 | }, 696 | "pluginVersion": "7.5.4", 697 | "targets": [ 698 | { 699 | "alias": "Ping", 700 | "groupBy": [ 701 | { 702 | "params": [ 703 | "$__interval" 704 | ], 705 | "type": "time" 706 | }, 707 | { 708 | "params": [ 709 | "null" 710 | ], 711 | "type": "fill" 712 | } 713 | ], 714 | "measurement": "ping", 715 | "orderByTime": "ASC", 716 | "policy": "default", 717 | "query": "SELECT mean(\"value\") FROM \"ping\" WHERE $timeFilter GROUP BY time($interval) fill(null)", 718 | "rawQuery": false, 719 | "refId": "A", 720 | "resultFormat": "time_series", 721 | "select": [ 722 | [ 723 | { 724 | "params": [ 725 | "value" 726 | ], 727 | "type": "field" 728 | }, 729 | { 730 | "params": [], 731 | "type": "mean" 732 | } 733 | ] 734 | ], 735 | "tags": [ 736 | { 737 | "key": "host", 738 | "operator": "=", 739 | "value": "Home" 740 | } 741 | ] 742 | } 743 | ], 744 | "timeFrom": null, 745 | "timeShift": null, 746 | "title": "Ping / Latency", 747 | "type": "timeseries" 748 | } 749 | ], 750 | "refresh": "10s", 751 | "schemaVersion": 27, 752 | "style": "dark", 753 | "tags": [], 754 | "templating": { 755 | "list": [ 756 | { 757 | "allValue": null, 758 | "current": { 759 | "selected": false, 760 | "text": "Home", 761 | "value": "Home" 762 | }, 763 | "datasource": "InfluxDB", 764 | "definition": "show tag values from download with key=\"location\"", 765 | "description": null, 766 | "error": null, 767 | "hide": 0, 768 | "includeAll": false, 769 | "label": null, 770 | "multi": false, 771 | "name": "location", 772 | "options": [], 773 | "query": "show tag values from download with key=\"location\"", 774 | "refresh": 1, 775 | "regex": "", 776 | "skipUrlSync": false, 777 | "sort": 0, 778 | "tagValuesQuery": "", 779 | "tags": [], 780 | "tagsQuery": "", 781 | "type": "query", 782 | "useTags": false 783 | } 784 | ] 785 | }, 786 | "time": { 787 | "from": "now-3h", 788 | "to": "now" 789 | }, 790 | "timepicker": { 791 | "refresh_intervals": [ 792 | "10s", 793 | "30s", 794 | "1m", 795 | "5m", 796 | "15m", 797 | "30m", 798 | "1h", 799 | "2h", 800 | "1d" 801 | ], 802 | "time_options": [ 803 | "10m", 804 | "1h", 805 | "6h", 806 | "12h", 807 | "24h", 808 | "3d", 809 | "7d", 810 | "30d" 811 | ] 812 | } 813 | } -------------------------------------------------------------------------------- /grafana/datasources/influxdb.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: InfluxDB 5 | 6 | datasources: 7 | - name: InfluxDB 8 | type: influxdb 9 | access: proxy 10 | database: network_speed 11 | user: root # Update this to your Influx User 12 | password: Secret123 # Update this to your Influx Password 13 | url: http://influxdb:8086 14 | -------------------------------------------------------------------------------- /influxdb/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t18n/network-speed-monitor/b8e5b8cfaa1622e697878e33dd58e7d23888d29b/influxdb/.gitkeep -------------------------------------------------------------------------------- /screenshots/Network-Speedtest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t18n/network-speed-monitor/b8e5b8cfaa1622e697878e33dd58e7d23888d29b/screenshots/Network-Speedtest.jpg -------------------------------------------------------------------------------- /speedtest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/node:12.22.1-prod 2 | 3 | WORKDIR /src 4 | 5 | COPY ./src . 6 | RUN npm install 7 | 8 | RUN apt-get update -y 9 | RUN apt-get install -y gnupg1 apt-transport-https dirmngr iputils-ping 10 | RUN curl -s https://install.speedtest.net/app/cli/install.deb.sh | bash 11 | RUN apt-get update 12 | RUN apt-get install speedtest 13 | 14 | CMD ["node", "index.js"] 15 | -------------------------------------------------------------------------------- /speedtest/src/index.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const Influx = require("influx"); 3 | const cron = require('node-cron'); 4 | 5 | /** 6 | * Convert Bytes to Megabit 7 | */ 8 | const convertBytesToMbps = bytes => { 9 | const KB = bytes / 1000; 10 | const MB = KB / 1000; 11 | const Mb = MB * 8; 12 | return Mb; 13 | }; 14 | 15 | /** 16 | * Get speedtest data in json format 17 | */ 18 | const getSpeedData = async () => { 19 | const speedtestArgs = ["--accept-license", "--accept-gdpr", "--format=json"]; 20 | 21 | if (process.env.SPEEDTEST_SERVER_ID) { 22 | speedtestArgs.push(`--server-id=${process.env.SPEEDTEST_SERVER_ID}`); 23 | } 24 | 25 | const { stdout } = await execa("speedtest", speedtestArgs); 26 | const result = JSON.parse(stdout); 27 | 28 | return { 29 | upload: convertBytesToMbps(result.upload.bandwidth), 30 | download: convertBytesToMbps(result.download.bandwidth), 31 | ping: result.ping.latency, 32 | isp: result.isp, 33 | ip: result.interface.internalIp, 34 | myExternalIp: result.interface.externalIp, 35 | myRequestServerId: result.server.id, 36 | requestServerName: result.server.name, 37 | requestServerLocation: `${result.server.location} - ${result.server.country}`, 38 | }; 39 | }; 40 | 41 | /** 42 | * Write to influx 43 | */ 44 | const writeToDB = async (influx, metrics) => { 45 | const points = Object.entries(metrics).map(([measurement, value]) => ({ 46 | measurement, 47 | tags: { host: process.env.SPEEDTEST_HOST }, 48 | fields: { value } 49 | })); 50 | 51 | await influx.writePoints(points); 52 | }; 53 | 54 | /** 55 | * Perform the speedtest 56 | */ 57 | const runSpeedTest = async () => { 58 | try { 59 | const influx = new Influx.InfluxDB({ 60 | host: 'localhost', 61 | database: process.env.INFLUXDB_DB, 62 | username: process.env.INFLUXDB_ADMIN_USER, 63 | password: process.env.INFLUXDB_ADMIN_PASSWORD, 64 | }); 65 | 66 | const speedMetrics = await getSpeedData(); 67 | console.log(`Speedtest results ${speedMetrics.timestamp}`); 68 | console.table(speedMetrics); 69 | await writeToDB(influx, speedMetrics); 70 | } catch (err) { 71 | console.error(`Speedtest error: ${err.message}`); 72 | process.exit(1); 73 | } 74 | 75 | console.log('Completed speed test successfully!'); 76 | process.exit(1); 77 | }; 78 | 79 | /** 80 | * Start cron job 81 | */ 82 | cron.schedule(process.env.CRON_CONFIG, () => { 83 | console.log(`${new Date().toISOString()} - Start speed test`); 84 | runSpeedTest(); 85 | }); 86 | -------------------------------------------------------------------------------- /speedtest/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speedtest", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "cross-spawn": { 8 | "version": "7.0.1", 9 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", 10 | "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", 11 | "requires": { 12 | "path-key": "^3.1.0", 13 | "shebang-command": "^2.0.0", 14 | "which": "^2.0.1" 15 | } 16 | }, 17 | "end-of-stream": { 18 | "version": "1.4.4", 19 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 20 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 21 | "requires": { 22 | "once": "^1.4.0" 23 | } 24 | }, 25 | "execa": { 26 | "version": "4.0.0", 27 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.0.tgz", 28 | "integrity": "sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA==", 29 | "requires": { 30 | "cross-spawn": "^7.0.0", 31 | "get-stream": "^5.0.0", 32 | "human-signals": "^1.1.1", 33 | "is-stream": "^2.0.0", 34 | "merge-stream": "^2.0.0", 35 | "npm-run-path": "^4.0.0", 36 | "onetime": "^5.1.0", 37 | "signal-exit": "^3.0.2", 38 | "strip-final-newline": "^2.0.0" 39 | } 40 | }, 41 | "get-stream": { 42 | "version": "5.1.0", 43 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 44 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 45 | "requires": { 46 | "pump": "^3.0.0" 47 | } 48 | }, 49 | "human-signals": { 50 | "version": "1.1.1", 51 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 52 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" 53 | }, 54 | "influx": { 55 | "version": "5.5.1", 56 | "resolved": "https://registry.npmjs.org/influx/-/influx-5.5.1.tgz", 57 | "integrity": "sha512-06kWkvKZzdk7ONBi+MydTTVcpziIfzIKcTUqXpK0Uc0KoSVye0umQYDwDPJvMJtBjiS6L2eadXI7YsnNvOxbQw==" 58 | }, 59 | "is-stream": { 60 | "version": "2.0.0", 61 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 62 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" 63 | }, 64 | "isexe": { 65 | "version": "2.0.0", 66 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 67 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 68 | }, 69 | "merge-stream": { 70 | "version": "2.0.0", 71 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 72 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 73 | }, 74 | "mimic-fn": { 75 | "version": "2.1.0", 76 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 77 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 78 | }, 79 | "moment": { 80 | "version": "2.29.1", 81 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", 82 | "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" 83 | }, 84 | "moment-timezone": { 85 | "version": "0.5.33", 86 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", 87 | "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", 88 | "requires": { 89 | "moment": ">= 2.9.0" 90 | } 91 | }, 92 | "node-cron": { 93 | "version": "3.0.0", 94 | "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", 95 | "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", 96 | "requires": { 97 | "moment-timezone": "^0.5.31" 98 | } 99 | }, 100 | "npm-run-path": { 101 | "version": "4.0.1", 102 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 103 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 104 | "requires": { 105 | "path-key": "^3.0.0" 106 | } 107 | }, 108 | "once": { 109 | "version": "1.4.0", 110 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 111 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 112 | "requires": { 113 | "wrappy": "1" 114 | } 115 | }, 116 | "onetime": { 117 | "version": "5.1.0", 118 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 119 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 120 | "requires": { 121 | "mimic-fn": "^2.1.0" 122 | } 123 | }, 124 | "path-key": { 125 | "version": "3.1.1", 126 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 127 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 128 | }, 129 | "pump": { 130 | "version": "3.0.0", 131 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 132 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 133 | "requires": { 134 | "end-of-stream": "^1.1.0", 135 | "once": "^1.3.1" 136 | } 137 | }, 138 | "shebang-command": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 141 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 142 | "requires": { 143 | "shebang-regex": "^3.0.0" 144 | } 145 | }, 146 | "shebang-regex": { 147 | "version": "3.0.0", 148 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 149 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 150 | }, 151 | "signal-exit": { 152 | "version": "3.0.2", 153 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 154 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 155 | }, 156 | "strip-final-newline": { 157 | "version": "2.0.0", 158 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 159 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" 160 | }, 161 | "which": { 162 | "version": "2.0.2", 163 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 164 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 165 | "requires": { 166 | "isexe": "^2.0.0" 167 | } 168 | }, 169 | "wrappy": { 170 | "version": "1.0.2", 171 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 172 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /speedtest/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "network-speedtest-monitor", 3 | "version": "0.0.1", 4 | "author": "Turbo Thinh", 5 | "license": "MIT", 6 | "dependencies": { 7 | "node-cron": "^3.0.0", 8 | "execa": "^5.0.0", 9 | "influx": "^5.8.0" 10 | } 11 | } 12 | --------------------------------------------------------------------------------