design changes, form updates, API fixes

pull/483/head
Hunter Long 4 years ago
parent 514d0f59e8
commit efcdd9fdd8

@ -28,7 +28,7 @@ env:
go: 1.14
go_import_path: github.com/statping/statping
install:
- "npm install -g sass newman cross-env wait-on"
- "npm install -g sass newman cross-env wait-on @sentry/cli"
- "pip install --user awscli"
- "go get github.com/mattn/goveralls"
- "go mod download"
@ -49,7 +49,6 @@ os:
- linux
script:
- "travis_retry make clean test-ci"
- "travis_retry make test-cypress"
- "if [[ \"$TRAVIS_BRANCH\" == \"master\" && \"$TRAVIS_PULL_REQUEST\" = \"false\" ]]; then make coverage; fi"
services:
- docker

@ -1,3 +1,10 @@
# 0.90.22
- Added range input types for integer form fields
- Modified Sentry error logging details
- Modified form field layouts for better UX.
- Modified Notifier form
- Fixed Notifier Test form and logic
# 0.90.21
- Fixed BASE_PATH when using a path for Statping
- Added Cypress testing

@ -40,12 +40,10 @@ test-ci: clean compile test-deps
SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
goveralls -coverprofile=coverage.out -service=travis-ci -repotoken ${COVERALLS}
test-cypress: clean
cypress: clean
echo "Statping Bin: "`which statping`
echo "Statping Version: "`statping version`
statping -port 8585 & wait-on http://localhost:8585/setup
cd frontend && yarn dev & wait-on http://localhost:8888
cd frontend && yarn cypress:test
cd frontend && yarn test
killall statping
test-api:
@ -159,6 +157,7 @@ clean:
rm -rf source/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf types/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf utils/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf frontend/{logs,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf dev/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
rm -rf {parts,prime,snap,stage}
rm -rf dev/test/cypress/videos
@ -283,6 +282,10 @@ xgo-install: clean
go get github.com/crazy-max/xgo
docker pull crazymax/xgo:${GOVERSION}
sentry-release:
sentry-cli releases new -p backend -p frontend v${VERSION}
sentry-cli releases set-commits --auto v${VERSION}
sentry-cli releases finalize v${VERSION}
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release
.SILENT: travis_s3_creds

@ -1,8 +1,6 @@
package main
import (
"fmt"
"github.com/getsentry/sentry-go"
"github.com/rendon/testcli"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
@ -20,14 +18,6 @@ var (
func init() {
dir = utils.Directory
//core.SampleHits = 480
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: "testing",
}); err != nil {
fmt.Println(err)
}
}
func TestStartServerCommand(t *testing.T) {

@ -1,28 +0,0 @@
package main
import (
"github.com/statping/statping/database"
"github.com/statping/statping/notifiers"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
)
func InitApp() error {
if _, err := core.Select(); err != nil {
return err
}
if _, err := services.SelectAllServices(true); err != nil {
return err
}
go services.CheckServices()
notifiers.InitNotifiers()
database.StartMaintenceRoutine()
core.App.Setup = true
core.App.Started = utils.Now()
return nil
}

@ -3,34 +3,31 @@ package main
import (
"flag"
"fmt"
"github.com/getsentry/sentry-go"
"github.com/statping/statping/handlers/protos"
"github.com/statping/statping/types/core"
"os"
"os/signal"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/statping/statping/database"
"github.com/statping/statping/handlers"
"github.com/statping/statping/notifiers"
"github.com/statping/statping/source"
"github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"os"
"os/signal"
"syscall"
)
var (
// VERSION stores the current version of Statping
VERSION string
// COMMIT stores the git commit hash for this version of Statping
COMMIT string
ipAddress string
grpcPort int
COMMIT string
ipAddress string
//grpcPort int
envFile string
verboseMode int
port int
log = utils.Log.WithField("type", "cmd")
httpServer = make(chan bool)
confgs *configs.DbConfig
)
@ -43,25 +40,32 @@ func parseFlags() {
envPort := utils.Getenv("PORT", 8080).(int)
envIpAddress := utils.Getenv("IP", "0.0.0.0").(string)
envVerbose := utils.Getenv("VERBOSE", 2).(int)
envGrpcPort := utils.Getenv("GRPC_PORT", 0).(int)
//envGrpcPort := utils.Getenv("GRPC_PORT", 0).(int)
flag.StringVar(&ipAddress, "ip", envIpAddress, "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", envPort, "Port to run the HTTP server")
flag.IntVar(&grpcPort, "grpc", envGrpcPort, "Port to run the gRPC server")
//flag.IntVar(&grpcPort, "grpc", envGrpcPort, "Port to run the gRPC server")
flag.IntVar(&verboseMode, "verbose", envVerbose, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
}
func init() {
core.New(VERSION)
}
// exit will return an error and return an exit code 1 due to this error
func exit(err error) {
sentry.CaptureException(err)
log.Fatalln(err)
utils.SentryErr(err)
Close()
os.Exit(2)
log.Fatalln(err)
}
func init() {
core.New(VERSION)
// Close will gracefully stop the database connection, and log file
func Close() {
utils.CloseLogs()
confgs.Close()
fmt.Println("Shutting down Statping")
}
// main will run the Statping application
@ -71,6 +75,8 @@ func main() {
parseFlags()
utils.SentryInit(VERSION)
if err := source.Assets(); err != nil {
exit(err)
}
@ -93,20 +99,12 @@ func main() {
exit(err)
}
}
log.Info(fmt.Sprintf("Starting Statping v%v", VERSION))
log.Info(fmt.Sprintf("Starting Statping v%s", VERSION))
if err := updateDisplay(); err != nil {
log.Warnln(err)
}
errorEnv := utils.Getenv("GO_ENV", "production").(string)
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: errorEnv,
}); err != nil {
log.Errorln(err)
}
confgs, err = configs.LoadConfigs()
if err != nil {
if err := SetupMode(); err != nil {
@ -165,13 +163,6 @@ func main() {
}
}
// Close will gracefully stop the database connection, and log file
func Close() {
sentry.Flush(3 * time.Second)
utils.CloseLogs()
confgs.Close()
}
func SetupMode() error {
return handlers.RunHTTPServer(ipAddress, port)
}
@ -181,7 +172,6 @@ func sigterm() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
fmt.Println("Shutting down Statping")
Close()
os.Exit(0)
}
@ -205,28 +195,25 @@ func mainProcess() error {
return nil
}
func StartHTTPServer() {
httpServer = make(chan bool)
go httpServerProcess(httpServer)
}
// InitApp will start the Statping instance with a valid database connection
// This function will gather all services in database, add/init Notifiers,
// and start the database cleanup routine
func InitApp() error {
if _, err := core.Select(); err != nil {
return err
}
func StopHTTPServer() {
if _, err := services.SelectAllServices(true); err != nil {
return err
}
}
go services.CheckServices()
func httpServerProcess(process <-chan bool) {
for {
select {
case <-process:
fmt.Println("HTTP Server has stopped")
return
default:
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Errorln(err)
exit(err)
}
}
}
}
notifiers.InitNotifiers()
core.App.Setup = true
core.App.Started = utils.Now()
const errorReporter = "https://2bedd272821643e1b92c774d3fdf28e7@sentry.statping.com/2"
go database.Maintenance()
return nil
}

@ -2,9 +2,7 @@ package database
import (
"fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"os"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
@ -13,54 +11,36 @@ import (
)
var (
log = utils.Log
removeRowsAfter = types.Day * 90
maintenceDuration = types.Hour
log = utils.Log.WithField("type", "database")
)
func StartMaintenceRoutine() {
dur := os.Getenv("REMOVE_AFTER")
var removeDur time.Duration
if dur != "" {
parsedDur, err := time.ParseDuration(dur)
if err != nil {
log.Errorf("could not parse duration: %s, using default: %s", dur, removeRowsAfter.String())
removeDur = removeRowsAfter
} else {
removeDur = parsedDur
}
} else {
removeDur = removeRowsAfter
}
log.Infof("Service Failure and Hit records will be automatically removed after %s", removeDur.String())
go databaseMaintence(removeDur)
}
// databaseMaintence will automatically delete old records from 'failures' and 'hits'
// Maintenance will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func databaseMaintence(dur time.Duration) {
//deleteAfter := time.Now().UTC().Add(dur)
func Maintenance() {
dur := utils.GetenvAs("REMOVE_AFTER", "2160h").Duration()
interval := utils.GetenvAs("CLEANUP_INTERVAL", "1h").Duration()
log.Infof("Database Cleanup runs every %s and will remove records older than %s", interval.String(), dur.String())
ticker := interval
time.Sleep(20 * types.Second)
for range time.Tick(ticker) {
deleteAfter := utils.Now().Add(-dur)
for range time.Tick(maintenceDuration) {
log.Infof("Deleting failures older than %s", dur.String())
//DeleteAllSince("failures", deleteAfter)
log.Infof("Deleting failures older than %s", deleteAfter.String())
deleteAllSince("failures", deleteAfter)
log.Infof("Deleting hits older than %s", dur.String())
//DeleteAllSince("hits", deleteAfter)
log.Infof("Deleting hits older than %s", deleteAfter.String())
deleteAllSince("hits", deleteAfter)
maintenceDuration = types.Hour
ticker = interval
}
}
// DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %s WHERE created_at < '%s';", table, database.FormatTime(date))
q := database.Exec(sql).Debug()
if q.Error() != nil {
log.Warnln(q.Error())
// deleteAllSince will delete a specific table's records based on a time.
func deleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %s WHERE created_at < '%s'", table, database.FormatTime(date))
log.Info(sql)
if err := database.Exec(sql).Error(); err != nil {
log.WithField("query", sql).Errorln(err)
}
}

@ -11,7 +11,7 @@
"cypress:open": "cypress open",
"cypress:test": "cypress run --record --key 49d99e5e-04c6-46df-beef-54b68e152a4d",
"test": "start-server-and-test start http://0.0.0.0:8888/api cypress:test",
"start": "statping -port 8888"
"start": "statping -port 8888 > /dev/null 2>&1"
},
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",

@ -14,6 +14,46 @@ HTML,BODY {
transition: height 0.3s ease;
}
.slider-info {
font-size: 9pt;
font-weight: bold;
}
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 5px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
border-radius: 50%;
width: 20px; /* Set a specific slider handle width */
height: 20px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 15px; /* Set a specific slider handle width */
height: 15px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
@-o-keyframes fadeIt {
0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; }
@ -548,6 +588,9 @@ HTML,BODY {
background-color: white;
transition: 0.2s all;
}
.switch-rd-gr input:checked + label::before {
background-color: #cd141b;
}
.switch input:checked + label::before {
background-color: #08d;
}

@ -69,8 +69,8 @@
<div class="form-group row">
<label for="switch-group-public" class="col-sm-4 col-form-label">Enabled</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="enabled = !!enabled" class="switch float-left">
<input v-model="enabled" type="checkbox" class="switch" id="switch-group-public" :checked="enabled">
<span class="switch float-left">
<input type="checkbox" class="switch" id="switch-group-public">
<label for="switch-group-public">Enabled Github Auth</label>
</span>
</div>

@ -1,14 +1,16 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card-header text-capitalize">{{notifier.title}}</div>
<div>
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card-header text-capitalize">
{{notifier.title}}
<span @click="enableToggle" class="switch switch-rd-gr float-right">
<input v-model="notifier.enabled" type="checkbox" class="switch-sm" :id="`enable_${notifier.method}`" v-bind:checked="notifier.enabled">
<label class="mb-0" :for="`enable_${notifier.method}`"></label>
</span>
</div>
<div class="card-body">
<form @submit.prevent="saveNotifier">
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
<div v-if="ok" class="alert alert-success col-12" role="alert">
<i class="fa fa-smile-beam"></i> The {{notifier.method}} notifier is working correctly!
</div>
<form @submit.prevent="saveNotifier">
<p class="small text-muted" v-html="notifier.description"/>
@ -20,43 +22,44 @@
</div>
<div class="row mt-4">
<div class="col-9 col-sm-6">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text">Limit</div>
</div>
<input v-model="notifier.limits" type="number" class="form-control" name="limits" min="1" max="60" placeholder="7">
<div class="input-group-append">
<div class="input-group-text">Per Minute</div>
</div>
</div>
</div>
<div class="col-3 col-sm-2 mt-1">
<span @click="notifier.enabled = !!notifier.enabled" class="switch">
<input type="checkbox" name="enabled-option" class="switch" v-model="notifier.enabled" v-bind:id="`switch-${notifier.method}`" v-bind:checked="notifier.enabled">
<label v-bind:for="`switch-${notifier.method}`"></label>
</span>
</div>
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize" :class="{'btn-primary': !saved, 'btn-success': saved}">
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}}
</button>
</div>
<div class="col-12 col-sm-12 mt-3">
<button @click.prevent="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i>
{{loadingTest ? "Loading..." : "Test Notifier"}}</button>
<div class="col-sm-12">
<span class="slider-info">Limit {{notifier.limits}} per hour</span>
<input v-model="notifier.limits" type="range" name="limits" class="slider" min="1" max="300">
<small class="form-text text-muted">Notifier '{{notifier.title}}' will send a maximum of {{notifier.limits}} notifications per hour.</small>
</div>
</div>
</form>
</div>
</div>
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
<div v-if="success" class="alert alert-success col-12" role="alert">{{notifier.title}} appears to be working!</div>
<div class="card text-black-50 bg-white mb-3">
<div class="card-body">
<div class="row">
<div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize btn-primary">
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save Settings"}}
</button>
</div>
<div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier" class="btn btn-outline-dark btn-block text-capitalize"><i class="fa fa-vial"></i>
{{loadingTest ? "Loading..." : "Test Notifier"}}</button>
</div>
</div>
</div>
</div>
<span class="d-block small text-center mb-3">
<span class="text-capitalize">{{notifier.title}}</span> Notifier created by <a :href="notifier.author_url" target="_blank">{{notifier.author}}</a>
</span>
</div>
</template>
@ -76,22 +79,27 @@ export default {
loading: false,
loadingTest: false,
error: null,
success: false,
saved: false,
ok: false,
form: {},
}
},
mounted() {
},
methods: {
async enableToggle() {
this.notifier.enabled = !!this.notifier.enabled
const form = {
enabled: !this.notifier.enabled,
method: this.notifier.method,
}
await Api.notifier_save(form)
},
async saveNotifier() {
this.loading = true
this.form.enabled = this.notifier.enabled
this.form.limits = parseInt(this.notifier.limits)
this.form.method = this.notifier.method
this.notifier.form.forEach((f) => {
let field = f.field.toLowerCase()
let field = f.field.toLowerCase()
let val = this.notifier[field]
if (this.isNumeric(val)) {
val = parseInt(val)
@ -99,34 +107,28 @@ export default {
this.form[field] = val
});
await Api.notifier_save(this.form)
// const notifiers = await Api.notifiers()
// await this.$store.commit('setNotifiers', notifiers)
const notifiers = await Api.notifiers()
await this.$store.commit('setNotifiers', notifiers)
this.saved = true
this.loading = false
setTimeout(() => {
this.saved = false
}, 2000)
},
async testNotifier() {
this.ok = false
this.success = false
this.loadingTest = true
let form = {}
this.form.method = this.notifier.method
this.notifier.form.forEach((f) => {
let field = f.field.toLowerCase()
let val = this.notifier[field]
if (this.isNumeric(val)) {
val = parseInt(val)
}
let val = this.notifier[field]
if (this.isNumeric(val)) {
val = parseInt(val)
}
this.form[field] = val
});
this.form.enabled = this.notifier.enabled
this.form.limits = parseInt(this.notifier.limits)
this.form.method = this.notifier.method
const tested = await Api.notifier_test(this.form)
if (tested === 'ok') {
this.ok = true
if (tested.success) {
this.success = true
} else {
this.error = tested
this.error = tested.error
}
this.loadingTest = false
},

@ -1,35 +1,28 @@
<template>
<form @submit.prevent="saveService">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Basic Information</div>
<div class="card-header">Service Information</div>
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input v-model="service.name" @keypress="service.permalink=service.name.split(' ').join('_')" type="text" name="name" class="form-control" placeholder="Name" required spellcheck="false" autocorrect="off">
<input v-model="service.name" @input="updatePermalink" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8">
<select v-model="service.type" class="form-control" id="service_type" >
<select v-model="service.type" class="form-control" id="service_type">
<option value="http">HTTP Service</option>
<option value="grpc">gRPC Service</option>
<option value="tcp">TCP Service</option>
<option value="udp">UDP Service</option>
<option value="icmp">ICMP Ping</option>
<option value="grpc">gRPC Service</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8">
<input v-model="service.domain" type="text" class="form-control" id="service_url" placeholder="https://google.com" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this URL</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Group</label>
<div class="col-sm-8">
@ -40,26 +33,79 @@
<small class="form-text text-muted">Attach this service to a group</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Permalink URL</label>
<div class="col-sm-8">
<input v-model="service.permalink" type="text" name="permalink" class="form-control" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Public Service</label>
<div class="col-8 mt-1">
<span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
<label v-if="service.public" for="switch-public">This service will be visible for everyone</label>
<label v-if="!service.public" for="switch-public">This service will only be visible for users and administrators.</label>
</span>
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval</label>
<div class="col-sm-8">
<span class="slider-info">{{secondsHumanize(service.check_interval)}}</span>
<input v-model="service.check_interval" type="range" class="slider" id="service_interval" min="1" max="1800" :step="stepVal(service.check_interval)">
<small id="interval" class="form-text text-muted">Interval to check your service state</small>
</div>
</div>
</div>
</div>
<div v-if="service.type !== 'icmp'" class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Request Details</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">{{service.type.toUpperCase()}} Request Details</div>
<div class="card-body">
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Service Check Type</label>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Service Endpoint {{service.type === 'http' ? "(URL)" : "(Domain)"}}</label>
<div class="col-sm-8">
<input v-model="service.domain" type="text" class="form-control" id="service_url" :placeholder="service.type === 'http' ? 'https://google.com' : '192.168.1.1'" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this address</small>
</div>
</div>
<div v-if="service.type.match(/^(tcp|udp|grpc)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">{{service.type.toUpperCase()}} Port</label>
<div class="col-sm-8">
<input v-model="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select v-model="service.method" name="method" class="form-control">
<option value="GET" >GET</option>
<option value="POST" >POST</option>
<option value="DELETE" >DELETE</option>
<option value="PATCH" >PATCH</option>
<option value="PUT" >PUT</option>
</select>
<small class="form-text text-muted">A GET request will simply request the endpoint, you can also send data with POST.</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Request Timeout</label>
<div class="col-sm-8">
<select v-model="service.method" name="method" class="form-control">
<option value="GET" >GET</option>
<option value="POST" >POST</option>
<option value="DELETE" >DELETE</option>
<option value="PATCH" >PATCH</option>
<option value="PUT" >PUT</option>
</select>
<small class="form-text text-muted">A GET request will simply request the endpoint, you can also send data with POST.</small>
<span class="slider-info">{{secondsHumanize(service.timeout)}}</span>
<input v-model="service.timeout" type="range" name="timeout" class="slider" min="1" max="180">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/) && service.method.match(/^(POST|PATCH|DELETE|PUT)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8">
@ -88,83 +134,56 @@
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
</div>
</div>
<div v-if="service.type.match(/^(tcp|udp)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">{{service.type.toUpperCase()}} Port</label>
<div class="col-sm-8">
<input v-model="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080">
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
<label for="switch-verify-ssl" v-if="service.verify_ssl">Verify SSL Certificate for this service</label>
<label for="switch-verify-ssl" v-if="!service.verify_ssl">Skip SSL Certificate verification for this service</label>
</span>
</div>
</div>
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Additional Options</div>
<div class="card-header">Notification Options</div>
<div class="card-body">
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input v-model="service.check_interval" type="number" class="form-control" min="1" id="service_interval" required>
<small id="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input v-model="service.timeout" type="number" name="timeout" class="form-control" placeholder="15" min="1">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Permalink URL</label>
<div class="col-sm-8">
<input v-model="service.permalink" type="text" name="permalink" class="form-control" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
<label for="switch-verify-ssl">Verify SSL Certificate for this service</label>
</span>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-8 mt-1">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
<label for="switch-notifications">Allow notifications to be sent for this service</label>
</span>
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify After Failures</label>
<div class="col-sm-8">
<span class="slider-info">{{service.notify_after === 0 ? "First Failure" : service.notify_after+' Failures'}}</span>
<input v-model="service.notify_after" type="range" name="notify_after" class="slider" id="notify_after" min="0" max="20">
<small class="form-text text-muted">Send Notification after {{service.notify_after === 0 ? 'the first Failure' : service.notify_after+' Failures'}} </small>
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label v-if="service.notify_all_changes" for="notify_all">Continuously send notifications when service is failing.</label>
<label v-if="!service.notify_all_changes" for="notify_all">Only notify one time when service hits an error</label>
</span>
</div>
</div>
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify After Failures</label>
<div class="col-sm-8">
<input v-model="service.notify_after" type="number" name="notify_after" class="form-control" id="notify_after" autocapitalize="none">
<small class="form-text text-muted">Send Notification after {{service.notify_after === 0 ? 'the first Failure' : service.notify_after+' Failures'}} </small>
</div>
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label for="notify_all">Continuously notify when service is failing.</label>
</span>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1">
<span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
<label for="switch-public">Show service details to the public</label>
</span>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
@ -172,8 +191,7 @@
</button>
</div>
</div>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</template>
@ -227,6 +245,30 @@
}
},
methods: {
updatePermalink() {
const a = 'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;'
const b = 'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------'
const p = new RegExp(a.split('').join('|'), 'g')
this.service.permalink = this.service.name.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, '-and-') // Replace & with 'and'
.replace(/[^\w\-]+/g, '') // Remove all non-word characters
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, '') // Trim - from end of text
},
stepVal(val) {
if (val > 1800) {
return 300
} else if (val > 300) {
return 60
} else if (val > 120) {
return 10
}
return 1
},
async saveService () {
let s = this.service
this.loading = true

@ -3,6 +3,7 @@ const { zonedTimeToUtc, utcToZonedTime, lastDayOfMonth, subSeconds, parse, getUn
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO'
import addSeconds from 'date-fns/addSeconds'
export default Vue.mixin({
methods: {
@ -15,6 +16,17 @@ export default Vue.mixin({
current() {
return parseISO(new Date())
},
secondsHumanize (val) {
const t2 = addSeconds(new Date(0), val)
if (val >= 60) {
let minword = "minute"
if (val >= 120) {
minword = "minutes"
}
return format(t2, "m '"+minword+"' s 'seconds'")
}
return format(t2, "s 'seconds'")
},
utc(val) {
return new Date.UTC(val)
},

@ -24,7 +24,26 @@
</a>
</div>
<h6 class="mt-4 mb-3 text-muted">Statping Links</h6>
<a href="https://github.com/statping/statping/wiki" class="mb-2 font-2 text-decoration-none text-muted">
<font-awesome-icon icon="question" class="mr-3"/> Documentation
</a>
<a href="https://github.com/statping/statping/wiki/API" class="mb-2 font-2 text-decoration-none text-muted">
<font-awesome-icon icon="laptop" class="mr-2"/> API Documentation
</a>
<a href="https://raw.githubusercontent.com/statping/statping/master/CHANGELOG.md" class="mb-2 font-2 text-decoration-none text-muted">
<font-awesome-icon icon="book" class="mr-3"/> Changelog
</a>
<a href="https://github.com/statping/statping" class="mb-2 font-2 text-decoration-none text-muted">
<font-awesome-icon icon="code-branch" class="mr-3"/> Statping Github Repo
</a>
</div>
</div>
<div class="col-md-9 col-sm-12">

@ -40,28 +40,52 @@ const routes = [
}
},{
path: 'users',
component: DashboardUsers
component: DashboardUsers,
meta: {
requiresAuth: true
}
},{
path: 'services',
component: DashboardServices
component: DashboardServices,
meta: {
requiresAuth: true
}
},{
path: 'create_service',
component: EditService
component: EditService,
meta: {
requiresAuth: true
}
},{
path: 'edit_service/:id',
component: EditService
component: EditService,
meta: {
requiresAuth: true
}
},{
path: 'messages',
component: DashboardMessages
component: DashboardMessages,
meta: {
requiresAuth: true
}
},{
path: 'settings',
component: Settings
component: Settings,
meta: {
requiresAuth: true
}
},{
path: 'logs',
component: Logs
component: Logs,
meta: {
requiresAuth: true
}
},{
path: 'help',
component: Logs
component: Logs,
meta: {
requiresAuth: true
}
}]
},
{

@ -1,4 +1,5 @@
module.exports = {
baseUrl: '/',
assetsDir: 'assets',
filenameHashing: false,
devServer: {

@ -112,7 +112,11 @@ func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
}
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request, statusCode ...int) {
log.Warnln(fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
log.WithField("url", r.URL.String()).
WithField("method", r.Method).
WithField("code", statusCode).
Errorln(fmt.Errorf("sending error response for %s: %s", r.URL.String(), err.Error()))
output := apiResponse{
Status: "error",
Error: err.Error(),

@ -2,18 +2,24 @@ package handlers
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"net/http"
"sort"
)
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
var notifs []notifications.Notification
notifiers := services.AllNotifiers()
returnJson(notifiers, w, r)
for _, n := range notifiers {
notif := n.Select()
notifer, _ := notifications.Find(notif.Method)
notif.UpdateFields(notifer)
notifs = append(notifs, *notif)
}
sort.Sort(notifications.NotificationOrder(notifs))
returnJson(notifs, w, r)
}
func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
@ -24,8 +30,7 @@ func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
notif = notif.UpdateFields(notifer)
returnJson(notif, w, r)
returnJson(notifer, w, r)
}
func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
@ -36,14 +41,12 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
var notif *notifications.Notification
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notif)
err = decoder.Decode(&notifer)
if err != nil {
sendErrorJson(err, w, r)
return
}
notifer = notifer.UpdateFields(notif)
err = notifer.Update()
if err != nil {
sendErrorJson(err, w, r)
@ -54,63 +57,31 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
var err error
form := parseForm(r)
vars := mux.Vars(r)
method := vars["method"]
enabled := form.Get("enable")
host := form.Get("host")
port := int(utils.ToInt(form.Get("port")))
username := form.Get("username")
password := form.Get("password")
var1 := form.Get("var1")
var2 := form.Get("var2")
apiKey := form.Get("api_key")
apiSecret := form.Get("api_secret")
limits := int(utils.ToInt(form.Get("limits")))
notifer, err := notifications.Find(vars["notifier"])
if err != nil {
sendErrorJson(err, w, r)
return
}
notifier, err := notifications.Find(method)
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notifer)
if err != nil {
log.Errorln(fmt.Sprintf("issue saving notifier %v: %v", method, err))
sendErrorJson(err, w, r)
return
}
n := notifier
notif := services.ReturnNotifier(notifer.Method)
err = notif.OnTest()
if host != "" {
n.Host = host
}
if port != 0 {
n.Port = port
resp := &notifierTestResp{
Success: err == nil,
Error: err,
}
if username != "" {
n.Username = username
}
if password != "" && password != "##########" {
n.Password = password
}
if var1 != "" {
n.Var1 = var1
}
if var2 != "" {
n.Var2 = var2
}
if apiKey != "" {
n.ApiKey = apiKey
}
if apiSecret != "" {
n.ApiSecret = apiSecret
}
if limits != 0 {
n.Limits = limits
}
n.Enabled = null.NewNullBool(enabled == "on")
returnJson(resp, w, r)
}
//err = notifications.OnTest(notifier)
//if err == nil {
// w.Write([]byte("ok"))
//} else {
// w.Write([]byte(err.Error()))
//}
type notifierTestResp struct {
Success bool `json:"success"`
Error error `json:"error,omitempty"`
}

@ -136,7 +136,7 @@ func Router() *mux.Router {
api.Handle("/api/notifiers", authenticated(apiNotifiersHandler, false)).Methods("GET")
api.Handle("/api/notifier/{notifier}", authenticated(apiNotifierGetHandler, false)).Methods("GET")
api.Handle("/api/notifier/{notifier}", authenticated(apiNotifierUpdateHandler, false)).Methods("POST")
api.Handle("/api/notifier/{method}/test", authenticated(testNotificationHandler, false)).Methods("POST")
api.Handle("/api/notifier/{notifier}/test", authenticated(testNotificationHandler, false)).Methods("POST")
// API MESSAGES Routes
api.Handle("/api/messages", scoped(apiAllMessagesHandler)).Methods("GET")

@ -119,7 +119,7 @@ func TestApiServiceRoutes(t *testing.T) {
Name: "Statping Service 1 Failure Data - 1 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=1h",
Method: "GET",
ResponseLen: 72,
ResponseLen: 73,
ExpectedStatus: 200,
},
{
@ -140,7 +140,7 @@ func TestApiServiceRoutes(t *testing.T) {
Name: "Statping Service 1 Failure Data",
URL: "/api/services/1/failure_data" + startEndQuery,
Method: "GET",
ResponseLen: 72,
ResponseLen: 73,
ExpectedStatus: 200,
},
{

@ -65,8 +65,7 @@ func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
err = user.Delete()
if err != nil {
if err := user.Delete(); err != nil {
sendErrorJson(err, w, r)
return
}

@ -31,7 +31,7 @@ func (s *slack) Select() *notifications.Notification {
var slacker = &slack{&notifications.Notification{
Method: slackMethod,
Title: "slack",
Description: "Send notifications to your slack channel when a service is offline. Insert your Incoming webhooker URL for your channel to receive notifications. Based on the <a href=\"https://api.slack.com/incoming-webhooks\">slack API</a>.",
Description: "Send notifications to your slack channel when a service is offline. Insert your Incoming webhook URL for your channel to receive notifications. Based on the <a href=\"https://api.slack.com/incoming-webhooks\">Slack API</a>.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(10 * time.Second),
@ -40,9 +40,9 @@ var slacker = &slack{&notifications.Notification{
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
Title: "Incoming webhooker Url",
Placeholder: "Insert your slack Webhook URL here.",
SmallText: "Incoming webhooker URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">slack Apps</a>",
Title: "Incoming Webhook Url",
Placeholder: "Insert your Slack Webhook URL here.",
SmallText: "Incoming Webhook URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">Slack Apps</a>",
DbField: "Host",
Required: true,
}}},

@ -54,8 +54,3 @@ func (c *Checkin) Delete() error {
q = db.Model(&Checkin{}).Delete(c)
return q.Error()
}
//func (c *Checkin) AfterDelete() error {
// //q := dbHits.Where("checkin = ?", c.Id).Delete(&CheckinHit{})
// return q.Error()
//}

@ -2,12 +2,13 @@ package checkins
import (
"fmt"
"github.com/statping/statping/utils"
"time"
)
func (c *Checkin) Expected() time.Duration {
last := c.LastHit()
now := time.Now().UTC()
now := utils.Now()
lastDir := now.Sub(last.CreatedAt)
sub := time.Duration(c.Period() - lastDir)
return sub

@ -13,18 +13,6 @@ func SetDB(database database.Database) {
db = database.Model(&Hit{})
}
func Find(id int64) (*Hit, error) {
var group Hit
q := db.Where("id = ?", id).Find(&group)
return &group, q.Error()
}
func All() []*Hit {
var hits []*Hit
db.Find(&hits)
return hits
}
func (h *Hit) Create() error {
q := db.Create(h)
return q.Error()

@ -14,12 +14,13 @@ func SetDB(database database.Database) {
}
func Find(method string) (*Notification, error) {
var notification Notification
q := db.Where("method = ?", method).Find(&notification)
if &notification == nil {
var n Notification
q := db.Where("method = ?", method).Find(&n)
if &n == nil {
return nil, errors.New("cannot find notifier")
}
return &notification, q.Error()
n.UpdateFields(&n)
return &n, q.Error()
}
func (n *Notification) Create() error {
@ -33,6 +34,7 @@ func (n *Notification) Create() error {
}
func (n *Notification) UpdateFields(notif *Notification) *Notification {
n.Limits = notif.Limits
n.Enabled = notif.Enabled
n.Host = notif.Host
n.Port = notif.Port
@ -46,21 +48,8 @@ func (n *Notification) UpdateFields(notif *Notification) *Notification {
}
func (n *Notification) Update() error {
if err := db.Update(n); err.Error() != nil {
return err.Error()
}
n.ResetQueue()
if n.Enabled.Bool {
n.Close()
n.Start()
} else {
n.Close()
if err := db.Update(n).Error(); err != nil {
return err
}
return nil
}
func loadAll() []*Notification {
var notifications []*Notification
db.Find(&notifications)
return notifications
}

@ -38,22 +38,6 @@ func (n *Notification) LastSent() time.Duration {
return since
}
func SelectNotifier(n *Notification) *Notification {
notif, err := Find(n.Method)
if err != nil {
log.Errorln(err)
return n
}
n.Host = notif.Host
n.Username = notif.Username
n.Password = notif.Password
n.ApiSecret = notif.ApiSecret
n.ApiKey = notif.ApiKey
n.Var1 = notif.Var1
n.Host = notif.Var2
return n
}
func (n *Notification) CanSend() bool {
if !n.Enabled.Bool {
return false
@ -87,13 +71,11 @@ func (n *Notification) GetValue(dbField string) string {
case "host":
return n.Host
case "port":
return fmt.Sprintf("%v", n.Port)
return fmt.Sprintf("%d", n.Port)
case "username":
return n.Username
case "password":
if n.Password != "" {
return "##########"
}
return n.Password
case "var1":
return n.Var1
case "var2":
@ -104,13 +86,9 @@ func (n *Notification) GetValue(dbField string) string {
return n.ApiSecret
case "limits":
return utils.ToString(int(n.Limits))
default:
return ""
}
return ""
}
// ResetQueue will clear the notifiers Queue
func (n *Notification) ResetQueue() {
n.Queue = nil
}
// start will start the go routine for the notifier queue

@ -1,42 +1,43 @@
package notifications
import (
"github.com/sirupsen/logrus"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"time"
)
var (
log = utils.Log
log = utils.Log.WithField("type", "notifier")
)
// Notification contains all the fields for a Statping Notifier.
type Notification struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Method string `gorm:"column:method" json:"method"`
Host string `gorm:"not null;column:host" json:"host,omitempty"`
Port int `gorm:"not null;column:port" json:"port,omitempty"`
Username string `gorm:"not null;column:username" json:"username,omitempty"`
Password string `gorm:"not null;column:password" json:"password,omitempty"`
Var1 string `gorm:"not null;column:var1" json:"var1,omitempty"`
Var2 string `gorm:"not null;column:var2" json:"var2,omitempty"`
ApiKey string `gorm:"not null;column:api_key" json:"api_key,omitempty"`
ApiSecret string `gorm:"not null;column:api_secret" json:"api_secret,omitempty"`
Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Limits int `gorm:"not null;column:limits" json:"limits"`
Removable bool `gorm:"column:removable" json:"removeable"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Form []NotificationForm `gorm:"-" json:"form"`
Title string `gorm:"-" json:"title"`
Description string `gorm:"-" json:"description"`
Author string `gorm:"-" json:"author"`
AuthorUrl string `gorm:"-" json:"author_url"`
Icon string `gorm:"-" json:"icon"`
Delay time.Duration `gorm:"-" json:"delay,string"`
Running chan bool `gorm:"-" json:"-"`
Id int64 `gorm:"primary_key;column:id" json:"id"`
Method string `gorm:"column:method" json:"method"`
Host string `gorm:"not null;column:host" json:"host,omitempty"`
Port int `gorm:"not null;column:port" json:"port,omitempty"`
Username string `gorm:"not null;column:username" json:"username,omitempty"`
Password string `gorm:"not null;column:password" json:"password,omitempty"`
Var1 string `gorm:"not null;column:var1" json:"var1,omitempty"`
Var2 string `gorm:"not null;column:var2" json:"var2,omitempty"`
ApiKey string `gorm:"not null;column:api_key" json:"api_key,omitempty"`
ApiSecret string `gorm:"not null;column:api_secret" json:"api_secret,omitempty"`
Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Limits int `gorm:"not null;column:limits" json:"limits"`
Removable bool `gorm:"column:removable" json:"removable"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Title string `gorm:"-" json:"title"`
Description string `gorm:"-" json:"description"`
Author string `gorm:"-" json:"author"`
AuthorUrl string `gorm:"-" json:"author_url"`
Icon string `gorm:"-" json:"icon"`
Delay time.Duration `gorm:"-" json:"delay,string"`
Running chan bool `gorm:"-" json:"-"`
Queue []RunFunc `gorm:"-" json:"-"`
Form []NotificationForm `gorm:"-" json:"form"`
Queue []RunFunc `gorm:"-" json:"-"`
lastSent time.Time `gorm:"-" json:"-"`
lastSentCount int `gorm:"-" json:"-"`
@ -44,6 +45,10 @@ type Notification struct {
Hits notificationHits `gorm:"-" json:"-"`
}
func (n *Notification) Logger() *logrus.Logger {
return log.WithField("notifier", n.Method).Logger
}
type RunFunc func(interface{}) error
// NotificationForm contains the HTML fields for each variable/input you want the notifier to accept.
@ -72,3 +77,11 @@ type notificationHits struct {
OnNewNotifier int64 `gorm:"-" json:"-"`
OnUpdatedNotifier int64 `gorm:"-" json:"-"`
}
// NotificationOrder will reorder the services based on 'order_id' (Order)
type NotificationOrder []Notification
// Sort interface for resorting the Notifications in order
func (c NotificationOrder) Len() int { return len(c) }
func (c NotificationOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] }
func (c NotificationOrder) Less(i, j int) bool { return c[i].Id < c[j].Id }

@ -3,33 +3,33 @@ package null
import "encoding/json"
// MarshalJSON for NullInt64
func (ni NullInt64) MarshalJSON() ([]byte, error) {
if !ni.Valid {
func (i NullInt64) MarshalJSON() ([]byte, error) {
if !i.Valid {
return []byte("null"), nil
}
return json.Marshal(ni.Int64)
return json.Marshal(i.Int64)
}
// MarshalJSON for NullFloat64
func (ni NullFloat64) MarshalJSON() ([]byte, error) {
if !ni.Valid {
func (f NullFloat64) MarshalJSON() ([]byte, error) {
if !f.Valid {
return []byte("null"), nil
}
return json.Marshal(ni.Float64)
return json.Marshal(f.Float64)
}
// MarshalJSON for NullBool
func (nb NullBool) MarshalJSON() ([]byte, error) {
if !nb.Valid {
func (bb NullBool) MarshalJSON() ([]byte, error) {
if !bb.Valid {
return []byte("null"), nil
}
return json.Marshal(nb.Bool)
return json.Marshal(bb.Bool)
}
// MarshalJSON for NullString
func (ns NullString) MarshalJSON() ([]byte, error) {
if !ns.Valid {
func (s NullString) MarshalJSON() ([]byte, error) {
if !s.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.String)
return json.Marshal(s.String)
}

@ -3,29 +3,29 @@ package null
import "encoding/json"
// Unmarshaler for NullInt64
func (nf *NullInt64) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &nf.Int64)
nf.Valid = (err == nil)
func (i *NullInt64) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &i.Int64)
i.Valid = (err == nil)
return err
}
// Unmarshaler for NullFloat64
func (nf *NullFloat64) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &nf.Float64)
nf.Valid = (err == nil)
func (f *NullFloat64) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &f.Float64)
f.Valid = (err == nil)
return err
}
// Unmarshaler for NullBool
func (nf *NullBool) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &nf.Bool)
nf.Valid = (err == nil)
func (bb *NullBool) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &bb.Bool)
bb.Valid = (err == nil)
return err
}
// Unmarshaler for NullString
func (nf *NullString) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &nf.String)
nf.Valid = (err == nil)
func (s *NullString) UnmarshalJSON(b []byte) error {
err := json.Unmarshal(b, &s.String)
s.Valid = (err == nil)
return err
}

@ -8,9 +8,10 @@ import (
"sort"
)
var log = utils.Log
var db database.Database
var (
db database.Database
log = utils.Log.WithField("type", "service")
)
func SetDB(database database.Database) {
db = database.Model(&Service{})
@ -60,38 +61,23 @@ func (s *Service) AfterCreate() error {
func (s *Service) Update() error {
q := db.Update(s)
allServices[s.Id] = s
if !s.AllowNotifications.Bool {
//for _, n := range CoreApp.Notifications {
// notif := n.(notifier.Notifier).Select()
// notif.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
//}
}
s.Close()
s.SleepDuration = s.Duration()
go ServiceCheckQueue(allServices[s.Id], true)
//notifier.OnUpdatedService(s.Service)
return q.Error()
}
func (s *Service) Delete() error {
s.Close()
if err := s.DeleteFailures(); err != nil {
return err
}
if err := s.DeleteHits(); err != nil {
return err
}
delete(allServices, s.Id)
q := db.Model(&Service{}).Delete(s)
//notifier.OnDeletedService(s.Service)
return q.Error()
}
@ -111,12 +97,3 @@ func (s *Service) DeleteCheckins() error {
}
return nil
}
//func (s *Service) AfterDelete() error {
//
// return nil
//}
func (s *Service) AfterFind() error {
return nil
}

@ -6,19 +6,21 @@ import (
)
var (
allNotifiers []ServiceNotifier
allNotifiers = make(map[string]ServiceNotifier)
)
func AllNotifiers() []ServiceNotifier {
func AllNotifiers() map[string]ServiceNotifier {
return allNotifiers
}
func ReturnNotifier(method string) ServiceNotifier {
return allNotifiers[method]
}
func FindNotifier(method string) *notifications.Notification {
for _, n := range allNotifiers {
notif := n.Select()
if notif.Method == method {
return notif
}
n := allNotifiers[method]
if n != nil {
return n.Select()
}
return nil
}

@ -3,7 +3,6 @@ package services
import (
"bytes"
"fmt"
"github.com/statping/statping/types/core"
"google.golang.org/grpc"
"net"
"net/http"
@ -31,7 +30,7 @@ func CheckServices() {
// CheckQueue is the main go routine for checking a service
func ServiceCheckQueue(s *Service, record bool) {
s.Start()
s.Checkpoint = time.Now()
s.Checkpoint = utils.Now()
s.SleepDuration = (time.Duration(s.Id) * 100) * time.Millisecond
CheckLoop:
@ -56,7 +55,7 @@ CheckLoop:
}
func parseHost(s *Service) string {
if s.Type == "tcp" || s.Type == "udp" {
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" {
return s.Domain
} else {
u, err := url.Parse(s.Domain)
@ -72,7 +71,7 @@ func dnsCheck(s *Service) (int64, error) {
var err error
t1 := utils.Now()
host := parseHost(s)
if s.Type == "tcp" {
if s.Type == "tcp" || s.Type == "udp" || s.Type == "grpc" {
_, err = net.LookupHost(host)
} else {
_, err = net.LookupIP(host)
@ -80,7 +79,7 @@ func dnsCheck(s *Service) (int64, error) {
if err != nil {
return 0, err
}
t2 := time.Now()
t2 := utils.Now()
subTime := t2.Sub(t1).Microseconds()
return subTime, err
}
@ -225,19 +224,27 @@ func CheckHttp(s *Service, record bool) *Service {
timeout := time.Duration(s.Timeout) * time.Second
var content []byte
var res *http.Response
var cnx string
var data *bytes.Buffer
var headers []string
if s.Headers.Valid {
headers = strings.Split(s.Headers.String, ",")
} else {
headers = nil
}
if s.Method == "POST" {
content, res, err = utils.HttpRequest(s.Domain, s.Method, "application/json", headers, bytes.NewBuffer([]byte(s.PostData.String)), timeout, s.VerifySSL.Bool)
if s.PostData.String != "" {
data = bytes.NewBuffer([]byte(s.PostData.String))
} else {
content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, headers, nil, timeout, s.VerifySSL.Bool)
data = bytes.NewBuffer(nil)
}
if s.Method == "POST" {
cnx = "application/json"
}
content, res, err = utils.HttpRequest(s.Domain, s.Method, cnx, headers, data, timeout, s.VerifySSL.Bool)
if err != nil {
if record {
recordFailure(s, fmt.Sprintf("HTTP Error %v", err))
@ -295,7 +302,8 @@ func recordSuccess(s *Service) {
}
func AddNotifier(n ServiceNotifier) {
allNotifiers = append(allNotifiers, n)
notif := n.Select()
allNotifiers[notif.Method] = n
}
func sendSuccess(s *Service) {
@ -307,17 +315,12 @@ func sendSuccess(s *Service) {
return
}
// dont send notification if server recently started (60 seconds)
if core.App.Started.Add(60 * time.Second).After(utils.Now()) {
s.SuccessNotified = true
return
}
for _, n := range allNotifiers {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending notification to: %s!", notif.Method)
if err := n.OnSuccess(s); err != nil {
log.Errorln(err)
notif.Logger().Errorln(err)
}
s.UserNotified = true
s.SuccessNotified = true
@ -367,7 +370,7 @@ func sendFailure(s *Service, f *failures.Failure) {
if notif.CanSend() {
log.Infof("Sending Failure notification to: %s!", notif.Method)
if err := n.OnFailure(s, f); err != nil {
log.Errorln(err)
notif.Logger().WithField("failure", f.Issue).Errorln(err)
}
s.UserNotified = true
s.SuccessNotified = true

@ -15,7 +15,7 @@ func AuthUser(username, password string) (*User, bool) {
log.Warnln(fmt.Errorf("user %v not found", username))
return nil, false
}
if CheckHash(password, user.Password) {
if checkHash(password, user.Password) {
user.UpdatedAt = time.Now().UTC()
user.Update()
return user, true
@ -23,8 +23,8 @@ func AuthUser(username, password string) (*User, bool) {
return nil, false
}
// CheckHash returns true if the password matches with a hashed bcrypt password
func CheckHash(password, hash string) bool {
// checkHash returns true if the password matches with a hashed bcrypt password
func checkHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

@ -21,9 +21,40 @@ var (
LastLines []*logRow
LockLines sync.Mutex
VerboseMode int
version string
)
const logFilePath = "/logs/statping.log"
const (
logFilePath = "/logs/statping.log"
errorReporter = "https://ddf2784201134d51a20c3440e222cebe@sentry.statping.com/4"
)
func SentryInit(v string) {
if v == "" {
v = "development"
}
version = v
errorEnv := Getenv("GO_ENV", "production").(string)
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: errorEnv,
Release: v,
}); err != nil {
Log.Errorln(err)
}
}
func SentryErr(err error) {
sentry.CaptureException(err)
}
func SentryLogEntry(entry *Logger.Entry) {
e := sentry.NewEvent()
e.Message = entry.Message
e.Release = version
e.Contexts = entry.Data
sentry.CaptureEvent(e)
}
type hook struct {
Entries []Logger.Entry
@ -32,6 +63,9 @@ type hook struct {
func (t *hook) Fire(e *Logger.Entry) error {
pushLastLine(e.Message)
if e.Level == Logger.ErrorLevel {
SentryLogEntry(e)
}
return nil
}
@ -120,8 +154,6 @@ func InitLogs() error {
})
checkVerboseMode()
sentry.CaptureMessage("It works!")
LastLines = make([]*logRow, 0)
return nil
}
@ -154,6 +186,7 @@ func CloseLogs() {
ljLogger.Rotate()
Log.Writer().Close()
ljLogger.Close()
sentry.Flush(5 * time.Second)
}
func pushLastLine(line interface{}) {

@ -44,6 +44,24 @@ func init() {
checkVerboseMode()
}
type env struct {
data interface{}
}
func GetenvAs(key string, defaultValue interface{}) *env {
return &env{
data: Getenv(key, defaultValue),
}
}
func (e *env) Duration() time.Duration {
t, err := time.ParseDuration(e.data.(string))
if err != nil {
Log.Errorln(err)
}
return t
}
func Getenv(key string, defaultValue interface{}) interface{} {
if val, ok := os.LookupEnv(key); ok {
if val != "" {
@ -216,7 +234,7 @@ func DurationReadable(d time.Duration) string {
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool) ([]byte, *http.Response, error) {
var err error
var req *http.Request
t1 := time.Now()
t1 := Now()
if req, err = http.NewRequest(method, url, body); err != nil {
httpMetric.Errors++
return nil, nil, err
@ -275,7 +293,7 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
contents, err := ioutil.ReadAll(resp.Body)
// record HTTP metrics
t2 := time.Now().Sub(t1).Milliseconds()
t2 := Now().Sub(t1).Milliseconds()
httpMetric.Requests++
httpMetric.Milliseconds += t2 / httpMetric.Requests
httpMetric.Bytes += int64(len(contents))

@ -1 +1 @@
0.90.21
0.90.22

Loading…
Cancel
Save