mirror of https://github.com/statping/statping
commit
2169221d97
|
@ -0,0 +1,29 @@
|
||||||
|
name: Deploy Master Release
|
||||||
|
on: deployment
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-release:
|
||||||
|
needs: upload-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Statping Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setting ENV's
|
||||||
|
run: echo ::set-env name=VERSION::$(cat version.txt)
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: crazy-max/ghaction-docker-buildx@v3
|
||||||
|
|
||||||
|
- name: Docker Login
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||||
|
|
||||||
|
- name: Docker Buildx (push)
|
||||||
|
run: |
|
||||||
|
docker buildx create --use
|
||||||
|
docker buildx build --tag=statping/statping,statping/statping:v${VERSION} --build-arg=VERSION=${VERSION} --platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 --output type=image,name=docker.io/statping/statping,push=true .
|
||||||
|
|
|
@ -5,6 +5,11 @@ on:
|
||||||
- dev
|
- dev
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
|
@ -136,6 +141,8 @@ jobs:
|
||||||
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
|
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
|
||||||
TWILIO_TO: ${{ secrets.TWILIO_TO }}
|
TWILIO_TO: ${{ secrets.TWILIO_TO }}
|
||||||
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
|
|
||||||
- name: Coveralls Testing Coverage
|
- name: Coveralls Testing Coverage
|
||||||
run: |
|
run: |
|
||||||
|
@ -310,25 +317,16 @@ jobs:
|
||||||
run: echo ::set-env name=VERSION::$(cat version.txt)
|
run: echo ::set-env name=VERSION::$(cat version.txt)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Base Docker Image
|
- name: Set up Docker Buildx
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
uses: crazy-max/ghaction-docker-buildx@v3
|
||||||
with:
|
|
||||||
name: statping/statping
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
dockerfile: Dockerfile.base
|
|
||||||
tags: "base"
|
|
||||||
|
|
||||||
- name: Dev Docker Image
|
- name: Docker Login
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ env.VERSION }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
ARCH: amd64
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||||
with:
|
|
||||||
name: statping/statping
|
- name: Docker Buildx (push)
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
run: |
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
docker buildx create --use
|
||||||
dockerfile: Dockerfile
|
docker buildx build --tag=statping/statping:dev --build-arg=VERSION=${VERSION} --platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 --output type=image,name=docker.io/statping/statping,push=true .
|
||||||
tags: "dev"
|
|
||||||
buildargs: VERSION,ARCH
|
|
||||||
|
|
|
@ -5,6 +5,9 @@ on:
|
||||||
- master
|
- master
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
|
@ -136,6 +139,8 @@ jobs:
|
||||||
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
|
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
|
||||||
TWILIO_TO: ${{ secrets.TWILIO_TO }}
|
TWILIO_TO: ${{ secrets.TWILIO_TO }}
|
||||||
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
|
|
||||||
- name: Coveralls Testing Coverage
|
- name: Coveralls Testing Coverage
|
||||||
run: |
|
run: |
|
||||||
|
@ -401,40 +406,6 @@ jobs:
|
||||||
builds/statping-windows-amd64.zip
|
builds/statping-windows-amd64.zip
|
||||||
builds/statping-windows-arm.zip
|
builds/statping-windows-arm.zip
|
||||||
|
|
||||||
docker-release:
|
|
||||||
needs: upload-release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Statping Repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setting ENV's
|
|
||||||
run: echo ::set-env name=VERSION::$(cat version.txt)
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Base Docker Image
|
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
|
||||||
with:
|
|
||||||
name: statping/statping
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
dockerfile: Dockerfile.base
|
|
||||||
tags: "base"
|
|
||||||
|
|
||||||
- name: Latest/Version Docker Image
|
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
|
||||||
env:
|
|
||||||
VERSION: ${{ env.VERSION }}
|
|
||||||
ARCH: amd64
|
|
||||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
|
||||||
with:
|
|
||||||
name: statping/statping
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
tags: "latest,v${{ env.VERSION }}"
|
|
||||||
buildargs: VERSION,ARCH
|
|
||||||
|
|
||||||
sentry-release:
|
sentry-release:
|
||||||
needs: upload-release
|
needs: upload-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -5,9 +5,7 @@ on:
|
||||||
- '*' # matches every branch
|
- '*' # matches every branch
|
||||||
- '*/*' # matches every branch containing a single '/'
|
- '*/*' # matches every branch containing a single '/'
|
||||||
- '!master' # excludes master
|
- '!master' # excludes master
|
||||||
pull_request:
|
- '!dev' # excludes dev
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
compile:
|
compile:
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
# 0.90.58 (07-09-2020)
|
||||||
|
- Fixed ICMP latency/ping durations
|
||||||
|
- Fixed webhook notifier
|
||||||
|
- Modified file structure for Vue admin dashboard components.
|
||||||
|
- Added Gotify notifier
|
||||||
|
|
||||||
# 0.90.57 (07-04-2020)
|
# 0.90.57 (07-04-2020)
|
||||||
- Fixed login issue
|
- Fixed login issue
|
||||||
|
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -331,5 +331,9 @@ certs:
|
||||||
-keyout key.pem \
|
-keyout key.pem \
|
||||||
-subj "/C=US/ST=California/L=Santa Monica/O=Statping/OU=Development/CN=localhost"
|
-subj "/C=US/ST=California/L=Santa Monica/O=Statping/OU=Development/CN=localhost"
|
||||||
|
|
||||||
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
|
buildx:
|
||||||
|
docker buildx create --use
|
||||||
|
docker buildx build --tag=statping/statping:dev --build-arg=VERSION=${VERSION} --platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 --output type=image,name=docker.io/statping/statping,push=true .
|
||||||
|
|
||||||
|
.PHONY: all build build-all buildx build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
|
||||||
.SILENT: travis_s3_creds
|
.SILENT: travis_s3_creds
|
||||||
|
|
61
cmd/cli.go
61
cmd/cli.go
|
@ -153,8 +153,10 @@ func onceCli() error {
|
||||||
func importCli(args []string) error {
|
func importCli(args []string) error {
|
||||||
var err error
|
var err error
|
||||||
var data []byte
|
var data []byte
|
||||||
filename := args[1]
|
if len(args) < 1 {
|
||||||
if data, err = ioutil.ReadFile(filename); err != nil {
|
return errors.New("invalid command arguments")
|
||||||
|
}
|
||||||
|
if data, err = ioutil.ReadFile(args[0]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var exportData ExportData
|
var exportData ExportData
|
||||||
|
@ -162,11 +164,40 @@ func importCli(args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("=== %s ===\n", exportData.Core.Name)
|
log.Printf("=== %s ===\n", exportData.Core.Name)
|
||||||
log.Printf("Services: %d\n", len(exportData.Services))
|
if exportData.Config != nil {
|
||||||
log.Printf("Checkins: %d\n", len(exportData.Checkins))
|
log.Printf("Configs: %s\n", exportData.Config.DbConn)
|
||||||
log.Printf("Groups: %d\n", len(exportData.Groups))
|
if exportData.Config.DbUser != "" {
|
||||||
log.Printf("Messages: %d\n", len(exportData.Messages))
|
log.Printf(" - Host: %s\n", exportData.Config.DbHost)
|
||||||
log.Printf("Users: %d\n", len(exportData.Users))
|
log.Printf(" - User: %s\n", exportData.Config.DbUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(exportData.Services) > 0 {
|
||||||
|
log.Printf("Services: %d\n", len(exportData.Services))
|
||||||
|
}
|
||||||
|
if len(exportData.Checkins) > 0 {
|
||||||
|
log.Printf("Checkins: %d\n", len(exportData.Checkins))
|
||||||
|
}
|
||||||
|
if len(exportData.Groups) > 0 {
|
||||||
|
log.Printf("Groups: %d\n", len(exportData.Groups))
|
||||||
|
}
|
||||||
|
if len(exportData.Messages) > 0 {
|
||||||
|
log.Printf("Messages: %d\n", len(exportData.Messages))
|
||||||
|
}
|
||||||
|
if len(exportData.Users) > 0 {
|
||||||
|
log.Printf("Users: %d\n", len(exportData.Users))
|
||||||
|
}
|
||||||
|
|
||||||
|
if exportData.Config != nil {
|
||||||
|
if ask("Create config.yml file from Configs?") {
|
||||||
|
log.Printf("Database Host: %s\n", exportData.Config.DbHost)
|
||||||
|
log.Printf("Database Port: %d\n", exportData.Config.DbPort)
|
||||||
|
log.Printf("Database User: %s\n", exportData.Config.DbUser)
|
||||||
|
log.Printf("Database Password: %s\n", exportData.Config.DbPass)
|
||||||
|
if err := exportData.Config.Save(utils.Directory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config, err := configs.LoadConfigs(configFile)
|
config, err := configs.LoadConfigs(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -175,8 +206,10 @@ func importCli(args []string) error {
|
||||||
if err = configs.ConnectConfigs(config, false); err != nil {
|
if err = configs.ConnectConfigs(config, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if data, err = ExportSettings(); err != nil {
|
if ask("Create database rows and sample data?") {
|
||||||
return fmt.Errorf("could not export settings: %v", err.Error())
|
if err := config.ResetCore(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ask("Import Core settings?") {
|
if ask("Import Core settings?") {
|
||||||
|
@ -221,7 +254,7 @@ func importCli(args []string) error {
|
||||||
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
|
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
|
||||||
s.Id = 0
|
s.Id = 0
|
||||||
if err := s.Create(); err != nil {
|
if err := s.Create(); err != nil {
|
||||||
return err
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,6 +409,7 @@ type gitUploader struct {
|
||||||
// ExportChartsJs renders the charts for the index page
|
// ExportChartsJs renders the charts for the index page
|
||||||
|
|
||||||
type ExportData struct {
|
type ExportData struct {
|
||||||
|
Config *configs.DbConfig `json:"config"`
|
||||||
Core *core.Core `json:"core"`
|
Core *core.Core `json:"core"`
|
||||||
Services []services.Service `json:"services"`
|
Services []services.Service `json:"services"`
|
||||||
Messages []*messages.Message `json:"messages"`
|
Messages []*messages.Message `json:"messages"`
|
||||||
|
@ -403,7 +437,14 @@ func ExportSettings() ([]byte, error) {
|
||||||
s.Failures = nil
|
s.Failures = nil
|
||||||
srvs = append(srvs, s)
|
srvs = append(srvs, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := configs.LoadConfigs(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
data := ExportData{
|
data := ExportData{
|
||||||
|
Config: cfg,
|
||||||
Core: c,
|
Core: c,
|
||||||
Notifiers: core.App.Notifications,
|
Notifiers: core.App.Notifications,
|
||||||
Checkins: checkins.All(),
|
Checkins: checkins.All(),
|
||||||
|
|
35
cmd/main.go
35
cmd/main.go
|
@ -96,39 +96,8 @@ func start() {
|
||||||
exit(err)
|
exit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !confgs.Db.HasTable("core") {
|
if err = confgs.ResetCore(); err != nil {
|
||||||
var srvs int64
|
exit(err)
|
||||||
if confgs.Db.HasTable(&services.Service{}) {
|
|
||||||
confgs.Db.Model(&services.Service{}).Count(&srvs)
|
|
||||||
if srvs > 0 {
|
|
||||||
exit(errors.Wrap(err, "there are already services setup."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := confgs.DropDatabase(); err != nil {
|
|
||||||
exit(errors.Wrap(err, "error dropping database"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := confgs.CreateDatabase(); err != nil {
|
|
||||||
exit(errors.Wrap(err, "error creating database"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := configs.CreateAdminUser(confgs); err != nil {
|
|
||||||
exit(errors.Wrap(err, "error creating default admin user"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.Params.GetBool("SAMPLE_DATA") {
|
|
||||||
log.Infoln("Adding Sample Data")
|
|
||||||
if err := configs.TriggerSamples(); err != nil {
|
|
||||||
exit(errors.Wrap(err, "error adding sample data"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := core.Samples(); err != nil {
|
|
||||||
exit(errors.Wrap(err, "error added core details"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = confgs.DatabaseChanges(); err != nil {
|
if err = confgs.DatabaseChanges(); err != nil {
|
||||||
|
|
|
@ -3917,7 +3917,7 @@
|
||||||
"",
|
"",
|
||||||
"pm.test(\"View All Notifiers\", function () {",
|
"pm.test(\"View All Notifiers\", function () {",
|
||||||
" var jsonData = pm.response.json();",
|
" var jsonData = pm.response.json();",
|
||||||
" pm.expect(jsonData.length).to.eql(11);",
|
" pm.expect(jsonData.length).to.eql(12);",
|
||||||
"});"
|
"});"
|
||||||
],
|
],
|
||||||
"type": "text/javascript"
|
"type": "text/javascript"
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ServiceInfo = () => import('@/components/Service/ServiceInfo')
|
const ServiceInfo = () => import('@/components/Dashboard/ServiceInfo')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DashboardIndex',
|
name: 'DashboardIndex',
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<div v-if="false" class="row mb-4 align-content-center">
|
<div v-if="false" class="row mb-4 align-content-center">
|
||||||
|
|
||||||
<div v-if="!service.online" class="col-3 text-left">
|
<div v-if="!service.online" class="col-3 text-left">
|
||||||
<span class="text-danger font-5 font-weight-bold"></span>
|
<span css="text-danger font-5 font-weight-bold"></span>
|
||||||
<span class="font-2 d-block">Current Downtime</span>
|
<span class="font-2 d-block">Current Downtime</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
import Checkin from '../../forms/Checkin';
|
import Checkin from '../../forms/Checkin';
|
||||||
import FormIncident from '../../forms/Incident';
|
import FormIncident from '../../forms/Incident';
|
||||||
import FormMessage from '../../forms/Message';
|
import FormMessage from '../../forms/Message';
|
||||||
import ServiceFailures from './ServiceFailures';
|
import ServiceFailures from '../Service/ServiceFailures';
|
||||||
import ServiceSparkLine from "./ServiceSparkLine";
|
import ServiceSparkLine from "./ServiceSparkLine";
|
||||||
import Api from "../../API";
|
import Api from "../../API";
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
show: false,
|
show: true,
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
formatter: (value) => { return value + " %" },
|
formatter: (value) => { return value + " %" },
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MiniSparkLine from './MiniSparkLine';
|
import MiniSparkLine from './MiniSparkLine';
|
||||||
import ServiceSparkLine from './ServiceSparkLine';
|
import ServiceSparkLine from '../Dashboard/ServiceSparkLine';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
|
|
|
@ -10,20 +10,6 @@
|
||||||
|
|
||||||
<ServiceTopStats :service="service"/>
|
<ServiceTopStats :service="service"/>
|
||||||
|
|
||||||
<div v-if="expanded" class="row">
|
|
||||||
<Analytics title="Last Failure" :func="stats.total_failures"/>
|
|
||||||
<Analytics title="Total Failures" :func="stats.total_failures"/>
|
|
||||||
<Analytics title="Highest Latency" :func="stats.high_latency"/>
|
|
||||||
<Analytics title="Lowest Latency" :func="stats.lowest_latency"/>
|
|
||||||
<Analytics title="Total Uptime" :func="stats.high_ping"/>
|
|
||||||
<Analytics title="Total Downtime" :func="stats.low_ping"/>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<router-link :to="serviceLink(service)" class="btn btn-block btn-outline-success mt-4" :class="{'btn-outline-success': service.online, 'btn-outline-danger': !service.online}">
|
|
||||||
View More Details
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {library} from '@fortawesome/fontawesome-svg-core'
|
import {library} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {fas} from '@fortawesome/fontawesome-free-solid';
|
import {fas} from '@fortawesome/fontawesome-free-solid';
|
||||||
import {fab} from '@fortawesome/free-brands-svg-icons';
|
import {fab} from '@fortawesome/free-brands-svg-icons';
|
||||||
import {far} from '@fortawesome/fontawesome-svg-core';
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
|
||||||
|
|
|
@ -123,6 +123,8 @@ export default Vue.mixin({
|
||||||
return "bell"
|
return "bell"
|
||||||
case "fas fa-mobile-alt":
|
case "fas fa-mobile-alt":
|
||||||
return "mobile"
|
return "mobile"
|
||||||
|
case "fa dot-circle":
|
||||||
|
return ["fa", "dot-circle"]
|
||||||
case "fas envelope-square":
|
case "fas envelope-square":
|
||||||
return ["fas", "envelope-square"]
|
return ["fas", "envelope-square"]
|
||||||
case "fab fa-slack":
|
case "fab fa-slack":
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/statping/statping/types/failures"
|
||||||
|
"github.com/statping/statping/types/notifications"
|
||||||
|
"github.com/statping/statping/types/notifier"
|
||||||
|
"github.com/statping/statping/types/services"
|
||||||
|
"github.com/statping/statping/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ notifier.Notifier = (*gotify)(nil)
|
||||||
|
|
||||||
|
type gotify struct {
|
||||||
|
*notifications.Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gotify) Select() *notifications.Notification {
|
||||||
|
return g.Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
var Gotify = &gotify{¬ifications.Notification{
|
||||||
|
Method: "gotify",
|
||||||
|
Title: "Gotify",
|
||||||
|
Description: "Use Gotify to receive push notifications. Add your Gotify URL and App Token to receive notifications.",
|
||||||
|
Author: "Hugo van Rijswijk",
|
||||||
|
AuthorUrl: "https://github.com/hugo-vrijswijk",
|
||||||
|
Icon: "broadcast-tower",
|
||||||
|
Delay: time.Duration(5 * time.Second),
|
||||||
|
Limits: 60,
|
||||||
|
SuccessData: `{"title": "{{.Service.Name}}", "message": "Your service '{{.Service.Name}}' is currently online!", "priority": 2}`,
|
||||||
|
FailureData: `{"title": "{{.Service.Name}}", "message": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}", "priority": 5}`,
|
||||||
|
DataType: "json",
|
||||||
|
Form: []notifications.NotificationForm{{
|
||||||
|
Type: "text",
|
||||||
|
Title: "Gotify URL",
|
||||||
|
SmallText: "Gotify server URL, including http(s):// and port if needed",
|
||||||
|
DbField: "Host",
|
||||||
|
Placeholder: "https://gotify.domain.com",
|
||||||
|
Required: true,
|
||||||
|
}, {
|
||||||
|
Type: "text",
|
||||||
|
Title: "App Token",
|
||||||
|
SmallText: "The Application Token generated by Gotify",
|
||||||
|
DbField: "api_key",
|
||||||
|
Placeholder: "TB5gatYYyR.FCD2",
|
||||||
|
Required: true,
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send will send a HTTP Post to the Gotify API. It accepts type: string
|
||||||
|
func (g *gotify) sendMessage(msg string) (string, error) {
|
||||||
|
var url string
|
||||||
|
if strings.HasSuffix(g.Host, "/") {
|
||||||
|
url = g.Host + "message"
|
||||||
|
} else {
|
||||||
|
url = g.Host + "/message"
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"X-Gotify-Key=" + g.ApiKey}
|
||||||
|
|
||||||
|
content, _, err := utils.HttpRequest(url, "POST", "application/json", headers, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
|
||||||
|
|
||||||
|
return string(content), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFailure will trigger failing service
|
||||||
|
func (g *gotify) OnFailure(s services.Service, f failures.Failure) (string, error) {
|
||||||
|
out, err := g.sendMessage(ReplaceVars(g.FailureData, s, f))
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSuccess will trigger successful service
|
||||||
|
func (g *gotify) OnSuccess(s services.Service) (string, error) {
|
||||||
|
out, err := g.sendMessage(ReplaceVars(g.SuccessData, s, failures.Failure{}))
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnTest will test the Gotify notifier
|
||||||
|
func (g *gotify) OnTest() (string, error) {
|
||||||
|
msg := `{"title:" "Test" "message": "Testing the Gotify Notifier", "priority": 0}`
|
||||||
|
content, err := g.sendMessage(msg)
|
||||||
|
|
||||||
|
return content, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSave will trigger when this notifier is saved
|
||||||
|
func (g *gotify) OnSave() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/statping/statping/database"
|
||||||
|
"github.com/statping/statping/types/core"
|
||||||
|
"github.com/statping/statping/types/failures"
|
||||||
|
"github.com/statping/statping/types/notifications"
|
||||||
|
"github.com/statping/statping/types/null"
|
||||||
|
"github.com/statping/statping/types/services"
|
||||||
|
"github.com/statping/statping/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GOTIFY_URL string
|
||||||
|
GOTIFY_TOKEN string
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGotifyNotifier(t *testing.T) {
|
||||||
|
err := utils.InitLogs()
|
||||||
|
require.Nil(t, err)
|
||||||
|
GOTIFY_URL = utils.Params.GetString("GOTIFY_URL")
|
||||||
|
GOTIFY_TOKEN = utils.Params.GetString("GOTIFY_TOKEN")
|
||||||
|
|
||||||
|
db, err := database.OpenTester()
|
||||||
|
require.Nil(t, err)
|
||||||
|
db.AutoMigrate(¬ifications.Notification{})
|
||||||
|
notifications.SetDB(db)
|
||||||
|
core.Example()
|
||||||
|
|
||||||
|
if GOTIFY_URL == "" {
|
||||||
|
t.Log("gotify notifier testing skipped, missing GOTIFY_URL environment variable")
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
if GOTIFY_TOKEN == "" {
|
||||||
|
t.Log("gotify notifier testing skipped, missing GOTIFY_TOKEN environment variable")
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Load gotify", func(t *testing.T) {
|
||||||
|
Gotify.Host = GOTIFY_URL
|
||||||
|
Gotify.Delay = time.Duration(100 * time.Millisecond)
|
||||||
|
Gotify.Enabled = null.NewNullBool(true)
|
||||||
|
|
||||||
|
Add(Gotify)
|
||||||
|
|
||||||
|
assert.Equal(t, "Hugo van Rijswijk", Gotify.Author)
|
||||||
|
assert.Equal(t, GOTIFY_URL, Gotify.Host)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gotify Notifier Tester", func(t *testing.T) {
|
||||||
|
assert.True(t, Gotify.CanSend())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gotify Notifier Tester OnSave", func(t *testing.T) {
|
||||||
|
_, err := Gotify.OnSave()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gotify OnFailure", func(t *testing.T) {
|
||||||
|
_, err := Gotify.OnFailure(services.Example(false), failures.Example())
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gotify OnSuccess", func(t *testing.T) {
|
||||||
|
_, err := Gotify.OnSuccess(services.Example(true))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gotify Test", func(t *testing.T) {
|
||||||
|
_, err := Gotify.OnTest()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ func InitNotifiers() {
|
||||||
Mobile,
|
Mobile,
|
||||||
Pushover,
|
Pushover,
|
||||||
statpingMailer,
|
statpingMailer,
|
||||||
|
Gotify,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,24 +82,27 @@ func (w *webhooker) Select() *notifications.Notification {
|
||||||
func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
|
func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
|
||||||
utils.Log.Infoln(fmt.Sprintf("sending body: '%v' to %v as a %v request", body, w.Host, w.Var1))
|
utils.Log.Infoln(fmt.Sprintf("sending body: '%v' to %v as a %v request", body, w.Host, w.Var1))
|
||||||
client := new(http.Client)
|
client := new(http.Client)
|
||||||
client.Timeout = time.Duration(10 * time.Second)
|
client.Timeout = 10 * time.Second
|
||||||
buf := bytes.NewBuffer(nil)
|
req, err := http.NewRequest(w.Var1, w.Host, bytes.NewBufferString(body))
|
||||||
if w.Var2 != "" {
|
|
||||||
buf = bytes.NewBuffer([]byte(body))
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(w.Var1, w.Host, buf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if w.ApiSecret != "" {
|
if w.ApiSecret != "" {
|
||||||
splitArray := strings.Split(w.ApiSecret, ",")
|
keyVal := strings.SplitN(w.ApiSecret, "=", 2)
|
||||||
for _, a := range splitArray {
|
if len(keyVal) == 2 {
|
||||||
split := strings.Split(a, "=")
|
if keyVal[0] != "" && keyVal[1] != "" {
|
||||||
req.Header.Add(split[0], split[1])
|
if strings.ToLower(keyVal[0]) == "host" {
|
||||||
|
req.Host = strings.TrimSpace(keyVal[1])
|
||||||
|
} else {
|
||||||
|
req.Header.Set(keyVal[0], keyVal[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.ApiKey != "" {
|
if w.ApiKey != "" {
|
||||||
req.Header.Add("Content-Type", w.ApiKey)
|
req.Header.Add("Content-Type", w.ApiKey)
|
||||||
|
} else {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Statping")
|
req.Header.Set("User-Agent", "Statping")
|
||||||
req.Header.Set("Statping-Version", utils.Version)
|
req.Header.Set("Statping-Version", utils.Version)
|
||||||
|
|
|
@ -21,18 +21,6 @@ func ConnectConfigs(configs *DbConfig, retry bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfigs(cfgFile string) (*DbConfig, error) {
|
|
||||||
writeAble, err := utils.DirWritable(utils.Directory)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !writeAble {
|
|
||||||
return nil, errors.Errorf("Directory %s is not writable!", utils.Directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoadConfigFile(cfgFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findDbFile(configs *DbConfig) (string, error) {
|
func findDbFile(configs *DbConfig) (string, error) {
|
||||||
location := utils.Directory + "/" + SqliteFilename
|
location := utils.Directory + "/" + SqliteFilename
|
||||||
if configs == nil {
|
if configs == nil {
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
package configs
|
package configs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/statping/statping/types/errors"
|
"errors"
|
||||||
"github.com/statping/statping/utils"
|
"github.com/statping/statping/utils"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadConfigFile(configFile string) (*DbConfig, error) {
|
func LoadConfigs(cfgFile string) (*DbConfig, error) {
|
||||||
|
writeAble, err := utils.DirWritable(utils.Directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !writeAble {
|
||||||
|
return nil, errors.New("Directory %s is not writable: " + utils.Directory)
|
||||||
|
}
|
||||||
p := utils.Params
|
p := utils.Params
|
||||||
log.Infof("Attempting to read config file at: %s", configFile)
|
log.Infof("Attempting to read config file at: %s", cfgFile)
|
||||||
p.SetConfigFile(configFile)
|
p.SetConfigFile(cfgFile)
|
||||||
p.SetConfigType("yaml")
|
p.SetConfigType("yaml")
|
||||||
p.ReadInConfig()
|
p.ReadInConfig()
|
||||||
|
|
||||||
db := new(DbConfig)
|
db := new(DbConfig)
|
||||||
content, err := utils.OpenFile(configFile)
|
content, err := utils.OpenFile(cfgFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err := yaml.Unmarshal([]byte(content), &db); err != nil {
|
if err := yaml.Unmarshal([]byte(content), &db); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -74,7 +81,7 @@ func LoadConfigFile(configFile string) (*DbConfig, error) {
|
||||||
Language: p.GetString("LANGUAGE"),
|
Language: p.GetString("LANGUAGE"),
|
||||||
SendReports: p.GetBool("ALLOW_REPORTS"),
|
SendReports: p.GetBool("ALLOW_REPORTS"),
|
||||||
}
|
}
|
||||||
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + configFile)
|
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + cfgFile)
|
||||||
|
|
||||||
if configs.DbConn == "" {
|
if configs.DbConn == "" {
|
||||||
return configs, errors.New("Starting in setup mode")
|
return configs, errors.New("Starting in setup mode")
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/statping/statping/source"
|
"github.com/statping/statping/source"
|
||||||
"github.com/statping/statping/types/notifications"
|
"github.com/statping/statping/types/notifications"
|
||||||
"github.com/statping/statping/utils"
|
"github.com/statping/statping/utils"
|
||||||
|
@ -20,6 +21,39 @@ import (
|
||||||
"github.com/statping/statping/types/users"
|
"github.com/statping/statping/types/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (d *DbConfig) ResetCore() error {
|
||||||
|
if d.Db.HasTable("core") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var srvs int64
|
||||||
|
if d.Db.HasTable(&services.Service{}) {
|
||||||
|
d.Db.Model(&services.Service{}).Count(&srvs)
|
||||||
|
if srvs > 0 {
|
||||||
|
return errors.New("there are already services setup.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := d.DropDatabase(); err != nil {
|
||||||
|
return errors.Wrap(err, "error dropping database")
|
||||||
|
}
|
||||||
|
if err := d.CreateDatabase(); err != nil {
|
||||||
|
return errors.Wrap(err, "error creating database")
|
||||||
|
}
|
||||||
|
if err := CreateAdminUser(d); err != nil {
|
||||||
|
return errors.Wrap(err, "error creating default admin user")
|
||||||
|
}
|
||||||
|
if utils.Params.GetBool("SAMPLE_DATA") {
|
||||||
|
log.Infoln("Adding Sample Data")
|
||||||
|
if err := TriggerSamples(); err != nil {
|
||||||
|
return errors.Wrap(err, "error adding sample data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := core.Samples(); err != nil {
|
||||||
|
return errors.Wrap(err, "error added core details")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DbConfig) DatabaseChanges() error {
|
func (d *DbConfig) DatabaseChanges() error {
|
||||||
var cr core.Core
|
var cr core.Core
|
||||||
d.Db.Model(&core.Core{}).Find(&cr)
|
d.Db.Model(&core.Core{}).Find(&cr)
|
||||||
|
|
|
@ -91,14 +91,21 @@ func CheckIcmp(s *Service, record bool) (*Service, error) {
|
||||||
timer := prometheus.NewTimer(metrics.ServiceTimer(s.Name))
|
timer := prometheus.NewTimer(metrics.ServiceTimer(s.Name))
|
||||||
defer timer.ObserveDuration()
|
defer timer.ObserveDuration()
|
||||||
|
|
||||||
if err := utils.Ping(s.Domain, s.Timeout); err != nil {
|
dur, err := utils.Ping(s.Domain, s.Timeout)
|
||||||
|
if err != nil {
|
||||||
if record {
|
if record {
|
||||||
recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
|
recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
|
||||||
}
|
}
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.PingTime = dur
|
||||||
|
s.Latency = dur
|
||||||
s.LastResponse = ""
|
s.LastResponse = ""
|
||||||
s.Online = true
|
s.Online = true
|
||||||
|
if record {
|
||||||
|
recordSuccess(s)
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,6 @@ func (t Timestamp) Ago() string {
|
||||||
// Command will run a terminal command with 'sh -c COMMAND' and return stdout and errOut as strings
|
// Command will run a terminal command with 'sh -c COMMAND' and return stdout and errOut as strings
|
||||||
// in, out, err := Command("sass assets/scss assets/css/base.css")
|
// in, out, err := Command("sass assets/scss assets/css/base.css")
|
||||||
func Command(name string, args ...string) (string, string, error) {
|
func Command(name string, args ...string) (string, string, error) {
|
||||||
Log.Info("Running command: " + name + " " + strings.Join(args, " "))
|
|
||||||
testCmd := exec.Command(name, args...)
|
testCmd := exec.Command(name, args...)
|
||||||
var stdout, stderr []byte
|
var stdout, stderr []byte
|
||||||
var errStdout, errStderr error
|
var errStdout, errStderr error
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -36,20 +37,27 @@ func DirWritable(path string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ping(address string, secondsTimeout int) error {
|
func Ping(address string, secondsTimeout int) (int64, error) {
|
||||||
ping, err := exec.LookPath("ping")
|
ping, err := exec.LookPath("ping")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
out, _, err := Command(ping, address, "-c", "1", "-W", strconv.Itoa(secondsTimeout))
|
out, _, err := Command(ping, address, "-c", "1", "-W", strconv.Itoa(secondsTimeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
if strings.Contains(out, "Unknown host") {
|
if strings.Contains(out, "Unknown host") {
|
||||||
return errors.New("unknown host")
|
return 0, errors.New("unknown host")
|
||||||
}
|
}
|
||||||
if strings.Contains(out, "100.0% packet loss") {
|
if strings.Contains(out, "100.0% packet loss") {
|
||||||
return errors.New("destination host unreachable")
|
return 0, errors.New("destination host unreachable")
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
r := regexp.MustCompile(`time=(.*) ms`)
|
||||||
|
strs := r.FindStringSubmatch(out)
|
||||||
|
if len(strs) < 2 {
|
||||||
|
return 0, errors.New("could not parse ping duration")
|
||||||
|
}
|
||||||
|
f, _ := strconv.ParseFloat(strs[1], 64)
|
||||||
|
return int64(f * 1000), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -25,17 +26,23 @@ func DirWritable(path string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ping(address string, secondsTimeout int) error {
|
func Ping(address string, secondsTimeout int) (int64, error) {
|
||||||
ping, err := exec.LookPath("ping")
|
ping, err := exec.LookPath("ping")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
out, _, err := Command(ping, address, "-n", "1", "-w", strconv.Itoa(secondsTimeout*1000))
|
out, _, err := Command(ping, address, "-n", "1", "-w", strconv.Itoa(secondsTimeout*1000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
if strings.Contains(out, "Destination Host Unreachable") {
|
if strings.Contains(out, "Destination Host Unreachable") {
|
||||||
return errors.New("destination host unreachable")
|
return 0, errors.New("destination host unreachable")
|
||||||
}
|
}
|
||||||
return nil
|
r := regexp.MustCompile(`Average = (.*)ms`)
|
||||||
|
strs := r.FindStringSubmatch(out)
|
||||||
|
if len(strs) < 2 {
|
||||||
|
return 0, errors.New("could not parse ping duration")
|
||||||
|
}
|
||||||
|
f, _ := strconv.ParseFloat(strs[1], 64)
|
||||||
|
return int64(f * 1000), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.90.57
|
0.90.58
|
||||||
|
|
Loading…
Reference in New Issue