0.90.62 updates

pull/773/head
hunterlong 2020-07-30 16:14:41 -07:00
parent 84e68ea183
commit 1a438a6198
49 changed files with 831 additions and 387 deletions

View File

@ -1,3 +1,10 @@
# 0.90.62 (07-30-2020)
- Added Notification logs
- Fixed issues with notifications
- Updated Incident UI
- Added additional testing for notifications
-
# 0.90.61 (07-22-2020)
- Modified sass layouts, organized and split up sections
- Modified Checkins to seconds rather than milliseconds (for cronjob)

View File

@ -17,11 +17,18 @@ ARCHS = 386 arm amd64 arm64
all: build-deps compile install test build
test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=0.99.99" -coverprofile=coverage.out ./...
go test -v -p=1 -ldflags="-X main.VERSION=${VERSION}" -coverprofile=coverage.out ./...
build: clean
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o statping --tags "netgo linux" ./cmd
go-build: clean
rm -rf source/dist
rm -rf source/rice-box.go
wget https://assets.statping.com/source.tar.gz
tar -xvf source.tar.gz
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o statping --tags "netgo" ./cmd
up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
make print_details
@ -291,11 +298,14 @@ post-release: frontend-build upload_to_s3 publish-homebrew dockerhub
publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests
upload_to_s3: travis_s3_creds
aws s3 cp ./source/dist/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/dist/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
aws s3 cp ./source/dist/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT)
upload_to_s3:
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/
rm -rf source.tar.gz
aws s3 cp source/dist/css/ s3://assets.statping.com/css/ --recursive --exclude "*" --include "*.css"
aws s3 cp source/dist/js/ s3://assets.statping.com/js/ --recursive --exclude "*" --include "*.js"
aws s3 cp source/dist/scss/ s3://assets.statping.com/scss/ --recursive --exclude "*" --include "*.scss"
aws s3 cp install.sh s3://assets.statping.com/
travis_s3_creds:
mkdir -p ~/.aws
@ -376,6 +386,10 @@ multiarch:
mkdir /tmp/.buildx-cache || true
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
delve:
go build -gcflags "all=-N -l" -o statping ./cmd
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./statping
check:
@echo "Checking the programs required for the build are installed..."
@echo "go: $(shell go version) - $(shell which go)" && go version >/dev/null 2>&1 || (echo "ERROR: go 1.14 is required."; exit 1)
@ -383,5 +397,5 @@ check:
@echo "yarn: $(shell yarn --version) - $(shell which yarn)" && yarn --version >/dev/null 2>&1 || (echo "ERROR: yarn is required."; exit 1)
@echo "All required programs are installed!"
.PHONY: all check build certs multiarch build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.PHONY: all check build certs multiarch go-build build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds

View File

