checkin updates, service view screen update, dont cache pages with "v" as a query param, checkins now in seconds

pull/755/head
hunterlong 4 years ago
parent 5c22841c20
commit fd62ac4c76

@ -155,6 +155,10 @@ class Api {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data))
}
async checkin(api) {
return axios.get('api/checkins/'+api).then(response => (response.data))
}
async checkin_create(data) {
return axios.post('api/checkins', data).then(response => (response.data))
}

@ -2,40 +2,81 @@
<div class="col-12">
<h2>{{service.name}} Checkins</h2>
<p class="mb-3">Tell your service to send a routine HTTP request to a Statping Checkin.</p>
<div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
<span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
<span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
<span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
<span class="d-block mt-2">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
<button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
<div v-for="(checkin, i) in checkins" class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
{{checkin.name}}
<button @click="deleteCheckin(checkin)" class="btn btn-sm btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/checkin/${checkin.api_key}`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small">Send a GET request to this URL every {{checkin.interval}} minutes</span>
<span class="small float-right mt-1">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3">Request expected every {{checkin.interval}} minutes</span>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{checkin.name}} Records
</div>
<div class="card-body" :class="{'d-none': !expanded}">
<div class="alert alert-primary small" :class="{'alert-success': hit.success, 'alert-danger': !hit.success}" v-for="(hit, i) in records(checkin)">
Checkin {{hit.success ? "Request" : "Failure"}} at {{hit.created_at}}
</div>
</div>
</div>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="curl_expanded = !curl_expanded" :icon="curl_expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
Cronjob Task
</div>
<div class="card-body" :class="{'d-none': !curl_expanded}">
This cronjob script will request the checkin endpoint every {{checkin.interval}} minutes. Add this cronjob task to the machine running this service.
<div class="input-group mt-2">
<input type="text" class="form-control" :value="`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${checkin.interval} * * * * /usr/bin/curl ${core.domain}/checkin/${checkin.api_key} >/dev/null 2>&1`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
<span class="small d-block">Using CURL</span>
</div>
</div>
</div>
<div class="card-footer">
<span :class="{'text-success': last_record(checkin).success, 'text-danger': !last_record(checkin).success}">
{{last_record(checkin).success ? "Checkin is currently working correctly" : "Checkin is currently failing"}}
</span>
</span>
</div>
</div>
<div class="col-12 alert alert-light">
<div class="card text-black-50 bg-white mt-4">
<div class="card-header text-capitalize">Create Checkin</div>
<div class="card-body">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
<div class="col-3">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-3">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
<button :disabled="btn_disabled" @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
@ -49,11 +90,12 @@ export default {
return {
service: {},
ready: false,
expanded: false,
curl_expanded: false,
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: 0
interval: 1,
service_id: 0,
}
}
},
@ -64,6 +106,12 @@ export default {
core() {
return this.$store.getters.core
},
btn_disabled() {
if (this.checkin.name === "" || this.checkin.interval <= 0) {
return true
}
return false
},
},
async created() {
if (this.$route.params) {
@ -74,22 +122,37 @@ export default {
}
},
methods: {
records(checkin) {
let hits = []
let failures = []
checkin.hits.forEach((hit) => {
hits.push({success: true, created_at: this.parseISO(hit.created_at), id: hit.id})
})
checkin.failures.forEach((failure) => {
failures.push({success: false, created_at: this.parseISO(failure.created_at), id: failure.id})
})
return hits.concat(failures).sort((a, b) => {return a.created_at-b.created_at}).reverse().slice(0,32)
},
last_record(checkin) {
const r = this.records(checkin)
return r[0]
},
fixInts() {
const c = this.checkin
this.checkin.interval = parseInt(c.interval)
this.checkin.grace = parseInt(c.grace)
return this.checkin
},
async saveCheckin() {
const c = this.fixInts()
await Api.checkin_create(c)
await this.updateCheckins()
this.checkin.name = ""
await this.load()
},
async deleteCheckin(checkin) {
await Api.checkin_delete(checkin)
await this.updateCheckins()
await this.load()
},
async updateCheckins() {
async load() {
const checkins = await Api.checkins()
this.$store.commit('setCheckins', checkins)
}

@ -1,7 +1,10 @@
<template>
<div>
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
<div class="card text-black-50 bg-white mt-3 mb-3">
<div class="card-header text-capitalize">Service Latency</div>
<div class="card-body">
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</div>
</div>
</template>

@ -21,8 +21,8 @@
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>

@ -85,7 +85,6 @@ export default Vue.mixin({
copy(txt) {
this.$copyText(txt).then(function (e) {
alert('Copied: \n'+txt)
console.log(e)
});
},
serviceLink(service) {

@ -18,41 +18,51 @@
<MessageBlock v-for="message in messagesInRange" v-bind:key="message.id" :message="message"/>
<div class="row mt-5 mb-4">
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-2">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
<option value="30m">30 Minutes</option>
<option value="1h">1 Hour</option>
<option value="3h">3 Hours</option>
<option value="6h">6 Hours</option>
<option value="12h">12 Hours</option>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="360h">15 Days</option>
</select>
<small class="d-block d-md-none d-block">Increment Timeframe</small>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">Timeframe</div>
<div class="card-body">
<div class="row">
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-4">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
<option value="30m">30 Minutes</option>
<option value="1h">1 Hour</option>
<option value="3h">3 Hours</option>
<option value="6h">6 Hours</option>
<option value="12h">12 Hours</option>
<option value="24h">1 Day</option>
<option value="168h">7 Days</option>
<option value="360h">15 Days</option>
</select>
<small class="d-block d-md-none d-block">Increment Timeframe</small>
</div>
</div>
</div>
</div>
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div v-if="!loading" class="col-12">
<div v-if="!loading" class="row">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
<div class="card text-black-50 bg-white mb-3">
<div class="card-header text-capitalize">Service Failures</div>
<div class="card-body">
<div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/>
</div>
</div>
</div>
</div>
@ -378,7 +388,7 @@ export default {
this.loading = false
},
async fetchUptime() {
const uptime = await Api.service_uptime(this.id, this.params.start, this.params.end)
const uptime = await Api.service_uptime(this.service.id, this.params.start, this.params.end)
window.console.log(uptime)
this.uptime_data = this.parse_uptime(uptime)
},

@ -2,6 +2,7 @@ package handlers
import (
"github.com/statping/statping/utils"
"net/url"
"sync"
"time"
)
@ -106,6 +107,13 @@ func (s Storage) Delete(key string) {
func (s Storage) Set(key string, content []byte, duration time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
u, err := url.Parse(key)
if err != nil {
return
}
if u.Query().Get("v") != "" {
return
}
s.items[key] = Item{
Content: content,
Expiration: utils.Now().Add(duration).UnixNano(),

@ -24,8 +24,7 @@ func findCheckin(r *http.Request) (*checkins.Checkin, string, error) {
}
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
chks := checkins.All()
returnJson(chks, w, r)
returnJson(checkins.All(), w, r)
}
func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
@ -39,8 +38,7 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
var checkin *checkins.Checkin
err := DecodeJSON(r, &checkin)
if err != nil {
if err := DecodeJSON(r, &checkin); err != nil {
sendErrorJson(err, w, r)
return
}
@ -63,22 +61,27 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
log.Infof("Checking %s was requested", checkin.Name)
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if last := checkin.LastHit(); last == nil {
checkin.Start()
}
hit := &checkins.CheckinHit{
Checkin: checkin.Id,
From: ip,
CreatedAt: utils.Now(),
}
log.Infof("Checking %s was requested", checkin.Name)
err = hit.Create()
if err != nil {
if err := hit.Create(); err != nil {
sendErrorJson(err, w, r)
return
}
checkin.Failing = false
checkin.LastHitTime = utils.Now()
sendJsonAction(hit.Id, "update", w, r)
}

@ -45,8 +45,7 @@ func TestApiCheckinRoutes(t *testing.T) {
Body: `{
"name": "Example Checkin",
"service_id": 1,
"checkin_interval": 300,
"grace_period": 60,
"interval": 300,
"api_key": "example"
}`,
},

@ -13,8 +13,7 @@ import (
var testCheckin = &Checkin{
ServiceId: 1,
Name: "Test Checkin",
Interval: 60,
GracePeriod: 10,
Interval: 3,
ApiKey: "tHiSiSaTeStXXX",
CreatedAt: utils.Now(),
UpdatedAt: utils.Now(),

@ -15,6 +15,11 @@ func SetDB(database database.Database) {
}
func (c *Checkin) AfterFind() {
c.AllHits = c.Hits()
c.AllFailures = c.Failures().LastAmount(64)
if last := c.LastHit(); last != nil {
c.LastHitTime = last.CreatedAt
}
metrics.Query("checkin", "find")
}
@ -41,9 +46,6 @@ func (c *Checkin) Create() error {
c.ApiKey = utils.RandomString(32)
}
q := db.Create(c)
c.Start()
go c.checkinRoutine()
return q.Error()
}

@ -2,13 +2,13 @@ package checkins
func (c *Checkin) LastHit() *CheckinHit {
var hit CheckinHit
dbHits.Where("checkin = ?", c.Id).Limit(1).Find(&hit)
dbHits.Where("checkin = ?", c.Id).Last(&hit)
return &hit
}
func (c *Checkin) Hits() []*CheckinHit {
var hits []*CheckinHit
dbHits.Where("checkin = ?", c.Id).Find(&hits)
dbHits.Where("checkin = ?", c.Id).Order("DESC").Find(&hits)
c.AllHits = hits
return hits
}

@ -7,6 +7,7 @@ import (
func (c *Checkin) CreateFailure(f *failures.Failure) error {
f.Checkin = c.Id
c.Failing = true
return failures.DB().Create(f).Error()
}

@ -10,22 +10,11 @@ func (c *Checkin) Expected() time.Duration {
last := c.LastHit()
now := utils.Now()
lastDir := now.Sub(last.CreatedAt)
sub := time.Duration(c.Period() - lastDir)
return sub
return c.Period() - lastDir
}
func (c *Checkin) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%ds", c.Interval))
if duration.Seconds() <= 15 {
return 15 * time.Second
}
return duration
}
// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response)
func (c *Checkin) Grace() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod))
return duration
return time.Duration(c.Interval) * time.Minute
}
// Start will create a channel for the checkin checking go routine

@ -25,11 +25,8 @@ func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) {
// checkinRoutine for checking if the last Checkin was within its interval
func (c *Checkin) checkinRoutine() {
lastHit := c.LastHit()
if lastHit == nil {
return
}
reCheck := c.Period()
CheckinLoop:
for {
select {
@ -38,20 +35,25 @@ CheckinLoop:
c.Failing = false
break CheckinLoop
case <-time.After(reCheck):
log.Infoln(fmt.Sprintf("Checkin '%s' expects a request every %v", c.Name, utils.FormatDuration(c.Period())))
if c.Expected() <= 0 {
issue := fmt.Sprintf("Checkin '%s' is failing, no request since %v", c.Name, lastHit.CreatedAt)
//log.Errorln(issue)
lastHit := c.LastHit()
ago := utils.Now().Sub(lastHit.CreatedAt)
log.Infoln(fmt.Sprintf("Checkin '%s' expects a request every %s last request was %s ago", c.Name, c.Period(), utils.DurationReadable(ago)))
if ago.Seconds() > c.Period().Seconds() {
issue := fmt.Sprintf("Checkin expects a request every %d seconds", c.Interval)
log.Warnln(issue)
fail := &failures.Failure{
Issue: issue,
Method: "checkin",
Service: c.ServiceId,
Checkin: c.Id,
PingTime: c.Expected().Milliseconds(),
PingTime: ago.Milliseconds(),
}
c.CreateFailure(fail)
if err := c.CreateFailure(fail); err != nil {
log.Errorln(err)
}
}
reCheck = c.Period()
}

@ -8,22 +8,20 @@ import (
func Samples() error {
log.Infoln("Inserting Sample Checkins...")
checkin1 := &Checkin{
Name: "Demo Checkin 1",
ServiceId: 1,
Interval: 300,
GracePeriod: 300,
ApiKey: "demoCheckin123",
Name: "Demo Checkin 1",
ServiceId: 1,
Interval: 3,
ApiKey: "demoCheckin123",
}
if err := checkin1.Create(); err != nil {
return err
}
checkin2 := &Checkin{
Name: "Example Checkin 2",
ServiceId: 2,
Interval: 900,
GracePeriod: 300,
ApiKey: utils.RandomString(7),
Name: "Example Checkin 2",
ServiceId: 2,
Interval: 1,
ApiKey: utils.RandomString(7),
}
if err := checkin2.Create(); err != nil {
return err

@ -11,7 +11,6 @@ type Checkin struct {
ServiceId int64 `gorm:"index;column:service" json:"service_id"`
Name string `gorm:"column:name" json:"name"`
Interval int64 `gorm:"column:check_interval" json:"interval"`
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

@ -38,7 +38,7 @@ func (f Failurer) List() []*Failure {
func (f Failurer) LastAmount(amount int) []*Failure {
var fail []*Failure
f.db.Order("id asc").Limit(amount).Find(&fail)
f.db.Order("id DESC").Limit(amount).Find(&fail)
return fail
}

@ -7,7 +7,9 @@ import (
// CheckinProcess runs the checkin routine for each checkin attached to service
func CheckinProcess(s *Service) {
for _, c := range s.Checkins() {
c.Start()
if last := c.LastHit(); last != nil {
c.Start()
}
}
}

@ -253,10 +253,6 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
return allServices, nil
}
for _, s := range all() {
if start {
CheckinProcess(s)
}
s.Failures = s.AllFailures().LastAmount(limitedFailures)
for _, c := range s.Checkins() {
s.AllCheckins = append(s.AllCheckins, c)
@ -264,6 +260,9 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
// collect initial service stats
s.UpdateStats()
allServices[s.Id] = s
if start {
CheckinProcess(s)
}
}
return allServices, nil
}

@ -61,11 +61,10 @@ var hit3 = &hits.Hit{
}
var exmapleCheckin = &checkins.Checkin{
ServiceId: 1,
Name: "Example Checkin",
Interval: 60,
GracePeriod: 30,
ApiKey: "wdededede",
ServiceId: 1,
Name: "Example Checkin",
Interval: 3,
ApiKey: "wdededede",
}
var fail1 = &failures.Failure{

Loading…
Cancel
Save