design changes, form updates, API fixes

pull/483/head
Hunter Long 2020-04-03 17:28:09 -07:00
parent 514d0f59e8
commit efcdd9fdd8
37 changed files with 600 additions and 548 deletions

View File

@ -28,7 +28,7 @@ env:
go: 1.14 go: 1.14
go_import_path: github.com/statping/statping go_import_path: github.com/statping/statping
install: 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" - "pip install --user awscli"
- "go get github.com/mattn/goveralls" - "go get github.com/mattn/goveralls"
- "go mod download" - "go mod download"
@ -49,7 +49,6 @@ os:
- linux - linux
script: script:
- "travis_retry make clean test-ci" - "travis_retry make clean test-ci"
- "travis_retry make test-cypress"
- "if [[ \"$TRAVIS_BRANCH\" == \"master\" && \"$TRAVIS_PULL_REQUEST\" = \"false\" ]]; then make coverage; fi" - "if [[ \"$TRAVIS_BRANCH\" == \"master\" && \"$TRAVIS_PULL_REQUEST\" = \"false\" ]]; then make coverage; fi"
services: services:
- docker - docker

View File

@ -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 # 0.90.21
- Fixed BASE_PATH when using a path for Statping - Fixed BASE_PATH when using a path for Statping
- Added Cypress testing - Added Cypress testing

View File

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

View File

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

View File

@ -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
}

View File

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

View File

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

View File

@ -11,7 +11,7 @@
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:test": "cypress run --record --key 49d99e5e-04c6-46df-beef-54b68e152a4d", "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", "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": { "dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3", "@fortawesome/fontawesome-free-solid": "^5.1.0-3",

View File