@ -39,7 +39,7 @@ type GroupQuery struct {
}
func (b GroupQuery) Find(data interface{}) error {
return b.db.Find(data).Error()
return b.db.Order("id DESC").Find(data).Error()
}
func (b GroupQuery) Database() Database {

View File

@ -1,7 +1,7 @@
<template>
<div id="app">
<router-view :loaded="loaded"/>
<Footer v-if="$route.path !== '/setup'"/>
<Footer v-if="$route.path !== '/setup'"/>
</div>
</template>
@ -55,5 +55,8 @@
<style lang="scss">
@import "./assets/css/bootstrap.min.css";
@import "./assets/scss/main";
@import "./assets/scss/base";
@import "./assets/scss/layout";
@import "./assets/scss/forms";
@import "./assets/scss/mobile";
</style>

View File

@ -154,6 +154,10 @@
padding: 5px 7px;
}
.service_li {
min-height: 115px !important;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;

View File

@ -31,7 +31,7 @@
.form-control[readonly] {
background-color: lighten($background-color, 12%) !important;
color: darken($background-color, 5%) !important;
color: lighten($input-color, 40%) !important;
}
/* The slider itself */
@ -268,6 +268,10 @@ input.inputTags-field:focus {
color: $text-color;
}
.nav-link.active A:HOVER {
color: white !important;
}
.nav-pills I {
margin-right: 10px;
}

View File

@ -1,4 +0,0 @@
@import 'base';
@import 'layout';
@import 'forms';
@import 'mobile';

View File

@ -74,10 +74,19 @@
position: absolute;
}
.btn-sm {
line-height: 1.4rem;
font-size: 0.83rem;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;
}
.badge {
padding-top: 2px;
padding-bottom: 3px;
}
.switch LABEL {
font-size: 10pt;
}
.full-col-12 {
padding-left: 0px;

View File

@ -1,7 +1,7 @@
/* Index Page */
$background-color: #f5f5f5;
$container-color: #ffffff;
$text-color: #1d1d1d;
$text-color: #2a2a2a;
$max-width: 860px;
$title-color: #4e4e4e;
$description-color: #828282;

View File

@ -6,7 +6,7 @@ CodeMirror.defineMode('mymode', () => {
if (stream.match(".Service") || (stream.match(".Core")) || (stream.match(".Failure"))) {
return "var-highlight"
} else if (stream.match(".Id") || stream.match(".Domain") || stream.match(".CreatedAt") ||
stream.match(".Name") || stream.match(".DowntimeAgo") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Name") || stream.match(".Downtime.Human") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Port") || stream.match(".FailuresLast24Hours") || stream.match(".PingTime")) {
return "var-sub-highlight"
} else if (stream.match("{{") || stream.match("}}")) {

View File

@ -6,7 +6,7 @@
<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>
<button @click="deleteCheckin(checkin)" class="btn btn-sm small btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
@ -18,8 +18,8 @@
</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>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Request expected every {{checkin.interval}} minutes</span>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
@ -62,15 +62,15 @@
<div class="card-body">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-7 col-md-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-3">
<div class="col-5 col-md-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">
<div class="col-12 col-md-4">
<label class="col-form-label"></label>
<button :disabled="btn_disabled" @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>

View File

@ -25,7 +25,7 @@
<thead>
<tr>
<th scope="col">{{ $t('dashboard.name') }}</th>
<th scope="col">{{ $tc('dashboard.service', 2) }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $tc('dashboard.service', 2) }}</th>
<th scope="col">{{ $t('dashboard.visibility') }}</th>
<th scope="col"></th>
</tr>
@ -36,7 +36,7 @@
<td><span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}}
</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td class="d-none d-md-table-cell">{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
<span class="badge text-uppercase" :class="{'badge-primary': group.public, 'badge-secondary': !group.public}">
{{group.public ? $t('public') : $t('private')}}

View File

@ -1,22 +1,77 @@
<template>
<div class="col-12">
<h2>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-outline-danger float-right">Delete All</button></h2>
<div class="list-group mt-3 mb-4">
<div v-if="service" class="col-12">
<h3>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-danger float-right">Delete All</button>
</h3>
<div class="alert alert-info" v-if="failures.length===0">
You don't have any failures for {{service.name}}. Way to go!
</div>
<div v-for="(failure, index) in failures" :key="index" class="mb-2 list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{failure.issue}}</h5>
<small>{{niceDate(failure.created_at)}}</small>
<div class="card mt-4 mb-4">
<div class="card-header">
Search and Filter
<span class="float-right">
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</span>
</div>
<div class="card-body">
<form>
<div class="form-row">
<div class="col">
<label for="fromdate">From Date</label>
<flatPickr id="fromdate" :disabled="loading" @on-change="load" v-model="start_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" required />
</div>
<div class="col">
<label for="todate">To Date</label>
<flatPickr id="todate" :disabled="loading" @on-change="load" v-model="end_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" required />
</div>
<div class="col">
<label for="search">Search Terms</label>
<input id="search" type="text" v-model="search" class="form-control">
</div>
</div>
<div class="form-row mt-3">
<div class="col">
<span @click="show_checkins = !!show_checkins" class="switch float-left">
<input v-model="show_checkins" type="checkbox" class="switch" id="showcheckins" v-bind:checked="show_checkins">
<label v-if="show_checkins" for="showcheckins">Showing Checkin Failures</label>
<label v-else for="showcheckins">View Checkin Failures</label>
</span>
</div>
</div>
</form>
</div>
<p class="mb-1">{{failure.issue}}</p>
</div>
<nav v-if="total > 4" class="mt-3">
<div v-if="failures.length === 0" class="alert alert-info">
<span v-if="search">
Could not find any failures with issue: "{{search}}"
</span>
<span v-else>
You don't have any failures for {{service.name}}. Way to go!
</span>
</div>
<table v-else class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Issue</th>
<th scope="col">Status Code</th>
<th scope="col">Ping</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
<tr v-for="(failure, index) in failures" :key="index">
<th class="font-1" scope="row">{{failure.id}}</th>
<td class="font-1">{{failure.issue}}</td>
<td class="font-1">{{failure.error_code}}</td>
<td class="font-1">{{humanTime(failure.ping)}}</td>
<td class="font-1">{{ago(failure.created_at)}}</td>
</tr>
</tbody>
</table>
<nav v-if="total > 4 && failures.length !== 0" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{'disabled': page===1}">
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
@ -35,42 +90,59 @@
</li>
</ul>
<div class="text-center">
<span>{{total}} Failures</span>
<span>{{total}} Failures</span>
</div>
</nav>
</div>
</div>
</template>
<script>
import Api from "../../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
export default {
name: 'Failures',
components: {
flatPickr
},
data() {
return {
service: {},
loading: true,
search: "",
show_checkins: false,
service: null,
fails: [],
limit: 10,
limit: 64,
offset: 0,
total: 0,
page: 1
page: 1,
start_time: this.nowSubtract(216000).toISOString(),
end_time: this.nowSubtract(0).toISOString(),
}
},
watch: {
'$route': 'reloadTimes',
},
computed: {
failures() {
return this.fails.sort(function(a,b) {return b.id - a.id;});
let sorted = this.fails
if (this.show_checkins) {
sorted = sorted.filter(f => f.method === "checkin");
} else {
sorted = sorted.filter(f => f.method !== "checkin");
}
if (this.search !== "") {
sorted = sorted.filter(f => f.issue.toLowerCase().includes(this.search));
}
return sorted
},
pages() {
return Math.floor(this.total / this.limit)
},
maxPages() {
const p = Math.floor(this.total / this.limit)
if (p > 16) {
return 16
} else {
return p
}
return Math.floor(this.total / this.limit)
}
},
async created() {
@ -94,7 +166,9 @@ export default {
await this.load()
},
async load() {
this.fails = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
this.loading = true
this.fails = await Api.service_failures(this.service.id, this.toUnix(this.parseISO(this.start_time)), this.toUnix(this.parseISO(this.end_time)), this.limit, this.offset)
this.loading = false
}
}
}

View File

@ -4,7 +4,7 @@
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
<font-awesome-icon icon="times" />
</button>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div class="card mb-4" :class="{'offline-card': !service.online}">
<div class="card-title px-4 pt-3">
<div class="card-header pb-1">
<h4 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
@ -9,8 +9,7 @@
</h4>
</div>
<div class="card-body p-3 p-md-1 pt-md-1 pb-md-1">
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<transition name="fade">
<div v-if="loaded" class="col-12 pb-2">

View File

@ -33,13 +33,13 @@
</td>
<td class="text-right">
<div class="btn-group">
<button v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="edit" />
</button>
<button @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<button :disabled="loading" @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="chart-area" />
</button>
<button v-if="$store.state.admin" @click.prevent="deleteService(service)" href="#" class="btn btn-sm btn-danger">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="deleteService(service)" class="btn btn-sm btn-danger">
<font-awesome-icon v-if="!loading" icon="times" />
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</button>

View File

@ -0,0 +1,39 @@
<template>
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='investigating', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
<span class="text-muted">- {{update.message}}
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
<span class="d-block small">{{ago(update.created_at)}} ago</span>
</div>
</template>
<script>
import Api from "@/API";
export default {
name: "IncidentUpdate",
props: {
update: {
required: true
},
admin: {
required: true
}
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
await this.loadUpdates()
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -3,7 +3,7 @@
<h4 v-if="group.name !== 'Empty Group'" class="group_header mb-3 mt-4">{{group.name}}</h4>
<div class="list-group online_list mb-4">
<a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<div v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">
{{service.online ? $t('online') : $t('offline')}}
@ -12,7 +12,8 @@
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
<div>
<div class="d-flex mt-3 mb-2">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount != 0" class="small">{{d.amount}}</span>
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small">{{d.amount}}</span>
</div>
</div>
<div class="row mt-2">

View File

@ -1,25 +1,24 @@
<template>
<div class="row">
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
<div v-for="(incident, i) in incidents" class="col-12">
<span class="braker mt-1 mb-3"></span>
<h6>Incident: {{incident.title}}
<h6>{{incident.title}}
<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>
</h6>
<span class="font-2" v-html="incident.description"></span>
<UpdatesBlock :incident="incident"/>
<div class="font-2 mb-3" v-html="incident.description"></div>
<IncidentUpdate v-for="(update, i) in incident.updates" v-bind:key="i" :update="update" :admin="false"/>
</div>
</div>
</template>
<script>
import Api from '../../API';
import UpdatesBlock from "@/components/Index/UpdatesBlock";
import IncidentUpdate from "@/components/Elements/IncidentUpdate";
export default {
name: 'IncidentsBlock',
components: {UpdatesBlock},
components: {IncidentUpdate},
props: {
service: {
type: Object,
@ -49,8 +48,7 @@ export default {
this.incidents = await Api.incidents_service(this.service.id)
},
async incident_updates(incident) {
await Api.incident_updates(incident).then((d) => {return d})
return o
return await Api.incident_updates(incident)
}
}
}

View File

@ -1,52 +0,0 @@
<template>
<div class="row">
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
<div class="col-md-2 col-12">
<span class="badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
</div>
<div class="col-md-12 col-12 mt-2 font-3">{{update.message}}</div>
<div class="col-12 font-1 float-right mt-2">{{ago(update.created_at)}} ago</div>
</div>
</div>
</template>
<script>
import Api from '../../API';
export default {
name: 'UpdatesBlock',
props: {
incident: {
type: Object,
required: true
}
},
data() {
return {
updates: null,
}
},
mounted () {
this.getIncidentUpdates()
},
methods: {
badgeClass(val) {
switch (val.toLowerCase()) {
case "resolved":
return "badge-success"
case "update":
return "badge-info"
case "investigating":
return "badge-danger"
}
},
async getIncidentUpdates() {
this.updates = await Api.incident_updates(this.incident)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,11 +1,6 @@
<template>
<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 class="service-chart-container">
<apexchart width="100%" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</template>

View File

@ -16,19 +16,19 @@
<div class="col-12 alert alert-light">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-12 col-md-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">
<div class="col-12 col-md-5">
<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">
<div class="col-12 col-md-5">
<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>
<div class="col-3">
<div class="col-12 col-md-5">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block mt-2">Save Checkin</button>
</div>

View File

@ -6,19 +6,11 @@
</div>
<div v-for="update in updates" :key="update.id">
<div class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
</div>
<IncidentUpdate :update="update" :admin="true"/>
</div>
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-3">
<div class="col-12 col-md-3 mb-3 mb-md-0">
<select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option>
<option value="Update">Update</option>
@ -26,11 +18,11 @@
<option value="Resolved">Resolved</option>
</select>
</div>
<div class="col-7">
<div class="col-12 col-md-7 mb-3 mb-md-0">
<input v-model="incident_update.message" rows="5" name="description" class="form-control" id="message" required>
</div>
<div class="col-2">
<div class="col-12 col-md-2">
<button @click.prevent="createIncidentUpdate"
:disabled="!incident_update.message"
type="submit" class="btn btn-block btn-primary">
@ -46,10 +38,11 @@
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import IncidentUpdate from "@/components/Elements/IncidentUpdate";
export default {
name: 'FormIncidentUpdates',
components: {},
components: {IncidentUpdate},
props: {
incident: {
type: Object,
@ -72,15 +65,6 @@
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.updates = this.updates.filter(obj => obj.id !== update.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
//await this.loadUpdates()
}
},
async createIncidentUpdate() {
this.res = await Api.incident_update_create(this.incident_update)
if (this.res.status === "success") {

View File

@ -39,7 +39,7 @@
<div class="col-sm-4">
<flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
</div>
<div class="col-sm-4">
<div class="col-sm-4 mt-3 mt-md-0">
<flatPickr v-model="message.end_on" @on-change="endChange" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required />
</div>
</div>

View File

@ -69,7 +69,7 @@
</div>
</div>
<div v-if="notifier.data_type" class="card mb-3">
<div v-if="notifier.data_type" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Outgoing Request
@ -136,6 +136,31 @@
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Failure"}}</button>
</div>
</div>
</div>
</div>
<div v-if="notifier.logs" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded_logs = !expanded_logs" :icon="expanded_logs ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Logs
<span class="badge badge-info float-right text-uppercase mt-1">{{notifier.logs.length}}</span>
</div>
<div class="card-body" :class="{'d-none': !expanded_logs}">
<div v-for="(log, i) in notifier.logs.reverse()" class="alert" :class="{'alert-danger': log.error, 'alert-dark': !log.success && !log.error, 'alert-success': log.success && !log.error}">
<span class="d-block">
Service '{{$store.getters.serviceById(log.service).name}}'
{{log.success ? "Success Triggered" : "Failure Triggered"}}
</span>
<div class="bg-white p-3 small mt-2">
<code>{{log.message}}</code>
</div>
<div class="row mt-2">
<span class="col-6 small">{{niceDate(log.created_at)}}</span>
</div>
</div>
</div>
</div>
@ -183,6 +208,7 @@ export default {
success: false,
saved: false,
expanded: false,
expanded_logs: false,
success_data: null,
failure_data: null,
form: {},

View File

@ -44,7 +44,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Public Service</label>
<div class="col-8 mt-1">
<div class="col-12 col-md-8 mt-1 mb-2">
<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>
@ -145,8 +145,8 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.redirect = !!service.redirect" class="switch float-left">
<input v-model="service.redirect" type="checkbox" name="redirect-option" class="switch" id="switch-redirect" v-bind:checked="service.redirect">
<label for="switch-redirect">Follow HTTP Redirects if server attempts</label>
@ -155,8 +155,8 @@
</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">
<label class="col-12 col-md-4 col-form-label">Verify SSL</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<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>
@ -166,8 +166,8 @@
</div>
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Use TLS Certificate</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">Use TLS Certificate</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="use_tls = !!use_tls" class="switch float-left">
<input v-model="use_tls" type="checkbox" name="verify_ssl-option" class="switch" id="switch-use-tls" v-bind:checked="use_tls">
<label for="switch-use-tls" v-if="use_tls">Custom TLS Certificates for mTLS services</label>
@ -209,7 +209,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-8 mt-1">
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
<label for="switch-notifications">Allow notifications to be sent for this service</label>
@ -226,7 +226,7 @@
</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">
<div class="col-12 col-md-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>

View File

@ -7,7 +7,7 @@
<div class="col-12">
<form @submit.prevent="saveSetup">
<div class="row">
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.language') }}</label>
<select @change="changeLanguages" v-model="setup.language" id="language" class="form-control">
@ -27,13 +27,13 @@
</select>
</div>
<div class="row">
<div class="col-6">
<div class="col-7 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('setup.host') }}</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
</div>
<div class="col-6">
<div class="col-5 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('port') }}</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="number" class="form-control" placeholder="5432">
@ -65,12 +65,11 @@
</span>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.project_name') }}</label>
@ -125,9 +124,11 @@
{{error}}
</div>
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? "Loading..." : "Save Settings"}}
</button>
<div class="col-12">
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? "Loading..." : "Save Settings"}}
</button>
</div>
</div>
</form>

View File

@ -5,12 +5,12 @@
<div class="col-12 full-col-12">
<div v-for="service in services_no_group" v-bind:key="service.id" class="list-group online_list mb-4">
<a class="service_li list-group-item list-group-item-action">
<div class="service_li list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
</div>
</div>

View File

@ -7,12 +7,12 @@
{{service.online ? $t('online') : $t('offline')}}
</span>
<h4 class="mt-2">
<span class="mt-2 font-3">
<router-link to="/" class="text-black-50 text-decoration-none">{{core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
<span class="badge float-right d-none d-md-block text-uppercase" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</h4>
</span>
<ServiceTopStats :service="service"/>
@ -20,17 +20,17 @@
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">Timeframe</div>
<div class="card-body">
<div class="card-body pb-4">
<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 />
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="reload" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" 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 />
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0">
<flatPickr :disabled="loading" @on-change="reload" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="form-control text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-4 mb-1 mb-md-0">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
@ -50,10 +50,14 @@
</div>
</div>
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div v-if="!loading" class="row">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></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">
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div v-if="!loading" class="row">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
</div>
</div>
<div class="card text-black-50 bg-white mb-3">
@ -362,11 +366,12 @@ export default {
},
},
watch: {
'$route': 'reload',
service: function(n, o) {
this.onnn()
this.reload()
},
load_timedata: function(n, o) {
this.onnn()
this.reload()
}
},
async mounted() {
@ -381,7 +386,7 @@ export default {
this.end_time = end
this.loading = false
},
async onnn() {
async reload() {
this.loading = true
await this.chartHits()
await this.fetchUptime()

View File

@ -18,6 +18,10 @@ const NotFound = () => import('@/pages/NotFound')
import VueRouter from "vue-router";
import Api from "./API";
const Loading = {
template: '<div class="jumbotron">LOADING</div>'
}
const routes = [
{
path: '/setup',
@ -56,6 +60,7 @@ const routes = [
},{
path: 'users',
component: DashboardUsers,
loading: Loading,
meta: {
requiresAuth: true
}

View File

@ -61,13 +61,13 @@ export default new Vuex.Store({
},
serviceByAll: (state) => (element) => {
if (element % 1 === 0) {
return state.services.find(s => s.id == element)
return state.services.find(s => s.id === element)
} else {
return state.services.find(s => s.permalink === element)
}
},
serviceById: (state) => (id) => {
return state.services.find(s => s.id == id)
return state.services.find(s => s.id === id)
},
serviceByPermalink: (state) => (permalink) => {
return state.services.find(s => s.permalink === permalink)

View File

@ -2,6 +2,7 @@ package handlers
import (
"github.com/gorilla/mux"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
@ -25,12 +26,11 @@ func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
notif := services.FindNotifier(vars["notifier"])
notifer, err := notifications.Find(notif.Method)
if err != nil {
sendErrorJson(err, w, r)
if notif == nil {
sendErrorJson(errors.New("could not find notifier"), w, r)
return
}
returnJson(notifer, w, r)
returnJson(notif, w, r)
}
func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -82,7 +82,7 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
"method": "slack",
"host": "` + slackWebhookUrl + `",
"success_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"The service {{.Service.Name}} is back online.\"\n }\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Service\",\n \"emoji\": true\n },\n \"style\": \"primary\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}",
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.DowntimeAgo}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.Downtime.Human}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
}
}`,
ExpectedStatus: 200,
@ -101,7 +101,7 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
"method": "slack",
"host": "` + slackWebhookUrl + `",
"success_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \"The service {{.Service.Name}} is back online.\"\n }\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Service\",\n \"emoji\": true\n },\n \"style\": \"primary\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}",
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.DowntimeAgo}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
"failure_data": "{\n \"blocks\": [{\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": \":warning: The service {{.Service.Name}} is currently offline! :warning:\"\n }\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"section\",\n \"fields\": [{\n \"type\": \"mrkdwn\",\n \"text\": \"*Service:*\\n{{.Service.Name}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*URL:*\\n{{.Service.Domain}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Status Code:*\\n{{.Service.LastStatusCode}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*When:*\\n{{.Failure.CreatedAt}}\"\n }, {\n \"type\": \"mrkdwn\",\n \"text\": \"*Downtime:*\\n{{.Service.Downtime.Human}}\"\n }, {\n \"type\": \"plain_text\",\n \"text\": \"*Error:*\\n{{.Failure.Issue}}\"\n }]\n }, {\n \"type\": \"divider\"\n }, {\n \"type\": \"actions\",\n \"elements\": [{\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"View Offline Service\",\n \"emoji\": true\n },\n \"style\": \"danger\",\n \"url\": \"{{.Core.Domain}}/service/{{.Service.Id}}\"\n }, {\n \"type\": \"button\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"Go to Statping\",\n \"emoji\": true\n },\n \"url\": \"{{.Core.Domain}}\"\n }]\n }]\n}"
}
}`,
ExpectedStatus: 200,

View File

@ -34,17 +34,16 @@ var slacker = &slack{&notifications.Notification{
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(10 * time.Second),
Host: null.NewNullString("https://webhooksurl.slack.com/***"),
Icon: "fab fa-slack",
SuccessData: null.NewNullString(`{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "The service {{.Service.Name}} is back online." } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Service", "emoji": true }, "style": "primary", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }`),
FailureData: null.NewNullString(`{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": ":warning: The service {{.Service.Name}} is currently offline! :warning:" } }, { "type": "divider" }, { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*Service:*\n{{.Service.Name}}" }, { "type": "mrkdwn", "text": "*URL:*\n{{.Service.Domain}}" }, { "type": "mrkdwn", "text": "*Status Code:*\n{{.Service.LastStatusCode}}" }, { "type": "mrkdwn", "text": "*When:*\n{{.Failure.CreatedAt}}" }, { "type": "mrkdwn", "text": "*Downtime:*\n{{.Service.DowntimeAgo}}" }, { "type": "plain_text", "text": "*Error:*\n{{.Failure.Issue}}" } ] }, { "type": "divider" }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Offline Service", "emoji": true }, "style": "danger", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }`),
FailureData: null.NewNullString(`{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": ":warning: The service {{.Service.Name}} is currently offline! :warning:" } }, { "type": "divider" }, { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*Service:*\n{{.Service.Name}}" }, { "type": "mrkdwn", "text": "*URL:*\n{{.Service.Domain}}" }, { "type": "mrkdwn", "text": "*Status Code:*\n{{.Service.LastStatusCode}}" }, { "type": "mrkdwn", "text": "*When:*\n{{.Failure.CreatedAt}}" }, { "type": "mrkdwn", "text": "*Downtime:*\n{{.Service.Downtime.Human}}" }, { "type": "plain_text", "text": "*Error:*\n{{.Failure.Issue}}" } ] }, { "type": "divider" }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Offline Service", "emoji": true }, "style": "danger", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }`),
DataType: "json",
RequestInfo: "Slack allows you to customize your own messages with many complex components. Checkout the <a target=\"_blank\" href=\"https://api.slack.com/reference/surfaces/formatting\">Slack Message API</a> to learn how you can create your own.",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
Title: "Incoming Webhook Url",
Placeholder: "Insert your Slack Webhook URL here.",
Placeholder: "https://hooks.slack.com/services/ETJ1B87WE/H76D6G8S30/H4d97R4EcZ40SpfyqPlAHr",
SmallText: "Incoming Webhook URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">Slack Apps</a>",
DbField: "Host",
Required: true,

View File

@ -16,7 +16,7 @@ import (
var (
log = utils.Log.WithField("type", "source")
TmplBox *rice.Box // HTML and other small files from the 'source/tmpl' directory, this will be loaded into '/assets'
DefaultScss = []string{"scss/base.scss", "scss/layout.scss", "scss/main.scss", "scss/mixin.scss", "scss/mobile.scss", "scss/variables.scss"}
DefaultScss = []string{"scss/base.scss", "scss/layout.scss", "scss/forms.scss", "scss/mixin.scss", "scss/mobile.scss", "scss/variables.scss"}
)
// Assets will load the Rice boxes containing the CSS, SCSS, JS, and HTML files.
@ -34,7 +34,7 @@ func scssRendered(name string) string {
path := spl[:len(spl)-2]
file := spl[len(spl)-1]
splFile := strings.Split(file, ".")
return fmt.Sprintf("%s/css/%s.css", strings.Join(path, "/"), splFile[len(splFile)-2])
return filepath.Join(strings.Join(path, "/"), "css", splFile[len(splFile)-2]+".css")
}
// CompileSASS will attempt to compile the SASS files into CSS
@ -50,12 +50,10 @@ func CompileSASS(files ...string) error {
}
for _, file := range files {
scssFile := fmt.Sprintf("%v/assets/%v", utils.Params.GetString("STATPING_DIR"), file)
scssFile := filepath.Join(utils.Params.GetString("STATPING_DIR"), "assets", file)
log.Infoln(fmt.Sprintf("Compiling SASS %v into %v", scssFile, scssRendered(scssFile)))
stdout, stderr, err := utils.Command(sassBin, scssFile, scssRendered(scssFile))
if err != nil {
log.Errorln(fmt.Sprintf("Failed to compile assets with SASS %v", err))
log.Errorln(fmt.Sprintf("%s %s %s", sassBin, scssFile, scssRendered(scssFile)))
@ -75,9 +73,7 @@ func UsingAssets(folder string) bool {
if _, err := os.Stat(folder + "/assets"); err == nil {
return true
} else {
useAssets := utils.Params.GetBool("USE_ASSETS")
if useAssets {
if utils.Params.GetBool("USE_ASSETS") {
log.Infoln("Environment variable USE_ASSETS was found.")
if err := CreateAllAssets(folder); err != nil {
log.Warnln(err)
@ -96,7 +92,7 @@ func UsingAssets(folder string) bool {
// SaveAsset will save an asset to the '/assets/' folder.
func SaveAsset(data []byte, path string) error {
path = fmt.Sprintf("%s/assets/%s", utils.Directory, path)
path = filepath.Join(utils.Directory, "assets", path)
err := utils.SaveFile(path, data)
if err != nil {
log.Errorln(fmt.Sprintf("Failed to save %v, %v", path, err))
@ -143,9 +139,15 @@ func CreateAllAssets(folder string) error {
return errors.Wrap(err, "copying all to public")
}
CopyToPublic(TmplBox, "", "robots.txt")
CopyToPublic(TmplBox, "", "banner.png")
CopyToPublic(TmplBox, "", "favicon.ico")
if err := CopyToPublic(TmplBox, "", "robots.txt"); err != nil {
return err
}
if err := CopyToPublic(TmplBox, "", "banner.png"); err != nil {
return err
}
if err := CopyToPublic(TmplBox, "", "favicon.ico"); err != nil {
return err
}
log.Infoln("Compiling CSS from SCSS style...")
err := CompileSASS(DefaultScss...)
log.Infoln("Statping assets have been inserted")
@ -165,13 +167,12 @@ func DeleteAllAssets(folder string) error {
// CopyAllToPublic will copy all the files in a rice box into a local folder
func CopyAllToPublic(box *rice.Box) error {
exclude := map[string]bool{
"base.gohtml": true,
"index.html": true,
}
err := box.Walk("/", func(path string, info os.FileInfo, err error) error {
return box.Walk("/", func(path string, info os.FileInfo, err error) error {
if info.Name() == "" {
return nil
}
@ -181,14 +182,12 @@ func CopyAllToPublic(box *rice.Box) error {
if info.IsDir() {
return nil
}
utils.Log.Infoln(path)
file, err := box.Bytes(path)
if err != nil {
return err
}
return SaveAsset(file, path)
})
return err
}
// CopyToPublic will create a file from a rice Box to the '/assets' directory

View File

@ -24,10 +24,9 @@ func TestCore_UsingAssets(t *testing.T) {
}
func TestCreateAssets(t *testing.T) {
CreateAllAssets(dir)
assert.Nil(t, CreateAllAssets(dir))
assert.True(t, UsingAssets(dir))
CompileSASS(DefaultScss...)
assert.FileExists(t, dir+"/assets/css/main.css")
assert.Nil(t, CompileSASS(DefaultScss...))
assert.FileExists(t, dir+"/assets/css/style.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
assert.FileExists(t, dir+"/assets/scss/base.scss")
@ -35,10 +34,10 @@ func TestCreateAssets(t *testing.T) {
assert.FileExists(t, dir+"/assets/scss/variables.scss")
}
//func TestCopyAllToPublic(t *testing.T) {
// err := CopyAllToPublic(TmplBox)
// require.Nil(t, err)
//}
func TestCopyAllToPublic(t *testing.T) {
err := CopyAllToPublic(TmplBox)
require.Nil(t, err)
}
func TestCompileSASS(t *testing.T) {
CompileSASS(DefaultScss...)

View File

@ -33,6 +33,7 @@ var testCheckinHits = []*CheckinHit{{
var testApiKey string
func TestInit(t *testing.T) {
t.Parallel()
err := utils.InitLogs()
require.Nil(t, err)
db, err := database.OpenTester()

View File

@ -14,30 +14,26 @@ func (n Notification) Name() string {
}
// LastSent returns a time.Duration of the last sent notification for the notifier
func (n Notification) LastSent() time.Duration {
return time.Since(n.lastSent)
func (n Notification) LastSentDur() time.Duration {
return time.Since(n.LastSent)
}
func (n *Notification) CanSend() bool {
if !n.Enabled.Bool {
return false
}
// the last sent notification was past 1 minute (limit per minute)
if n.lastSent.Add(60 * time.Minute).Before(utils.Now()) {
if n.lastSentCount != 0 {
n.lastSentCount--
if n.LastSent.Add(60 * time.Minute).Before(utils.Now()) {
if n.LastSentCount != 0 {
n.LastSentCount--
}
}
// dont send if already beyond the notifier's limit
if n.lastSentCount >= n.Limits {
if n.LastSentCount >= n.Limits {
return false
}
// action to do since notifier is able to send
n.lastSentCount++
n.lastSent = utils.Now()
return true
}
@ -66,28 +62,3 @@ func (n *Notification) GetValue(dbField string) string {
return ""
}
}
// start will start the go routine for the notifier queue
func (n *Notification) Start() {
n.Running = make(chan bool)
}
// close will stop the go routine for queue
func (n *Notification) Close() {
if n.IsRunning() {
close(n.Running)
}
}
// IsRunning will return true if the notifier is currently running a queue
func (n *Notification) IsRunning() bool {
if n.Running == nil {
return false
}
select {
case <-n.Running:
return false
default:
return true
}
}

View File

@ -41,8 +41,18 @@ type Notification struct {
Running chan bool `gorm:"-" json:"-"`
Form []NotificationForm `gorm:"-" json:"form"`
lastSent time.Time `gorm:"-" json:"-"`
lastSentCount int `gorm:"-" json:"-"`
LastSent time.Time `gorm:"-" json:"-"`
LastSentCount int `gorm:"-" json:"-"`
sentCount int `gorm:"-" json:"-"`
Logs []*NotificationLog `gorm:"-" json:"logs,omitempty"`
}
type NotificationLog struct {
Message string `gorm:"-" json:"message"`
Error error `gorm:"-" json:"error,omitempty"`
Success bool `gorm:"-" json:"success"`
Service int64 `gorm:"-" json:"service"`
CreatedAt time.Time `gorm:"-" json:"created_at"`
}
func (n *Notification) Logger() *logrus.Logger {

View File

@ -19,6 +19,13 @@ func (s *Service) AfterFind() {
metrics.Query("service", "find")
}
func (s *Service) AfterCreate() error {
s.prevOnline = true
allServices[s.Id] = s
metrics.Query("service", "create")
return nil
}
func (s *Service) AfterUpdate() {
metrics.Query("service", "update")
}
@ -76,12 +83,6 @@ func (s *Service) Create() error {
return nil
}
func (s *Service) AfterCreate() error {
allServices[s.Id] = s
metrics.Query("service", "create")
return nil
}
func (s *Service) Update() error {
q := db.Update(s)
allServices[s.Id] = s

View File

@ -178,32 +178,6 @@ func addDurations(s []series, on bool) int64 {
return dur
}
type ser struct {
Time time.Time
Online bool
}
type UptimeSeries struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Uptime int64 `json:"uptime"`
Downtime int64 `json:"downtime"`
Series []series `json:"series"`
}
type ByTime []ser
func (a ByTime) Len() int { return len(a) }
func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type series struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Online bool `json:"online"`
}
// Start will create a channel for the service checking go routine
func (s *Service) Start() {
if s.IsRunning() {
@ -257,6 +231,7 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
for _, c := range s.Checkins() {
s.AllCheckins = append(s.AllCheckins, c)
}
s.prevOnline = true
// collect initial service stats
s.UpdateStats()
allServices[s.Id] = s
@ -325,19 +300,12 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
return s.Online24Hours
}
// Uptime returns the duration of how long the service was online
func (s Service) Uptime() utils.Duration {
return utils.Duration{Duration: utils.Now().Sub(s.LastOffline)}
}
// Downtime returns the amount of time of a offline service
// Downtime returns the duration of how long the service has been offline
func (s Service) Downtime() utils.Duration {
return utils.Duration{Duration: utils.Now().Sub(s.LastOnline)}
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []Service
// Sort interface for resroting the Services in order
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }

View File

@ -0,0 +1,87 @@
package services
import (
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/utils"
)
func sendSuccess(s *Service) {
if !s.AllowNotifications.Bool {
return
}
if s.prevOnline == s.Online {
return
}
for _, n := range allNotifiers {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending notification to: %s!", notif.Method)
out, err := n.OnSuccess(*s)
if err != nil {
notif.Logger().Errorln(err)
logMessage(notif.Method, "", err, false, s.Id)
return
}
logMessage(notif.Method, out, nil, true, s.Id)
notif.LastSentCount++
notif.LastSent = utils.Now()
}
}
s.prevOnline = true
s.notifyAfterCount++
}
func sendFailure(s *Service, f *failures.Failure) {
if !s.AllowNotifications.Bool {
return
}
if s.prevOnline == s.Online && !s.UpdateNotify.Bool {
return
}
if s.NotifyAfter != 0 {
if s.NotifyAfter > s.notifyAfterCount {
s.notifyAfterCount++
return
}
}
for _, n := range allNotifiers {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending Failure notification to: %s!", notif.Method)
out, err := n.OnFailure(*s, *f)
if err != nil {
notif.Logger().WithField("failure", f.Issue).Errorln(err)
logMessage(notif.Method, "", err, false, s.Id)
}
logMessage(notif.Method, out, nil, false, s.Id)
notif.LastSentCount++
notif.LastSent = utils.Now()
}
}
s.prevOnline = false
s.notifyAfterCount++
}
func logMessage(method string, msg string, error error, onSuccesss bool, serviceId int64) {
notif := FindNotifier(method)
l := &notifications.NotificationLog{
Message: msg,
Error: error,
Success: onSuccesss,
Service: serviceId,
CreatedAt: utils.Now(),
}
notif.Logs = append(notif.Logs, l)
if len(notif.Logs) > 32 {
notif.Logs = notif.Logs[1:]
}
}

View File

@ -339,7 +339,6 @@ func RecordSuccess(s *Service) {
metrics.Gauge("online", 1., s.Name, s.Type)
metrics.Inc("success", s.Name)
sendSuccess(s)
s.SuccessNotified = true
}
func AddNotifier(n ServiceNotifier) {
@ -347,30 +346,6 @@ func AddNotifier(n ServiceNotifier) {
allNotifiers[notif.Method] = n
}
func sendSuccess(s *Service) {
if !s.AllowNotifications.Bool {
return
}
// dont send notification if server was already previous online
if s.SuccessNotified {
return
}
for _, n := range allNotifiers {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending notification to: %s!", notif.Method)
if _, err := n.OnSuccess(*s); err != nil {
notif.Logger().Errorln(err)
}
s.UserNotified = true
s.SuccessNotified = true
//s.UpdateNotify.Bool
}
}
s.notifyAfterCount = 0
}
// RecordFailure will create a new 'Failure' record in the database for a offline service
func RecordFailure(s *Service, issue string) {
s.LastOffline = utils.Now()
@ -389,42 +364,12 @@ func RecordFailure(s *Service, issue string) {
log.Error(err)
}
s.Online = false
s.SuccessNotified = false
s.DownText = s.DowntimeText()
metrics.Gauge("online", 0., s.Name, s.Type)
metrics.Inc("failure", s.Name)
sendFailure(s, fail)
}
func sendFailure(s *Service, f *failures.Failure) {
if !s.AllowNotifications.Bool {
return
}
// ignore failure if user was already notified and
// they have "continuous notifications" switched off.
if s.UserNotified && !s.UpdateNotify.Bool {
return
}
if s.notifyAfterCount > s.NotifyAfter {
for _, n := range allNotifiers {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending Failure notification to: %s!", notif.Method)
if _, err := n.OnFailure(*s, *f); err != nil {
notif.Logger().WithField("failure", f.Issue).Errorln(err)
}
s.UserNotified = true
s.SuccessNotified = true
//s.UpdateNotify.Bool
}
}
}
s.notifyAfterCount++
}
// Check will run checkHttp for HTTP services and checkTcp for TCP services
// if record param is set to true, it will add a record into the database.
func (s *Service) CheckService(record bool) {

View File

@ -41,13 +41,11 @@ func Example(online bool) Service {
Checkpoint: time.Time{},
SleepDuration: 5 * time.Second,
LastResponse: "The example service is hitting this page",
NotifyAfter: 0,
NotifyAfter: 2,
notifyAfterCount: 0,
AllowNotifications: null.NewNullBool(true),
UserNotified: false,
UpdateNotify: null.NewNullBool(true),
DownText: "The service was responding with 500 status code",
SuccessNotified: false,
LastStatusCode: 200,
Failures: nil,
AllCheckins: nil,
@ -56,6 +54,7 @@ func Example(online bool) Service {
LastCheck: utils.Now().Add(-37 * time.Second),
LastOnline: utils.Now().Add(-37 * time.Second),
LastOffline: utils.Now().Add(-75 * time.Second),
prevOnline: true,
}
}

View File

@ -107,8 +107,7 @@ func (s *exampleGRPC) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feat
}
func TestStartExampleEndpoints(t *testing.T) {
err := utils.InitLogs()
require.Nil(t, err)
startupDb(t)
// root CA for Linux: /etc/ssl/certs/ca-certificates.crt
// root CA for MacOSX: /opt/local/share/curl/curl-ca-bundle.crt
@ -178,7 +177,7 @@ func TestStartExampleEndpoints(t *testing.T) {
time.Sleep(15 * time.Second)
}
func TestServices(t *testing.T) {
func startupDb(t *testing.T) {
err := utils.InitLogs()
require.Nil(t, err)
db, err := database.OpenTester()
@ -199,6 +198,9 @@ func TestServices(t *testing.T) {
db.Create(&fail2)
db.Create(&incident1)
db.Create(&incidentUpdate1)
}
func TestServices(t *testing.T) {
tlsCert := utils.Params.GetString("STATPING_DIR") + "/cert.pem"
tlsCertKey := utils.Params.GetString("STATPING_DIR") + "/key.pem"
@ -222,7 +224,7 @@ func TestServices(t *testing.T) {
Timeout: 5,
VerifySSL: null.NewNullBool(false),
}
e, err = CheckHttp(e, false)
e, err := CheckHttp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -263,7 +265,7 @@ func TestServices(t *testing.T) {
TLSCert: null.NewNullString(tlsCert),
TLSCertKey: null.NewNullString(tlsCertKey),
}
e, err = CheckHttp(e, false)
e, err := CheckHttp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -279,7 +281,7 @@ func TestServices(t *testing.T) {
Type: "tcp",
Timeout: 5,
}
e, err = CheckTcp(e, false)
e, err := CheckTcp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -297,7 +299,7 @@ func TestServices(t *testing.T) {
TLSCert: null.NewNullString(tlsCert),
TLSCertKey: null.NewNullString(tlsCertKey),
}
e, err = CheckTcp(e, false)
e, err := CheckTcp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -313,7 +315,7 @@ func TestServices(t *testing.T) {
Type: "udp",
Timeout: 5,
}
e, err = CheckTcp(e, false)
e, err := CheckTcp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -329,7 +331,7 @@ func TestServices(t *testing.T) {
Type: "grpc",
Timeout: 5,
}
e, err = CheckGrpc(e, false)
e, err := CheckGrpc(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -338,14 +340,13 @@ func TestServices(t *testing.T) {
})
t.Run("Test ICMP Check", func(t *testing.T) {
t.SkipNow()
e := &Service{
Name: "Example ICMP",
Domain: "localhost",
Type: "icmp",
Timeout: 5,
}
e, err = CheckIcmp(e, false)
e, err := CheckIcmp(e, false)
require.Nil(t, err)
assert.True(t, e.Online)
assert.False(t, e.LastCheck.IsZero())
@ -522,6 +523,7 @@ func TestServices(t *testing.T) {
for _, c := range checkin {
assert.Len(t, c.Failures().List(), 0)
assert.Len(t, c.Hits(), 0)
assert.False(t, c.IsRunning())
}
assert.Len(t, item.AllFailures().List(), 0)
@ -591,10 +593,6 @@ services:
err = utils.DeleteFile(utils.Directory + "/services.yml")
require.Nil(t, err)
assert.NoFileExists(t, utils.Directory+"/services.yml")
})
t.Run("Test Close", func(t *testing.T) {
assert.Nil(t, db.Close())
})
}

View File

@ -0,0 +1,321 @@
package services
import (
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestServiceNotifications(t *testing.T) {
t.Run("Strategy #1 - Startup - [online, always notify changes, notify after first", func(t *testing.T) {
allNotifiers[notification.Method] = notification
service := Example(true)
failure := failures.Example()
service.UpdateNotify = null.NewNullBool(true)
service.NotifyAfter = 0
tests := []notifyTest{
{
Name: "service already online",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notification,
ExpectedSuccess: 0,
ExpectedFailures: 0,
CountLogs: 0,
},
{
Name: "service triggers online",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notification,
ExpectedSuccess: 0,
ExpectedFailures: 0,
CountLogs: 0,
},
{
Name: "service triggers offline, was online",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notification,
ExpectedSuccess: 0,
ExpectedFailures: 1,
CountLogs: 1,
},
{
Name: "service triggers offline again, already was offline, notify",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notification,
ExpectedSuccess: 0,
ExpectedFailures: 2,
CountLogs: 2,
},
}
runNotifyTests(t, notification, tests...)
})
t.Run("Strategy #2 - Delayed Failure - [online, notify only 1 time on change, notify after 2 changes", func(t *testing.T) {
allNotifiers[notification.Method] = notification
service := Example(true)
failure := failures.Example()
notif := notification
service.UpdateNotify = null.NewNullBool(false)
service.NotifyAfter = 2
assert.True(t, notif.CanSend())
assert.True(t, notif.Enabled.Bool)
tests := []notifyTest{
{
Name: "service already online",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 0,
ExpectedFailures: 2,
CountLogs: 2,
},
{
Name: "service triggers offline (1st)",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 0,
ExpectedFailures: 2,
CountLogs: 2,
},
{
Name: "service triggers offline (2nd) ignore",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 0,
ExpectedFailures: 2,
CountLogs: 2,
},
{
Name: "service triggers offline (3rd) NOTIFY",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 0,
ExpectedFailures: 3,
CountLogs: 3,
},
{
Name: "service triggers back online, notify",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 1,
ExpectedFailures: 3,
CountLogs: 4,
},
}
runNotifyTests(t, notif, tests...)
})
t.Run("Strategy #3 - Back Online - [offline, notify once for changes, notify after 2 changes", func(t *testing.T) {
allNotifiers[notification.Method] = notification
service := Example(false)
failure := failures.Example()
notif := notification
service.prevOnline = false
service.UpdateNotify = null.NewNullBool(false)
service.NotifyAfter = 2
assert.True(t, notif.CanSend())
assert.True(t, notif.Enabled.Bool)
tests := []notifyTest{
{
Name: "service already offline",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 1,
ExpectedFailures: 3,
CountLogs: 4,
},
{
Name: "service triggers offline again, ignore",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 1,
ExpectedFailures: 3,
CountLogs: 4,
},
{
Name: "service triggers offline again, ignore",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 1,
ExpectedFailures: 3,
CountLogs: 4,
},
{
Name: "service triggers back online, NOTIFY",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 2,
ExpectedFailures: 3,
CountLogs: 5,
},
{
Name: "service triggers online, ignore",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 2,
ExpectedFailures: 3,
CountLogs: 5,
},
}
runNotifyTests(t, notif, tests...)
})
t.Run("Strategy #4 - Disabled - [online, notifications are disabled", func(t *testing.T) {
allNotifiers[notification.Method] = notification
service := Example(false)
service.AllowNotifications = null.NewNullBool(false)
failure := failures.Example()
notif := notification
tests := []notifyTest{
{
Name: "service offline",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 2,
ExpectedFailures: 3,
CountLogs: 5,
},
{
Name: "service online",
OnSuccess: false,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 2,
ExpectedFailures: 3,
CountLogs: 5,
},
{
Name: "service offline",
OnSuccess: true,
Service: &service,
Failure: &failure,
Notifier: notif,
ExpectedSuccess: 2,
ExpectedFailures: 3,
CountLogs: 5,
},
}
runNotifyTests(t, notif, tests...)
})
t.Run("Test Close", func(t *testing.T) {
assert.Nil(t, db.Close())
})
}
func runNotifyTests(t *testing.T, notif *exampleNotifier, tests ...notifyTest) {
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
if test.OnSuccess {
RecordSuccess(test.Service)
} else {
RecordFailure(test.Service, "test issue")
}
assert.Equal(t, test.ExpectedSuccess, notif.success)
assert.Equal(t, test.ExpectedFailures, notif.failures)
assert.Equal(t, test.CountLogs, notif.LastSentCount)
})
}
}
var notification = &exampleNotifier{Notification: &notifications.Notification{
Method: "test",
CreatedAt: utils.Now().Add(-5 * time.Second),
Limits: 60,
Enabled: null.NewNullBool(true),
}, failures: 0, success: 0}
type notifyTest struct {
Name string
OnSuccess bool
Service *Service
Failure *failures.Failure
Notifier ServiceNotifier
ExpectedSuccess int
ExpectedFailures int
CountLogs int
}
type exampleNotifier struct {
*notifications.Notification
failures int
success int
saves int
tests int
}
func (e *exampleNotifier) OnSuccess(s Service) (string, error) {
e.success++
return "", nil
}
func (e *exampleNotifier) OnFailure(s Service, f failures.Failure) (string, error) {
e.failures++
return "", nil
}
func (e *exampleNotifier) OnSave() (string, error) {
e.saves++
return "", nil
}
func (e *exampleNotifier) Select() *notifications.Notification {
return e.Notification
}
func (e *exampleNotifier) OnTest() (string, error) {
e.tests++
return "", nil
}

View File

@ -44,12 +44,9 @@ type Service struct {
SleepDuration time.Duration `gorm:"-" json:"-" yaml:"-"`
LastResponse string `gorm:"-" json:"-" yaml:"-"`
NotifyAfter int64 `gorm:"column:notify_after" json:"notify_after" yaml:"notify_after" scope:"user,admin"`
notifyAfterCount int64 `gorm:"-" json:"-" yaml:"-"`
AllowNotifications null.NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" yaml:"allow_notifications" scope:"user,admin"`
UserNotified bool `gorm:"-" json:"-" yaml:"-"` // True if the User was already notified about a Downtime
UpdateNotify null.NullBool `gorm:"default:true;column:notify_all_changes" json:"notify_all_changes" yaml:"notify_all_changes" scope:"user,admin"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool`
DownText string `gorm:"-" json:"-" yaml:"-"` // Contains the current generated Downtime Text
SuccessNotified bool `gorm:"-" json:"-" yaml:"-"` // Is 'true' if the user has already be informed that the Services now again available
DownText string `gorm:"-" json:"-" yaml:"-"` // Contains the current generated Downtime Text // Is 'true' if the user has already be informed that the Services now again available // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code" yaml:"-"`
Failures []*failures.Failure `gorm:"-" json:"failures,omitempty" yaml:"-" scope:"user,admin"`
AllCheckins []*checkins.Checkin `gorm:"-" json:"checkins,omitempty" yaml:"-" scope:"user,admin"`
@ -59,10 +56,47 @@ type Service struct {
LastOnline time.Time `gorm:"-" json:"last_success" yaml:"-"`
LastOffline time.Time `gorm:"-" json:"last_error" yaml:"-"`
Stats *Stats `gorm:"-" json:"stats,omitempty" yaml:"-"`
notifyAfterCount int64 `gorm:"-" json:"-" yaml:"-"`
prevOnline bool `gorm:"-" json:"-" yaml:"-"`
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []Service
// Sort interface for resroting the Services in order
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }
type Stats struct {
Failures int `gorm:"-" json:"failures"`
Hits int `gorm:"-" json:"hits"`
FirstHit time.Time `gorm:"-" json:"first_hit"`
}
type ser struct {
Time time.Time
Online bool
}
type UptimeSeries struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Uptime int64 `json:"uptime"`
Downtime int64 `json:"downtime"`
Series []series `json:"series"`
}
type ByTime []ser
func (a ByTime) Len() int { return len(a) }
func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type series struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Duration int64 `json:"duration"`
Online bool `json:"online"`
}

View File

@ -1 +1 @@
0.90.61
0.90.62