incidents vue

pull/429/head
Hunter Long 2020-03-12 21:06:06 -07:00
parent 89c103279b
commit d8a02b31a2
50 changed files with 4348 additions and 283 deletions

View File

@ -1,43 +1,12 @@
FROM node:10.17.0 AS frontend
RUN npm install yarn -g
WORKDIR /statping
COPY ./frontend/package.json .
COPY ./frontend/yarn.lock .
RUN yarn install --pure-lockfile --network-timeout 1000000
COPY ./frontend .
RUN yarn build && rm -rf node_modules && yarn cache clean
# Compiles webpacked Vue production build for frontend at /statping/dist
# Statping Golang BACKEND building from source
# Creates "/go/bin/statping" and "/usr/local/bin/sass" for copying
FROM golang:1.14-alpine AS backend
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
RUN apk add --update --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
WORKDIR /go/src/github.com/statping/statping
ADD go.mod go.sum ./
RUN go mod download
ENV GO111MODULE on
RUN go get github.com/stretchr/testify/assert && \
go get github.com/stretchr/testify/require && \
go get github.com/GeertJohan/go.rice/rice && \
go get github.com/cortesi/modd/cmd/modd && \
go get github.com/crazy-max/xgo
COPY . .
COPY --from=frontend /statping/dist/ ./source/dist/
RUN make clean generate embed build
RUN chmod a+x statping && mv statping /go/bin/statping
FROM statping/statping:base AS base
# Statping main Docker image that contains all required libraries
FROM alpine:latest
RUN apk --no-cache add libgcc libstdc++ curl jq
COPY --from=backend /go/bin/statping /usr/local/bin/
COPY --from=backend /usr/local/bin/sass /usr/local/bin/
COPY --from=backend /usr/local/share/ca-certificates /usr/local/share/
COPY --from=base /go/bin/statping /usr/local/bin/
COPY --from=base /usr/local/bin/sass /usr/local/bin/
COPY --from=base /usr/local/share/ca-certificates /usr/local/share/
WORKDIR /app

View File

@ -5,7 +5,7 @@ COPY ./frontend/package.json .
COPY ./frontend/yarn.lock .
RUN yarn install --pure-lockfile --network-timeout 1000000
COPY ./frontend .
RUN yarn build && rm -rf node_modules && yarn cache clean
RUN yarn build && yarn cache clean
# Statping Golang BACKEND building from source
@ -26,6 +26,9 @@ RUN go get github.com/stretchr/testify/assert && \
go get github.com/cortesi/modd/cmd/modd && \
go get github.com/crazy-max/xgo
COPY . .
COPY --from=frontend /statping/dist/ ./source/dist/
COPY --from=frontend /statping/ ./frontend/
RUN make clean generate embed build
RUN chmod a+x statping && mv statping /go/bin/statping
# /go/bin/statping - statping binary
# /usr/local/bin/sass - sass binary
# /statping - Vue frontend (from frontend)

View File

@ -20,9 +20,6 @@ up:
down:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml down --volumes --remove-orphans
test: clean
go test -v -p=1 -ldflags="-X main.VERSION=dev" -coverprofile=coverage.out ./...
lite: clean
docker build -t hunterlong/statping:dev -f dev/Dockerfile.dev .
docker-compose -f dev/docker-compose.lite.yml down
@ -30,6 +27,9 @@ lite: clean
reup: down clean compose-build-full up
test: clean
go test -v -p=1 -ldflags="-X main.VERSION=dev" -coverprofile=coverage.out ./...
yarn-serve:
cd frontend && yarn serve
@ -61,20 +61,23 @@ compose-build-full: docker-base
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml build --parallel --build-arg VERSION=${VERSION}
docker-base:
docker build -t hunterlong/statping:base -f Dockerfile.base --no-cache --build-arg VERSION=${VERSION} .
docker build -t statping/statping:base -f Dockerfile.base --no-cache --build-arg VERSION=${VERSION} .
docker-latest: docker-base
docker build -t hunterlong/statping:latest --build-arg VERSION=${VERSION} .
docker build -t statping/statping:latest --build-arg VERSION=${VERSION} .
docker-vue:
docker build -t hunterlong/statping:vue --build-arg VERSION=${VERSION} .
docker build -t statping/statping:vue --build-arg VERSION=${VERSION} .
docker-test:
docker-compose -f docker-compose.test.yml up --remove-orphans
push-base: docker-base
docker push hunterlong/statping:base
docker push statping/statping:base
push-vue: clean docker-base docker-vue
docker push hunterlong/statping:base
docker push hunterlong/statping:vue
docker push statping/statping:base
docker push statping/statping:vue
modd:
modd -f ./dev/modd.conf

View File

@ -41,16 +41,16 @@ func StartMaintenceRoutine() {
// databaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func databaseMaintence(dur time.Duration) {
deleteAfter := time.Now().UTC().Add(dur)
//deleteAfter := time.Now().UTC().Add(dur)
time.Sleep(20 * types.Second)
for range time.Tick(maintenceDuration) {
log.Infof("Deleting failures older than %s", dur.String())
DeleteAllSince("failures", deleteAfter)
//DeleteAllSince("failures", deleteAfter)
log.Infof("Deleting hits older than %s", dur.String())
DeleteAllSince("hits", deleteAfter)
//DeleteAllSince("hits", deleteAfter)
maintenceDuration = types.Hour
}

3474
dev/postman.json vendored Normal file

File diff suppressed because it is too large Load Diff

38
docker-compose.test.yml Normal file
View File

@ -0,0 +1,38 @@
version: '2.4'
services:
sut:
build:
context: .
dockerfile: ./Dockerfile.base
links:
- postgres
depends_on:
- postgres
entrypoint: make test
environment:
DB_CONN: postgres
DB_USER: root
DB_PASS: password123
DB_HOST: postgres
DB_DATABASE: statping
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping Testing
DOMAIN: http://localhost:8080
DESCRIPTION: This is a TESTING environment
ADMIN_USER: admin
ADMIN_PASS: admin
postgres:
image: postgres
environment:
POSTGRES_PASSWORD: password123
POSTGRES_DB: statping
POSTGRES_USER: root
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root"]
interval: 15s
timeout: 10s
retries: 20

View File

@ -7,7 +7,8 @@
"build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production",
"dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress",
"lint": "vue-cli-service lint",
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js",
"backend-test": "newman run -e ../dev/postman_environment.json --delay-request 500 ../dev/postman.json"
},
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",