@ -14,6 +14,46 @@ HTML,BODY {
transition: height 0.3s ease; 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 { @-o-keyframes fadeIt {
0% { background-color: #f5f5f5; } 0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; } 50% { background-color: #f2f2f2; }
@ -548,6 +588,9 @@ HTML,BODY {
background-color: white; background-color: white;
transition: 0.2s all; transition: 0.2s all;
} }
.switch-rd-gr input:checked + label::before {
background-color: #cd141b;
}
.switch input:checked + label::before { .switch input:checked + label::before {
background-color: #08d; background-color: #08d;
} }

View File

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

View File

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

View File

@ -1,35 +1,28 @@
<template> <template>
<form @submit.prevent="saveService"> <form @submit.prevent="saveService">
<div class="card contain-card text-black-50 bg-white mb-4"> <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="card-body">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label> <label class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <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> <small class="form-text text-muted">Give your service a name you can recognize</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8"> <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="http">HTTP Service</option>
<option value="grpc">gRPC Service</option>
<option value="tcp">TCP Service</option> <option value="tcp">TCP Service</option>
<option value="udp">UDP Service</option> <option value="udp">UDP Service</option>
<option value="icmp">ICMP Ping</option> <option value="icmp">ICMP Ping</option>
<option value="grpc">gRPC Service</option>
</select> </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> <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> </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"> <div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Group</label> <label for="service_type" class="col-sm-4 col-form-label">Group</label>
<div class="col-sm-8"> <div class="col-sm-8">
@ -40,26 +33,79 @@
<small class="form-text text-muted">Attach this service to a group</small> <small class="form-text text-muted">Attach this service to a group</small>
</div> </div>
</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> </div>
<div v-if="service.type !== 'icmp'" class="card contain-card text-black-50 bg-white mb-4"> <div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Request Details</div> <div class="card-header">{{service.type.toUpperCase()}} Request Details</div>
<div class="card-body"> <div class="card-body">
<div v-if="service.type.match(/^(http)$/)" class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Service Check Type</label> <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"> <div class="col-sm-8">
<select v-model="service.method" name="method" class="form-control"> <span class="slider-info">{{secondsHumanize(service.timeout)}}</span>
<option value="GET" >GET</option> <input v-model="service.timeout" type="range" name="timeout" class="slider" min="1" max="180">
<option value="POST" >POST</option> <small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
<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> </div>
<div v-if="service.type.match(/^(http)$/) && service.method.match(/^(POST|PATCH|DELETE|PUT)$/)" class="form-group row"> <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> <label class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8"> <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> <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> </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 v-if="service.type.match(/^(http)$/)" class="form-group row">
<div class="col-sm-8"> <label class="col-sm-4 col-form-label">Verify SSL</label>
<input v-model="service.port" type="number" name="port" class="form-control" id="service_port" placeholder="8080"> <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> </div>
</div> </div>
<div class="card contain-card text-black-50 bg-white mb-4"> <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="card-body">
<div class="form-group row"> <div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-sm-8"> <div class="col-8 mt-1">
<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">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left"> <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"> <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> <label for="switch-notifications">Allow notifications to be sent for this service</label>
</span> </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> </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="form-group row">
<div class="col-12"> <div class="col-12">
<button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block"> <button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
@ -172,8 +191,7 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div> <div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form> </form>
</template> </template>
@ -227,6 +245,30 @@
} }
}, },
methods: { 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 () { async saveService () {
let s = this.service let s = this.service
this.loading = true this.loading = true

View File

@ -3,6 +3,7 @@ const { zonedTimeToUtc, utcToZonedTime, lastDayOfMonth, subSeconds, parse, getUn
import formatDistanceToNow from 'date-fns/formatDistanceToNow' import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format' import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO' import parseISO from 'date-fns/parseISO'
import addSeconds from 'date-fns/addSeconds'
export default Vue.mixin({ export default Vue.mixin({
methods: { methods: {
@ -15,6 +16,17 @@ export default Vue.mixin({
current() { current() {
return parseISO(new Date()) 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) { utc(val) {
return new Date.UTC(val) return new Date.UTC(val)
}, },

View File

@ -24,7 +24,26 @@
</a> </a>
</div> </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> </div>
<div class="col-md-9 col-sm-12"> <div class="col-md-9 col-sm-12">

View File

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

View File

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

View File

@ -112,7 +112,11 @@ func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
} }
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request, statusCode ...int) { 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{ output := apiResponse{
Status: "error", Status: "error",
Error: err.Error(), Error: err.Error(),

View File

@ -2,18 +2,24 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/statping/statping/types/notifications" "github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/types/services" "github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"net/http" "net/http"
"sort"
) )
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) { func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
var notifs []notifications.Notification
notifiers := services.AllNotifiers() 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) { func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
@ -24,8 +30,7 @@ func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }
notif = notif.UpdateFields(notifer) returnJson(notifer, w, r)
returnJson(notif, w, r)
} }
func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) { func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
@ -36,14 +41,12 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
var notif *notifications.Notification
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notif) err = decoder.Decode(&notifer)
if err != nil { if err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }
notifer = notifer.UpdateFields(notif)
err = notifer.Update() err = notifer.Update()
if err != nil { if err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
@ -54,63 +57,31 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
} }
func testNotificationHandler(w http.ResponseWriter, r *http.Request) { func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
var err error
form := parseForm(r)
vars := mux.Vars(r) vars := mux.Vars(r)
method := vars["method"] notifer, err := notifications.Find(vars["notifier"])
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")))
notifier, err := notifications.Find(method)
if err != nil { if err != nil {
log.Errorln(fmt.Sprintf("issue saving notifier %v: %v", method, err))
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }
n := notifier decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notifer)
if err != nil {
sendErrorJson(err, w, r)
return
}
if host != "" { notif := services.ReturnNotifier(notifer.Method)
n.Host = host err = notif.OnTest()
}
if port != 0 {
n.Port = port
}
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")
//err = notifications.OnTest(notifier) resp := &notifierTestResp{
//if err == nil { Success: err == nil,
// w.Write([]byte("ok")) Error: err,
//} else { }
// w.Write([]byte(err.Error())) returnJson(resp, w, r)
//} }
type notifierTestResp struct {
Success bool `json:"success"`
Error error `json:"error,omitempty"`
} }

View File

@ -136,7 +136,7 @@ func Router() *mux.Router {
api.Handle("/api/notifiers", authenticated(apiNotifiersHandler, false)).Methods("GET") 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(apiNotifierGetHandler, false)).Methods("GET")
api.Handle("/api/notifier/{notifier}", authenticated(apiNotifierUpdateHandler, false)).Methods("POST") 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 MESSAGES Routes
api.Handle("/api/messages", scoped(apiAllMessagesHandler)).Methods("GET") api.Handle("/api/messages", scoped(apiAllMessagesHandler)).Methods("GET")

View File

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

View File

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

View File

@ -31,7 +31,7 @@ func (s *slack) Select() *notifications.Notification {
var slacker = &slack{&notifications.Notification{ var slacker = &slack{&notifications.Notification{
Method: slackMethod, Method: slackMethod,
Title: "slack", 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", Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong", AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(10 * time.Second), Delay: time.Duration(10 * time.Second),
@ -40,9 +40,9 @@ var slacker = &slack{&notifications.Notification{
Limits: 60, Limits: 60,
Form: []notifications.NotificationForm{{ Form: []notifications.NotificationForm{{
Type: "text", Type: "text",
Title: "Incoming webhooker Url", Title: "Incoming Webhook Url",
Placeholder: "Insert your slack Webhook URL here.", Placeholder: "Insert your Slack Webhook URL here.",
SmallText: "Incoming webhooker URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">slack Apps</a>", SmallText: "Incoming Webhook URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">Slack Apps</a>",
DbField: "Host", DbField: "Host",
Required: true, Required: true,
}}}, }}},

View File

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

View File

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

View File

@ -13,18 +13,6 @@ func SetDB(database database.Database) {
db = database.Model(&Hit{}) 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 { func (h *Hit) Create() error {
q := db.Create(h) q := db.Create(h)
return q.Error() return q.Error()

View File

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

View File

@ -38,22 +38,6 @@ func (n *Notification) LastSent() time.Duration {
return since 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 { func (n *Notification) CanSend() bool {
if !n.Enabled.Bool { if !n.Enabled.Bool {
return false return false
@ -87,13 +71,11 @@ func (n *Notification) GetValue(dbField string) string {
case "host": case "host":
return n.Host return n.Host
case "port": case "port":
return fmt.Sprintf("%v", n.Port) return fmt.Sprintf("%d", n.Port)
case "username": case "username":
return n.Username return n.Username
case "password": case "password":
if n.Password != "" { return n.Password
return "##########"
}
case "var1": case "var1":
return n.Var1 return n.Var1
case "var2": case "var2":
@ -104,13 +86,9 @@ func (n *Notification) GetValue(dbField string) string {
return n.ApiSecret return n.ApiSecret
case "limits": case "limits":
return utils.ToString(int(n.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 // start will start the go routine for the notifier queue

View File

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

View File

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

View File

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

View File

@ -8,9 +8,10 @@ import (
"sort" "sort"
) )
var log = utils.Log var (
db database.Database
var db database.Database log = utils.Log.WithField("type", "service")
)
func SetDB(database database.Database) { func SetDB(database database.Database) {
db = database.Model(&Service{}) db = database.Model(&Service{})
@ -60,38 +61,23 @@ func (s *Service) AfterCreate() error {
func (s *Service) Update() error { func (s *Service) Update() error {
q := db.Update(s) q := db.Update(s)
allServices[s.Id] = 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.Close()
s.SleepDuration = s.Duration() s.SleepDuration = s.Duration()
go ServiceCheckQueue(allServices[s.Id], true) go ServiceCheckQueue(allServices[s.Id], true)
//notifier.OnUpdatedService(s.Service)
return q.Error() return q.Error()
} }
func (s *Service) Delete() error { func (s *Service) Delete() error {
s.Close() s.Close()
if err := s.DeleteFailures(); err != nil { if err := s.DeleteFailures(); err != nil {
return err return err
} }
if err := s.DeleteHits(); err != nil { if err := s.DeleteHits(); err != nil {
return err return err
} }
delete(allServices, s.Id) delete(allServices, s.Id)
q := db.Model(&Service{}).Delete(s) q := db.Model(&Service{}).Delete(s)
//notifier.OnDeletedService(s.Service)
return q.Error() return q.Error()
} }
@ -111,12 +97,3 @@ func (s *Service) DeleteCheckins() error {
} }
return nil return nil
} }
//func (s *Service) AfterDelete() error {
//
// return nil
//}
func (s *Service) AfterFind() error {
return nil
}

View File

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

View File

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

View File

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

View File

@ -21,9 +21,40 @@ var (
LastLines []*logRow LastLines []*logRow
LockLines sync.Mutex LockLines sync.Mutex
VerboseMode int 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 { type hook struct {
Entries []Logger.Entry Entries []Logger.Entry
@ -32,6 +63,9 @@ type hook struct {
func (t *hook) Fire(e *Logger.Entry) error { func (t *hook) Fire(e *Logger.Entry) error {
pushLastLine(e.Message) pushLastLine(e.Message)
if e.Level == Logger.ErrorLevel {
SentryLogEntry(e)
}
return nil return nil
} }
@ -120,8 +154,6 @@ func InitLogs() error {
}) })
checkVerboseMode() checkVerboseMode()
sentry.CaptureMessage("It works!")
LastLines = make([]*logRow, 0) LastLines = make([]*logRow, 0)
return nil return nil
} }
@ -154,6 +186,7 @@ func CloseLogs() {
ljLogger.Rotate() ljLogger.Rotate()
Log.Writer().Close() Log.Writer().Close()
ljLogger.Close() ljLogger.Close()
sentry.Flush(5 * time.Second)
} }
func pushLastLine(line interface{}) { func pushLastLine(line interface{}) {

View File

@ -44,6 +44,24 @@ func init() {
checkVerboseMode() 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{} { func Getenv(key string, defaultValue interface{}) interface{} {
if val, ok := os.LookupEnv(key); ok { if val, ok := os.LookupEnv(key); ok {
if val != "" { 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) { 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 err error
var req *http.Request var req *http.Request
t1 := time.Now() t1 := Now()
if req, err = http.NewRequest(method, url, body); err != nil { if req, err = http.NewRequest(method, url, body); err != nil {
httpMetric.Errors++ httpMetric.Errors++
return nil, nil, err return nil, nil, err
@ -275,7 +293,7 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
contents, err := ioutil.ReadAll(resp.Body) contents, err := ioutil.ReadAll(resp.Body)
// record HTTP metrics // record HTTP metrics
t2 := time.Now().Sub(t1).Milliseconds() t2 := Now().Sub(t1).Milliseconds()
httpMetric.Requests++ httpMetric.Requests++
httpMetric.Milliseconds += t2 / httpMetric.Requests httpMetric.Milliseconds += t2 / httpMetric.Requests
httpMetric.Bytes += int64(len(contents)) httpMetric.Bytes += int64(len(contents))

View File

@ -1 +1 @@
0.90.21 0.90.22