mirror of https://github.com/statping/statping
0.90.62 updates
parent
84e68ea183
commit
1a438a6198
|
@ -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)
|
||||
|
|
28
Makefile
28
Makefile
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -154,6 +154,10 @@
|
|||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.service_li {
|
||||
min-height: 115px !important;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
line-height: 1.3;
|
||||
font-size: 0.75rem;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
@import 'base';
|
||||
@import 'layout';
|
||||
@import 'forms';
|
||||
@import 'mobile';
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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("}}")) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')}}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">×</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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">×</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") {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -34,17 +34,16 @@ var slacker = &slack{¬ifications.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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 := ¬ifications.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:]
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -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: ¬ifications.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
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.90.61
|
||||
0.90.62
|
||||
|
|
Loading…
Reference in New Issue