View File

@ -40,6 +40,10 @@ class Api {
return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_ping(id, start, end, group, fill=true) {
return axios.get('/api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_failures_data(id, start, end, group, fill=true) {
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
@ -97,6 +101,22 @@ class Api {
return axios.delete('/api/users/' + id).then(response => (response.data))
}
async incident_update_create(incident, data) {
return axios.post('/api/incidents/'+incident.id+'/updates', data).then(response => (response.data))
}
async incidents_service(service) {
return axios.get('/api/services/'+service.id+'/incidents').then(response => (response.data))
}
async incident_create(data) {
return axios.post('/api/incidents', data).then(response => (response.data))
}
async incident_delete(incident) {
return axios.delete('/api/incidents/'+incident.id).then(response => (response.data))
}
async messages() {
return axios.get('/api/messages').then(response => (response.data))
}

View File

@ -4,23 +4,47 @@ HTML,BODY {
.index-chart {
height: $service-card-height;
/*Animation*/
-webkit-transition: height 0.3s ease;
-moz-transition: height 0.3s ease;
-o-transition: height 0.3s ease;
-ms-transition: height 0.3s ease;
transition: height 0.3s ease;
}
@-o-keyframes fadeIt {
0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; }
100% { background-color: #f5f5f5; }
}
@keyframes fadeIt {
0% { background-color: #f5f5f5; }
50% { background-color: #f2f2f2; }
100% { background-color: #f5f5f5; }
}
.backgroundAnimated {
background-image:none !important;
-o-animation: fadeIt 1s ease-in-out;
animation: fadeIt 1s ease-in-out;
animation-iteration-count: infinite;
}
.loader {
background-color: #f5f5f5;
min-height: 35px;
border-radius: 3px;
}
.sub-service-card {
border: 1px solid #dcdcdc87;
padding-top: 7px;
padding-bottom: 10px;
height: 155px;
min-height: 70px;
}
.expanded-service {
height: 89vh;
/*Animation*/
-webkit-transition: height 2s ease;
-moz-transition: height 2s ease;
-o-transition: height 2s ease;
-ms-transition: height 2s ease;
transition: height 2s ease;
height: 550px;
.stats_area {
display: none;
@ -140,7 +164,7 @@ HTML,BODY {
.stats_area {
text-align: center;
color: #a5a5a5;
line-height: 1.3;
}
.lower_canvas {
@ -171,39 +195,43 @@ HTML,BODY {
}
.font-0 {
font-size: 5pt;
font-size: 0.35rem;
}
.font-1 {
font-size: 7pt;
font-size: 0.8rem;
}
.font-2 {
font-size: 9pt;
font-size: 0.8rem;
}
.font-3 {
font-size: 11pt;
font-size: 1.1rem;
}
.font-4 {
font-size: 14pt;
font-size: 1.7rem;
}
.font-5 {
font-size: 17pt;
font-size: 2.3rem;
}
.font-6 {
font-size: 24pt;
font-size: 2.8rem;
}
.font-7 {
font-size: 31pt;
font-size: 3rem;
}
.font-8 {
font-size: 38pt;
font-size: 3.5rem;
}
.subtitle {
color: $subtitle-color;
}
.badge {
@ -261,7 +289,6 @@ HTML,BODY {
.card-body H4 A {
color: $service-title;
font-size: $service-title-size;
text-decoration: none;
}

View File

@ -4,10 +4,19 @@
background-color: $sm-background-color;
}
.index-chart {
height: 380px;
}
.online_list {
box-shadow: $mobile-card-shadow;
}
.expanded-service {
height: 810px;
}
.sm-container {
margin-top: 0px !important;
padding: 0 !important;
@ -71,8 +80,7 @@
border-radius: $sm-border-radius;
padding: $sm-padding;
background-color: $sm-service-background;
box-shadow: 0px 3px 10px 2px;
color: #b5b5b5;
box-shadow: $mobile-card-shadow;
}
.card-body {
@ -92,7 +100,6 @@
.stats_area .col-4 {
padding-left: 0;
padding-right: 0;
font-size: 0.6rem;
}
.navbar-item {
@ -121,4 +128,44 @@
.service-chart-container {
height: 200px;
}
.font-0 {
font-size: 0.35rem;
}
.font-1 {
font-size: 0.65rem;
}
.font-2 {
font-size: 0.8rem;
}
.font-3 {
font-size: 1rem;
}
.font-4 {
font-size: 1.2rem;
}
.font-5 {
font-size: 1.5rem;
}
.font-6 {
font-size: 1.9rem;
}
.font-7 {
font-size: 2.2rem;
}
.font-8 {
font-size: 2.5rem;
}
}

View File

@ -3,6 +3,8 @@ $background-color: #fcfcfc;
$max-width: 860px;
$title-color: #464646;
$description-color: #939393;
$subtitle-color: #747474;
$mobile-card-shadow: 2px 3px 10px #b7b7b7;
/* Status Container */
$service-background: #ffffff;
@ -12,7 +14,7 @@ $service-title-size: 1.8rem;
$service-stats-color: #4f4f4f;
$service-description-color: #fff;
$service-stats-size: 2.3rem;
$service-card-height: 480px !important;
$service-card-height: 480px;
/* Button Colors */
$success-color: #47d337;

View File

@ -1,9 +1,5 @@
<template>
<div>
<h3>Cache</h3>
<div v-if="!cache && cache.length !== 0" class="alert alert-danger">
There are no cached files
</div>
<table class="table">
<thead>
<tr>

View File

@ -1,18 +1,18 @@
<template>
<div class="col-12 mt-3">
<div class="col-12 mt-4 mt-md-3">
<div class="row stats_area mb-5">
<div class="col-4">
<span class="lg_number">{{$store.getters.services.length}}</span>
Total Services
<span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span>
<span class="font-2">Total Services</span>
</div>
<div class="col-4">
<span class="lg_number">{{failuresLast24Hours()}}</span>
Failures last 24 Hours
<span class="font-6 font-weight-bold d-block">{{failuresLast24Hours()}}</span>
<span class="font-2">Failures last 24 Hours</span>
</div>
<div class="col-4">
<span class="lg_number">{{$store.getters.onlineServices(true).length}}</span>
Online Services
<span class="font-6 font-weight-bold d-block">{{$store.getters.onlineServices(true).length}}</span>
<span class="font-2">Online Services</span>
</div>
</div>

View File

@ -19,7 +19,7 @@
<td class="d-none d-md-table-cell">
<router-link :to="serviceLink(message.service)">{{service(message.service)}}</router-link>
</td>
<td class="d-none d-md-table-cell">{{toLocal(message.start_on)}}</td>
<td class="d-none d-md-table-cell">{{niceDate(message.start_on)}}</td>
<td class="text-right">
<div class="btn-group">
<a @click.prevent="editMessage(message, edit)" href="#" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>

View File

@ -10,7 +10,6 @@
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Groups</div>
<div class="card-body">

View File

@ -1,6 +1,6 @@
<template>
<div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Users</div>
<div class="card-body">
<table class="table table-striped">
@ -18,7 +18,7 @@
<td>{{user.username}}</td>
<td v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td>
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td>
<td class="d-none d-md-table-cell">{{toLocal(user.updated_at)}}</td>
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
<td class="text-right">
<div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="" class="btn btn-outline-secondary"><font-awesome-icon icon="user" /> Edit</a>

View File

@ -4,7 +4,7 @@
<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">
<router-link class="no-decoration" :to="serviceLink(service)">{{service.name}}</router-link>
<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"/>

View File

@ -1,7 +1,7 @@
<template>
<div>
<h1 class="col-12 text-center pt-4 mt-4 header-title">{{$store.getters.core.name}}</h1>
<h5 class="col-12 text-center mb-5 header-desc">{{$store.getters.core.description}}</h5>
<h1 class="col-12 text-center pt-4 mt-4 mb-3 header-title font-6">{{$store.getters.core.name}}</h1>
<h5 class="col-12 text-center mb-5 header-desc font-3">{{$store.getters.core.description}}</h5>
</div>
</template>

View File

@ -1,15 +1,13 @@
<template>
<div class="col-6 mt-4">
<div class="col-12 sub-service-card">
<div class="col-8 float-left p-0 mt-1 mb-3">
<span class="font-5 d-block">{{title}}</span>
<span class="text-muted font-3 d-block font-weight-bold">{{subtitle}}</span>
<div class="col-12 col-md-6 mt-2 mt-md-4">
<div class="col-12 pt-2 sub-service-card">
<div class="col-8 float-left p-0">
<span class="font-4 d-block text-muted">{{func.title}}</span>
<span class="font-2 d-block subtitle">{{func.subtitle}}</span>
</div>
<div class="col-4 float-right text-right mt-2 p-0">
<span class="text-success font-5 font-weight-bold">{{value}}</span>
<span class="text-success font-4 font-weight-bold">{{func.value}}</span>
</div>
<MiniSparkLine :series="[{name: 'okokokok', data:[{x: '2019-01-01', y: 120},{x: '2019-01-02', y: 160},{x: '2019-01-03', y: 240},{x: '2019-01-04', y: 45}]}]"/>
</div>
</div>
</template>
@ -23,30 +21,24 @@
name: 'Analytics',
components: { MiniSparkLine, ServiceSparkLine },
props: {
title: {
type: String,
func: {
type: Object,
required: true
},
subtitle: {
type: String,
required: true
},
value: {
type: Number,
required: true
},
level: {
type: Number,
required: false
}
},
data() {
return {
value: 0,
title: "",
subtitle: "",
chart: [],
}
},
async mounted() {
await this.latencyYesterday();
this.value = this.func.value;
this.title = this.func.title;
this.subtitle = this.func.subtitle;
this.chart = this.convertToChartData(this.func.chart);
},
async latencyYesterday() {
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)

View File

@ -0,0 +1,21 @@
<template>
<div class="row stats_area mt-5 mb-4">
okok
</div>
</template>
<script>
export default {
name: 'Incidents',
props: {
service: {
type: Object,
required: true
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,28 +1,34 @@
<template>
<div class="mb-md-4 mb-5">
<div class="mb-md-4 mb-4">
<div class="card index-chart" :class="{'expanded-service': expanded}">
<div class="card-body">
<div class="col-12">
<h4 class="mt-3">
<router-link :to="serviceLink(service)" class="d-inline-block text-truncate" style="max-width: 65vw;" :in_service="service">{{service.name}}</router-link>
<h4 class="mt-2">
<router-link :to="serviceLink(service)" class="d-inline-block text-truncate font-4" style="max-width: 65vw;" :in_service="service">{{service.name}}</router-link>
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h4>
<ServiceTopStats :service="service"/>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" level="100" value="35%" subtitle="417 Days ago"/>
<Analytics title="Total Failures" level="100" value="35%" subtitle="417 Days ago"/>
<Analytics title="Highest Latency" level="100" value="450ms" subtitle="417 Days ago"/>
<Analytics title="Lowest Latency" level="100" value="120ms" subtitle="417 Days ago"/>
<Analytics title="Total Uptime" level="100" value="35%" subtitle="850ms"/>
<Analytics title="Total Downtime" level="100" value="35%" subtitle="32ms"/>
<Analytics title="Last Failure" :func="stats.total_failures"/>
<Analytics title="Total Failures" :func="stats.total_failures"/>
<Analytics title="Highest Latency" :func="stats.high_latency"/>
<Analytics title="Lowest Latency" :func="stats.lowest_latency"/>
<Analytics title="Total Uptime" :func="stats.high_ping"/>
<Analytics title="Total Downtime" :func="stats.low_ping"/>
<div class="col-12">
<router-link :to="serviceLink(service)" class="btn btn-block btn-outline-success mt-4" :class="{'btn-outline-success': service.online, 'btn-outline-danger': !service.online}">
View More Details
</router-link>
</div>
</div>
</div>
</div>
<div v-if="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<div v-show="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<ServiceChart :service="service" :visible="visible"/>
</div>
@ -36,16 +42,15 @@
<a v-for="(timeframe, i) in timeframes" @click="timeframe.picked = true" class="dropdown-item" href="#">{{timeframe.text}}</a>
</div>
<span class="d-none float-right d-md-inline">
<span class="d-none float-left d-md-inline">
{{smallText(service)}}
</span>
</div>
</div>
<div class="col-md-4 col-6 float-right">
<router-link :to="serviceLink(service)" class="d-none btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
View Service</router-link>
<button @click="expanded = !expanded" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
<button v-if="!expanded" @click="showMoreStats" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
<button v-if="expanded" @click="expanded = false" class="btn btn-sm float-right dyn-dark text-white" :class="{'btn-outline-success': service.online, 'bg-danger': !service.online}">Hide</button>
</div>
<div v-if="expanded" class="row">
@ -61,6 +66,7 @@
import Analytics from './Analytics';
import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import Graphing from '../../graphing'
export default {
name: 'ServiceBlock',
@ -82,17 +88,66 @@ export default {
{value: "3", text: "3 Hours" },
{value: "1m", text: "1 Month" },
{value: "3", text: "Last 3 Months" },
]
],
stats: {
total_failures: {
title: "Total Failures",
subtitle: "Last 7 Days",
value: 0,
},
high_latency: {
title: "Highest Latency",
subtitle: "Last 7 Days",
value: 0,
},
lowest_latency: {
title: "Lowest Latency",
subtitle: "Last 7 Days",
value: 0,
},
high_ping: {
title: "Highest Ping",
subtitle: "Last 7 Days",
value: 0,
},
low_ping: {
title: "Lowest Ping",
subtitle: "Last 7 Days",
value: 0,
}
}
}
},
methods: {
async showMoreStats() {
this.expanded = !this.expanded;
const failData = await Graphing.failures(this.service, 7)
this.stats.total_failures.chart = failData.data;
this.stats.total_failures.value = failData.total;
const hitsData = await Graphing.hits(this.service, 7)
this.stats.high_latency.chart = hitsData.chart;
this.stats.high_latency.value = this.humanTime(hitsData.high);
this.stats.lowest_latency.chart = hitsData.chart;
this.stats.lowest_latency.value = this.humanTime(hitsData.low);
const pingData = await Graphing.pings(this.service, 7)
this.stats.high_ping.chart = pingData.chart;
this.stats.high_ping.value = this.humanTime(pingData.high);
this.stats.low_ping.chart = pingData.chart;
this.stats.low_ping.value = this.humanTime(pingData.low);
},
smallText(s) {
if (s.online) {
return `Online, last checked ${this.ago(this.parseTime(s.last_success))}`
return `Online, last checked ${this.ago(s.last_success)}`
} else {
const last = s.last_failure
if (last) {
return `Offline, last error: ${last} ${this.ago(this.parseTime(last.created_at))}`
return `Offline, last error: ${last} ${this.ago(last.created_at)}`
}
return `Offline`
}

View File

@ -105,16 +105,16 @@
tooltip: {
theme: false,
enabled: true,
custom: function({series, seriesIndex, dataPointIndex, w}) {
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
if (val >= 1000) {
val = (val * 0.1).toFixed(0) + " milliseconds"
} else {
val = (val * 0.01).toFixed(0) + " microseconds"
}
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
let humanVal = this.humanTime(val);
return `<div class="chartmarker">
<span>Average Response Time: </span>
<span class="font-3">${humanVal}</span>
<span>${dt}</span>
</div>`
},
fixed: {
enabled: true,
@ -170,14 +170,14 @@
methods: {
async chartHits(group) {
const start = this.nowSubtract(84600 * 3)
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group, false)
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group, true)
if (this.data.length === 0 && group !== "1h") {
await this.chartHits("1h")
}
this.series = [{
name: this.service.name,
...this.convertToChartData(this.data, 0.01)
...this.convertToChartData(this.data)
}]
this.ready = true
}

View File

@ -4,12 +4,12 @@
<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>{{toLocal(failure.created_at)}}</small>
<small>{{niceDate(failure.created_at)}}</small>
</div>
<p class="mb-1">{{failure.issue}}</p>
</div>
<nav aria-label="Page navigation example">
<nav aria-label="page navigation example">
<ul class="pagination">
<li class="page-item">
<a class="page-link" href="#" aria-label="Previous">
@ -49,6 +49,7 @@ export default {
failures: [],
limit: 15,
offset: 0,
total: this.service.stats.failures
}
},
async mounted () {
@ -61,10 +62,6 @@ export default {
} else {
return `Offline, last error: ${s.last_failure.issue} ${this.ago(s.last_failure.created_at)}`
}
},
ago(t1) {
const tm = this.parseTime(t1)
return this.duration(this.$moment().utc(), tm)
}
}
}

View File

@ -110,7 +110,7 @@
let dataArr = []
data.forEach((d) => {
dataArr.push({x: this.parseTime(d.timeframe).getUTCDate(), y: d.amount});
dataArr.push({x: this.parseISO(d.timeframe), y: d.amount});
});
let date = new Date(dataArr[0].x);

View File

@ -1,5 +1,5 @@
<template v-if="service">
<div class="col-12 card mb-4" style="min-height: 360px" :class="{'offline-card': !service.online}">
<div class="col-12 card mb-4" style="min-height: 280px" :class="{'offline-card': !service.online}">
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<h4 class="card-title mb-4"><router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
@ -8,11 +8,11 @@
</h4>
<transition name="fade">
<div v-if="loaded && service.online" class="row">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0">
<div v-if="loaded && service.online" class="row pb-3">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div>
<div class="col-md-6 col-sm-12 mt-4 mt-md-0">
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3">
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
</div>
@ -42,8 +42,33 @@
:end="this.toUnix(this.nowSubtract(86400))"
group="24h" expression="latencyPercent"/>
</div>
<div class="col-4">
<button @click.prevent="openTab='incident'" class="btn btn-block btn-outline-secondary">Create Incident</button>
</div>
<div class="col-4">
<button @click.prevent="openTab='message'" class="btn btn-block btn-outline-secondary">Create Announcement</button>
</div>
<div class="col-4">
<button @click.prevent="openTab='failures'" class="btn btn-block btn-outline-secondary" :disabled="!service.failures">
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
</div>
<div v-if="openTab === 'incident'" class="col-12 mt-4">
<FormIncident :service="service" />
</div>
<div v-if="openTab === 'message'" class="col-12 mt-4">
<FormMessage :service="service"/>
</div>
<div v-if="openTab === 'failures'" class="col-12 mt-4">
<ServiceFailures :service="service"/>
</div>
</div>
</transition>
</div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
@ -55,6 +80,9 @@
</template>
<script>
import FormIncident from '../../forms/Incident';
import FormMessage from '../../forms/Message';
import ServiceFailures from './ServiceFailures';
import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../../API";
import StatsGen from "./StatsGen";
@ -62,7 +90,10 @@
export default {
name: 'ServiceInfo',
components: {
StatsGen,
ServiceFailures,
FormIncident,
FormMessage,
StatsGen,
ServiceSparkLine
},
props: {
@ -73,6 +104,7 @@
},
data() {
return {
openTab: "",
set1: [],
set2: [],
loaded: false,

View File

@ -65,7 +65,7 @@
fixed: {
enabled: true,
position: 'topRight',
offsetX: -5,
offsetX: 0,
offsetY: 0,
},
x: {

View File

@ -1,16 +1,16 @@
<template>
<div class="row stats_area mt-5 mb-4">
<div class="col-4">
<span class="lg_number">{{service.avg_response}}ms</span>
Average Response
<span class="font-5 d-block font-weight-bold">{{humanTime(service.avg_response)}}</span>
<span class="font-1 subtitle">Average Response</span>
</div>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Uptime last 24 Hours
<span class="font-5 d-block font-weight-bold">{{service.online_24_hours}}%</span>
<span class="font-1 subtitle">Uptime last 24 Hours</span>
</div>
<div class="col-4">
<span class="lg_number">{{service.online_7_days}}%</span>
Uptime last 7 Days
<span class="font-5 d-block font-weight-bold">{{service.online_7_days}}%</span>
<span class="font-1 subtitle">Uptime last 7 Days</span>
</div>
</div>
</template>

View File

@ -0,0 +1,101 @@
<template>
<div>
<div v-for="(incident, i) in incidents" class="card contain-card text-black-50 bg-white 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
</button></div>
<div class="card-body bg-light pt-1">
<FormIncidentUpdates :incident="incident"/>
<span class="font-2">Created: {{niceDate(incident.created_at)}} | Last Update: {{niceDate(incident.updated_at)}}</span>
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">Create Incident for {{service.name}}</div>
<div class="card-body">
<form @submit.prevent="createIncident">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input v-model="incident.title" type="text" name="title" class="form-control" id="title" placeholder="Incident Title" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea v-model="incident.description" rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="createIncident"
:disabled="!incident.title || !incident.description"
type="submit" class="btn btn-block btn-primary">
Create Incident
</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</template>
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import FormIncidentUpdates from './IncidentUpdates';
export default {
name: 'FormIncident',
components: {
FormIncidentUpdates
},
props: {
service: {
type: Object
}
},
data () {
return {
incident: {
title: "",
description: "",
service: this.service.id,
},
incidents: [],
}
},
async mounted () {
this.incidents = await Api.incidents_service(this.service)
},
methods: {
async createIncident() {
await Api.incident_create(this.incident)
const incidents = await Api.incidents()
this.$store.commit('setIncidents', incidents)
this.incident = {}
},
async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) {
await Api.incident_delete(incident)
this.incidents = await Api.incidents_service(this.service)
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,96 @@
<template>
<div>
<div v-for="(update, i) in updates" class="col-12 bg-active card pt-2 pb-2 mt-3 pl-3 pr-3">
<span class="font-4">
<font-awesome-icon v-if="update.type === 'Resolved'" icon="check-circle" class="mr-2"/>
<font-awesome-icon v-if="update.type === 'Update'" icon="asterisk" class="mr-2"/>
<font-awesome-icon v-if="update.type === 'Investigating'" icon="lightbulb" class="mr-2"/>
<font-awesome-icon v-if="update.type === 'Unknown'" icon="question" class="mr-2"/>
{{update.type}}
</span>
<span class="font-3 mt-3">{{update.message}}</span>
</div>
<div class="col-12 bg-active card pt-2 pb-2 mt-3 pl-3 pr-3">
<form @submit.prevent="createIncidentUpdate">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Update Type</label>
<div class="col-sm-8">
<select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option>
<option value="Update">Update</option>
<option value="Unknown">Unknown</option>
<option value="Resolved">Resolved</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">New Update</label>
<div class="col-sm-8">
<textarea v-model="incident_update.message" rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="createIncidentUpdate"
:disabled="!incident.title || !incident.description"
type="submit" class="btn btn-block btn-primary">
Add Update
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
export default {
name: 'FormIncidentUpdates',
components: {
},
props: {
incident: {
type: Object
}
},
data () {
return {
updates: this.incident.updates,
incident_update: {
incident: this.incident,
message: "",
type: ""
}
}
},
async mounted () {
this.updates = await Api.incident_updates(this.incident)
},
methods: {
async createIncidentUpdate(incident) {
await Api.incident_update_create(incident, this.incident_update)
const updates = await Api.incident_updates()
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Message"}}
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Annoucement"}}
<transition name="slide-fade">
<button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button>
@ -12,7 +12,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input v-model="message.title" type="text" name="title" class="form-control" id="title" placeholder="Message Title" required>
<input v-model="message.title" type="text" name="title" class="form-control" id="title" placeholder="Announcement Title" required>
</div>
</div>
@ -24,7 +24,7 @@
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<label class="col-sm-4 col-form-label">Announcement Date Range</label>
<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>
@ -33,7 +33,7 @@
</div>
</div>
<div class="form-group row">
<div v-show="this.service === null" class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select v-model="message.service_id" class="form-control" name="service" id="service_id">
@ -104,6 +104,9 @@
in_message: {
type: Object
},
service: {
type: Object
},
edit: {
type: Function
}
@ -135,7 +138,12 @@
this.message = this.in_message
}
},
methods: {
mounted () {
if (this.service) {
this.service_id = this.service.id
}
},
methods: {
startChange(e) {
window.console.log(e)
},

View File

@ -1,4 +1,7 @@
<template>
<div class="card text-black-50 bg-white mb-5">
<div class="card-header text-capitalize">{{notifier.title}}</div>
<div class="card-body">
<form @submit.prevent="saveNotifier">
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
@ -7,7 +10,6 @@
<i class="fa fa-smile-beam"></i> The {{notifier.method}} notifier is working correctly!
</div>
<h4 class="text-capitalize">{{notifier.title}}</h4>
<p class="small text-muted" v-html="notifier.description"/>
<div v-for="(form, index) in notifier.form" v-bind:key="index" class="form-group">
@ -50,11 +52,12 @@
</div>
<span class="d-block small text-center mt-5 mb-5">
</form>
</div>
<span class="d-block small text-center mb-3">
<span class="text-capitalize">{{notifier.title}}</span> Notifier created by <a :href="notifier.author_url" target="_blank">{{notifier.author}}</a>
</span>
</form>
</div>
</template>
<script>

View File

@ -79,7 +79,8 @@
},
watch: {
in_user() {
const u = this.in_user
let u = this.in_user
u.password = null
this.user = u
}
},

76
frontend/src/graphing.js Normal file
View File

@ -0,0 +1,76 @@
import axios from 'axios'
import API from './API'
import subSeconds from 'date-fns/subSeconds'
import getUnixTime from 'date-fns/getUnixTime'
class Graphing {
constructor () {
}
subtract(seconds) {
return getUnixTime(subSeconds(new Date(), seconds))
}
now() {
return getUnixTime(new Date())
}
async hits(service, days, group='24h') {
const query = await API.service_hits(service.id, this.subtract(86400 * days), this.now(), group)
let total = 0;
let high = 0;
let low = 99999999;
query.map((d) => {
if (high <= d.amount) {
high = d.amount
}
if (low >= d.amount && d.amount !== 0) {
low = d.amount
}
total += d.amount;
});
const average = total / query.length;
return {chart: query, average: average, total: total, high: high, low: low}
}
async pings(service, days, group='24h') {
const query = await API.service_ping(service.id, this.subtract(86400 * days), this.now(), group)
let total = 0;
let high = 0;
let low = 99999999;
query.map((d) => {
if (high <= d.amount) {
high = d.amount
}
if (low >= d.amount && d.amount !== 0) {
low = d.amount
}
total += d.amount;
});
const average = total / query.length;
return {chart: query, average: average, total: total, high: high, low: low}
}
async failures(service, days, group='24h') {
const query = await API.service_failures_data(service.id, this.subtract(86400 * days), this.now(), group)
let total = 0;
let high = 0;
let lowest = 99999999;
query.map((d) => {
if (d.amount >= high) {
high = d.amount
}
if (lowest >= d.amount) {
lowest = d.amount
}
total += d.amount;
});
const average = total / query.length;
return {data: query, average: average, total: total, high: high, low: lowest}
}
}
const graphing = new Graphing()
export default graphing

View File

@ -16,7 +16,7 @@ export default Vue.mixin({
return new Date.UTC(val)
},
ago(t1) {
return formatDistanceToNow(t1)
return formatDistanceToNow(parseISO(t1))
},
daysInMonth(t1) {
return lastDayOfMonth(t1)
@ -29,17 +29,10 @@ export default Vue.mixin({
},
niceDate(val) {
return format(parseISO(val), "EEEE, MMM do h:mma")
},
parseTime(val) {
return parseISO(val)
},
parseISO(v) {
return parseISO(v)
},
toLocal(val, suf = 'at') {
const t = this.parseTime(val)
return format(t, `EEEE, MMM do h:mma`)
},
toUnix(val) {
return getUnixTime(val)
},
@ -47,7 +40,7 @@ export default Vue.mixin({
return fromUnixTime(val)
},
isBetween(t1, t2) {
return differenceInSeconds(t1, t2) >= 0
return differenceInSeconds(parseISO(t1), parseISO(t2)) >= 0
},
hour() {
return 3600
@ -116,6 +109,12 @@ export default Vue.mixin({
})
return {data: newSet}
},
humanTime(val) {
if (val >= 10000) {
return Math.floor(val / 10000) + "ms"
}
return Math.floor(val / 1000) + "μs"
},
lastDayOfMonth(month) {
return new Date(Date.UTC(new Date().getUTCFullYear(), month + 1, 0))
},

View File

@ -3,6 +3,18 @@
<Header/>
<div v-show="$store.getters.groups.length === 0">
<div class="col-12">
<div class="col-12 loader backgroundAnimated"></div>
<div class="col-12 mt-3 border-0 loader backgroundAnimated" style="min-height: 200px;"></div>
</div>
<div class="col-12 mt-4">
<div class="col-12 loader backgroundAnimated"></div>
<div class="col-12 mt-3 border-0 loader backgroundAnimated" style="min-height: 200px;"></div>
</div>
</div>
<div v-for="(group, index) in $store.getters.groupsInOrder" v-bind:key="index">
<Group :group=group />
</div>

View File

@ -37,10 +37,6 @@
<ServiceHeatmap :service="service"/>
</div>
<!-- <div v-if="series" class="service-chart-container">-->
<!-- <apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></apexchart>-->
<!-- </div>-->
<nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
@ -295,8 +291,8 @@ export default {
this.messages = this.$store.getters.serviceMessages(this.service.id)
},
messageInRange(message) {
const start = this.isBetween(this.now(), this.parseTime(message.start_on))
const end = this.isBetween(this.parseTime(message.end_on), this.now())
const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date())
return start && end
},
async getService(s) {
@ -327,7 +323,7 @@ export default {
this.ready = true
},
startEndTimes() {
const start = this.toUnix(this.parseTime(this.service.stats.first_hit))
const start = this.toUnix(this.service.stats.first_hit)
const end = this.toUnix(new Date())
return {start, end}
}

View File

@ -35,6 +35,7 @@ export default new Vuex.Store({
services: state => state.services,
groups: state => state.groups,
messages: state => state.messages,
incidents: state => state.incidents,
users: state => state.users,
notifiers: state => state.notifiers,
@ -100,6 +101,9 @@ export default new Vuex.Store({
setMessages (state, messages) {
state.messages = messages
},
setIncidents (state, incidents) {
state.incidents = incidents
},
setUsers (state, users) {
state.users = users
},
@ -121,6 +125,8 @@ export default new Vuex.Store({
context.commit("setServices", services);
const messages = await Api.messages()
context.commit("setMessages", messages)
const incidents = await Api.incidents()
context.commit("setIncidents", incidents)
context.commit("setHasPublicData", true)
// if (core.logged_in) {
// const notifiers = await Api.notifiers()

View File

@ -100,6 +100,7 @@ func TestSetupRoutes(t *testing.T) {
}
return nil
},
AfterTest: StopServices,
}}
for _, v := range tests {
@ -284,3 +285,10 @@ func SetTestENV() error {
func UnsetTestENV() error {
return os.Setenv("GO_ENV", "production")
}
func StopServices() error {
for _, s := range services.All() {
s.Close()
}
return nil
}

View File

@ -13,6 +13,32 @@ func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
returnJson(inc, w, r)
}
func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incids := incidents.FindByService(utils.ToInt(vars["id"]))
returnJson(incids, w, r)
}
func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var update *incidents.IncidentUpdate
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&update)
if err != nil {
sendErrorJson(err, w, r)
return
}
update.IncidentId = utils.ToInt(vars["id"])
err = update.Create()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(update, "create", w, r)
}
func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
var incident *incidents.Incident
decoder := json.NewDecoder(r.Body)
@ -62,3 +88,18 @@ func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {
}
sendJsonAction(incident, "delete", w, r)
}
func apiDeleteIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
update, err := incidents.FindUpdate(utils.ToInt(vars["uid"]))
if err != nil {
sendErrorJson(err, w, r)
return
}
err = update.Delete()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(update, "delete", w, r)
}

View File

@ -131,10 +131,14 @@ func Router() *mux.Router {
//api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
// API INCIDENTS Routes
api.Handle("/api/incidents", readOnly(apiAllIncidentsHandler, false)).Methods("GET")
api.Handle("/api/incidents", authenticated(apiCreateIncidentHandler, false)).Methods("POST")
api.Handle("/api/incidents/:id", authenticated(apiIncidentUpdateHandler, false)).Methods("POST")
api.Handle("/api/incidents/:id", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
api.Handle("/api/services/{id}/incidents", http.HandlerFunc(apiServiceIncidentsHandler)).Methods("GET")
api.Handle("/api/services/{id}/incidents", authenticated(apiCreateIncidentHandler, false)).Methods("POST")
api.Handle("/api/incidents/{id}", authenticated(apiIncidentUpdateHandler, false)).Methods("POST")
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
// API INCIDENTS UPDATES Routes
api.Handle("/api/incidents/{id}/updates", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST")
api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE")
// API USER Routes
api.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")

View File

@ -104,7 +104,7 @@ func TestApiServiceRoutes(t *testing.T) {
Name: "Statping Reorder Services",
URL: "/api/services/reorder",
Method: "POST",
Body: `[{"service":1,"order":1},{"service":5,"order":2},{"service":2,"order":3},{"service":3,"order":4},{"service":4,"order":5}]`,
Body: `[{"service":1,"order":1},{"service":4,"order":2},{"service":2,"order":3},{"service":3,"order":4}]`,
ExpectedStatus: 200,
HttpHeaders: []string{"Content-Type=application/json"},
},
@ -114,18 +114,18 @@ func TestApiServiceRoutes(t *testing.T) {
HttpHeaders: []string{"Content-Type=application/json"},
Method: "POST",
Body: `{
"name": "New Service",
"domain": "https://statping.com",
"expected": "",
"expected_status": 200,
"check_interval": 30,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 30,
"order_id": 0
}`,
"name": "New Service",
"domain": "https://statping.com",
"expected": "",
"expected_status": 200,
"check_interval": 30,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 30,
"order_id": 0
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success","type":"service","method":"create"`},
FuncTest: func() error {
@ -142,18 +142,18 @@ func TestApiServiceRoutes(t *testing.T) {
HttpHeaders: []string{"Content-Type=application/json"},
Method: "POST",
Body: `{
"name": "Updated New Service",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 60,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0
}`,
"name": "Updated New Service",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 60,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"name":"Updated New Service"`, `"method":"update"`},
},

View File

@ -2,28 +2,37 @@ package handlers
import (
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
func TestApiUsersRoutes(t *testing.T) {
form := url.Values{}
form.Add("username", "adminupdated")
form.Add("password", "password12345")
badForm := url.Values{}
badForm.Add("username", "adminupdated")
badForm.Add("password", "wrongpassword")
tests := []HTTPTest{
{
Name: "Statping All Users",
URL: "/api/users",
Method: "GET",
ExpectedStatus: 200,
ResponseLen: 4,
ResponseLen: 1,
}, {
Name: "Statping Create User",
URL: "/api/users",
HttpHeaders: []string{"Content-Type=application/json"},
Method: "POST",
Body: `{
"username": "adminuser2",
"email": "info@adminemail.com",
"password": "passsword123",
"admin": true
}`,
"username": "adminuser2",
"email": "info@adminemail.com",
"password": "passsword123",
"admin": true
}`,
ExpectedStatus: 200,
}, {
Name: "Statping View User",
@ -35,35 +44,33 @@ func TestApiUsersRoutes(t *testing.T) {
URL: "/api/users/1",
Method: "POST",
Body: `{
"username": "adminupdated",
"email": "info@email.com",
"password": "password12345",
"admin": true
}`,
"username": "adminupdated",
"email": "info@email.com",
"password": "password12345",
"admin": true
}`,
ExpectedStatus: 200,
}, {
Name: "Statping Delete User",
URL: "/api/users/1",
URL: "/api/users/2",
Method: "DELETE",
ExpectedStatus: 200,
}, {
Name: "Statping Login User",
URL: "/api/login",
Method: "POST",
Body: `{
"username": "admin",
"password": "admin"
}`, ExpectedContains: []string{`"token"`},
ExpectedStatus: 200,
Name: "Statping Login User",
URL: "/api/login",
Method: "POST",
Body: form.Encode(),
ExpectedContains: []string{`"token"`},
ExpectedStatus: 200,
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
}, {
Name: "Statping Bad Login User",
URL: "/api/login",
Method: "POST",
Body: `{
"username": "admin",
"password": "wrongpassword"
}`, ExpectedContains: []string{`"token"`},
ExpectedStatus: 200,
Name: "Statping Bad Login User",
URL: "/api/login",
Method: "POST",
Body: badForm.Encode(),
ExpectedContains: []string{`incorrect authentication`},
ExpectedStatus: 200,
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
}}
for _, v := range tests {

View File

@ -62,7 +62,7 @@ func TestInit(t *testing.T) {
}
func TestFind(t *testing.T) {
Append(example)
appendList(example)
itemer, err := Find(example.Method)
require.Nil(t, err)

View File

@ -85,7 +85,9 @@ func (d *DbConfig) DropDatabase() error {
}
func (d *DbConfig) Close() {
d.Db.Close()
if d.Db != nil {
d.Db.Close()
}
}
// CreateDatabase will CREATE TABLES for each of the Statping elements

View File

@ -59,22 +59,21 @@ func (h Hitters) DeleteAll() error {
return q.Error()
}
func (h Hitters) Sum() float64 {
result := struct {
amount float64
}{0}
func (h Hitters) Sum() int64 {
var r IntResult
h.db.Select("AVG(latency) as amount").Scan(&result)
return result.amount
h.db.Select("CAST(SUM(latency) as INT) as amount").Scan(&r)
return r.Amount
}
func (h Hitters) Avg() float64 {
result := struct {
amount float64
}{0}
type IntResult struct {
Amount int64
}
h.db.Select("AVG(latency) as amount").Scan(&result)
return result.amount
func (h Hitters) Avg() int64 {
var r IntResult
h.db.Select("CAST(AVG(latency) as INT) as amount").Scan(&r)
return r.Amount
}
func AllHits(obj ColumnIDInterfacer) Hitters {

View File

@ -2,10 +2,14 @@ package incidents
import "github.com/statping/statping/database"
var db database.Database
var (
db database.Database
dbUpdate database.Database
)
func SetDB(database database.Database) {
db = database.Model(&Incident{})
dbUpdate = database.Model(&IncidentUpdate{})
}
func Find(id int64) (*Incident, error) {
@ -14,6 +18,23 @@ func Find(id int64) (*Incident, error) {
return &incident, q.Error()
}
func FindUpdate(id int64) (*IncidentUpdate, error) {
var update IncidentUpdate
q := dbUpdate.Where("id = ?", id).Find(&update)
return &update, q.Error()
}
func FindByService(id int64) []*Incident {
var incidents []*Incident
db.Where("service = ?", id).Find(&incidents)
for _, i := range incidents {
var updates []*IncidentUpdate
dbUpdate.Where("incident = ?", id).Find(&updates)
i.AllUpdates = updates
}
return incidents
}
func All() []*Incident {
var incidents []*Incident
db.Find(&incidents)

View File

@ -38,7 +38,7 @@ func humanMicro(val int64) string {
if val < 10000 {
return fmt.Sprintf("%d μs", val)
}
return fmt.Sprintf("%v ms", float64(val)*0.001)
return fmt.Sprintf("%0.0f ms", float64(val)*0.001)
}
// IsRunning returns true if the service go routine is running
@ -70,8 +70,6 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
for _, s := range all() {
allServices[s.Id] = s
if start {
CheckinProcess(s)
}
@ -85,6 +83,8 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
// collect initial service stats
s.UpdateStats()
allServices[s.Id] = s
}
return allServices, nil
@ -163,7 +163,7 @@ func (s *Service) UpdateStats() *Service {
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() float64 {
func (s *Service) AvgTime() int64 {
return s.AllHits().Avg()
}
@ -176,18 +176,22 @@ func (s *Service) OnlineDaysPercent(days int) float32 {
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 {
failed := s.FailuresSince(ago)
failsList := failed.List()
if len(failsList) == 0 {
failsList := failed.Count()
total := s.HitsSince(ago)
hitsList := total.Count()
if failsList == 0 {
s.Online24Hours = 100.00
return s.Online24Hours
}
total := s.HitsSince(ago)
hitsList := total.List()
if len(hitsList) == 0 {
if hitsList == 0 {
s.Online24Hours = 0
return s.Online24Hours
}
avg := float64(len(hitsList)) / float64(len(failsList)) * 100
avg := (float64(failsList) / float64(hitsList)) * 100
avg = 100 - avg
if avg < 0 {
avg = 0

View File

@ -239,7 +239,7 @@ func recordSuccess(s *Service) {
log.Error(err)
}
log.WithFields(utils.ToFields(hit, s)).Infoln(
fmt.Sprintf("Service #%d '%v' Successful Response: %v | Lookup in: %v | Online: %v | Interval: %d seconds", s.Id, s.Name, humanMicro(hit.Latency), humanMicro(hit.PingTime), s.Online, s.Interval))
fmt.Sprintf("Service #%d '%v' Successful Response: %s | Lookup in: %s | Online: %v | Interval: %d seconds", s.Id, s.Name, humanMicro(hit.Latency), humanMicro(hit.PingTime), s.Online, s.Interval))
s.LastLookupTime = hit.PingTime
s.LastLatency = hit.Latency
//notifiers.OnSuccess(s)

View File

@ -34,22 +34,22 @@ var example = &Service{
var hit1 = &hits.Hit{
Service: 1,
Latency: 0.1234,
PingTime: 0.01234,
Latency: 123456,
PingTime: 123456,
CreatedAt: utils.Now().Add(-120 * time.Second),
}
var hit2 = &hits.Hit{
Service: 1,
Latency: 0.2345,
PingTime: 0.02345,
Latency: 123456,
PingTime: 123456,
CreatedAt: utils.Now().Add(-60 * time.Second),
}
var hit3 = &hits.Hit{
Service: 1,
Latency: 0.3456,
PingTime: 0.03456,
Latency: 123456,
PingTime: 123456,
CreatedAt: utils.Now().Add(-30 * time.Second),
}
@ -65,7 +65,7 @@ var fail1 = &failures.Failure{
Issue: "example not found",
ErrorCode: 404,
Service: 1,
PingTime: 0.0123,
PingTime: 123456,
CreatedAt: utils.Now().Add(-160 * time.Second),
}
@ -73,7 +73,7 @@ var fail2 = &failures.Failure{
Issue: "example 2 not found",
ErrorCode: 500,
Service: 1,
PingTime: 0.0123,
PingTime: 123456,
CreatedAt: utils.Now().Add(-5 * time.Second),
}
@ -155,6 +155,13 @@ func TestService_Duration(t *testing.T) {
assert.Equal(t, float64(30), item.Duration().Seconds())
}
func TestService_CountHits(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
count := item.AllHits().Count()
assert.NotZero(t, count)
}
func TestService_AvgTime(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)

View File

@ -45,7 +45,7 @@ type Service struct {
PingTime int64 `gorm:"-" json:"ping_time"`
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
Online7Days float32 `gorm:"-" json:"online_7_days"`
AvgResponse float64 `gorm:"-" json:"avg_response"`
AvgResponse int64 `gorm:"-" json:"avg_response"`
FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"`
Running chan bool `gorm:"-" json:"-"`
Checkpoint time.Time `gorm:"-" json:"-"`
@ -71,11 +71,9 @@ type Service struct {
}
type Stats struct {
Failures int `gorm:"-" json:"failures"`
Hits int `gorm:"-" json:"hits"`
LastLookupTime int64 `gorm:"-" json:"last_lookup"`
LastLatency int64 `gorm:"-" json:"last_latency"`
FirstHit time.Time `gorm:"-" json:"first_hit"`
Failures int `gorm:"-" json:"failures"`
Hits int `gorm:"-" json:"hits"`
FirstHit time.Time `gorm:"-" json:"first_hit"`
}
// BeforeCreate for Service will set CreatedAt to UTC