mirror of https://github.com/statping/statping
UI, and more issue fixes
parent
6257ecb5cb
commit
3b7df28c47
|
@ -1,2 +1,3 @@
|
|||
github: hunterlong
|
||||
patreon: statping
|
||||
custom: ['https://www.buymeacoffee.com/hunterlong']
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
# 0.90.26 (04-13-2020)
|
||||
- Fixed Delete Failures button/function
|
||||
- Removed timezone field from Settings (core)
|
||||
- Modified CDN asset URL
|
||||
- Fixed single Service view, more complex charts
|
||||
|
||||
# 0.90.25
|
||||
- Added string response on OnTest for Notifiers
|
||||
- Modified UI to show user the response for a Notifier.
|
||||
|
|
4
Makefile
4
Makefile
|
@ -27,8 +27,8 @@ lite: clean
|
|||
|
||||
reup: down clean compose-build-full up
|
||||
|
||||
test: clean
|
||||
go test -v -p=4 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
|
||||
test: clean compile
|
||||
go test -v -p=1 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
|
||||
|
||||
# build all arch's and release Statping
|
||||
release: test-deps
|
||||
|
|
|
@ -146,6 +146,45 @@ type isObject interface {
|
|||
Db() Database
|
||||
}
|
||||
|
||||
func ParseRequest(r *http.Request) (*GroupQuery, error) {
|
||||
fields := parseGet(r)
|
||||
grouping := fields.Get("group")
|
||||
startField := utils.ToInt(fields.Get("start"))
|
||||
endField := utils.ToInt(fields.Get("end"))
|
||||
limit := utils.ToInt(fields.Get("limit"))
|
||||
offset := utils.ToInt(fields.Get("offset"))
|
||||
fill, _ := strconv.ParseBool(fields.Get("fill"))
|
||||
orderBy := fields.Get("order")
|
||||
if limit == 0 {
|
||||
limit = 10000
|
||||
}
|
||||
|
||||
if grouping == "" {
|
||||
grouping = "1h"
|
||||
}
|
||||
groupDur, err := time.ParseDuration(grouping)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
groupDur = 1 * time.Hour
|
||||
}
|
||||
|
||||
query := &GroupQuery{
|
||||
Start: time.Unix(startField, 0).UTC(),
|
||||
End: time.Unix(endField, 0).UTC(),
|
||||
Group: groupDur,
|
||||
Order: orderBy,
|
||||
Limit: int(limit),
|
||||
Offset: int(offset),
|
||||
FillEmpty: fill,
|
||||
}
|
||||
|
||||
if query.Start.After(query.End) {
|
||||
return nil, errors.New("start time is after ending time")
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
|
||||
fields := parseGet(r)
|
||||
grouping := fields.Get("group")
|
||||
|
@ -169,6 +208,9 @@ func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
|
|||
log.Errorln(err)
|
||||
groupDur = 1 * time.Hour
|
||||
}
|
||||
if endField == 0 {
|
||||
endField = utils.Now().Unix()
|
||||
}
|
||||
|
||||
query := &GroupQuery{
|
||||
Start: time.Unix(startField, 0).UTC(),
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
<base href="{{BasePath}}">
|
||||
{{if USE_CDN}}
|
||||
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/css/base.css">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/font/all.css">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/vendor.css">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/style.css">
|
||||
<link rel="stylesheet" href="https://assets.statping.com/main.css">
|
||||
{{else}}
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
||||
{{if USING_ASSETS}}
|
||||
|
@ -33,11 +33,11 @@
|
|||
<div id="app" class="statping_container"></div>
|
||||
|
||||
{{if USE_CDN}}
|
||||
<script src="https://assets.statping.com/js/bundle.js"></script>
|
||||
<script src="https://assets.statping.com/js/vendor.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/js/polyfill.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/js/style.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/js/main.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/bundle.js"></script>
|
||||
<script src="https://assets.statping.com/vendor.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/polyfill.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/style.chunk.js"></script>
|
||||
<script src="https://assets.statping.com/main.chunk.js"></script>
|
||||
{{else}}
|
||||
<% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %>
|
||||
<%= bodyTag %> <% }) %>
|
||||
|
|
|
@ -56,8 +56,8 @@ class Api {
|
|||
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_uptime(id) {
|
||||
return axios.get('api/services/' + id + '/uptime_data').then(response => (response.data))
|
||||
async service_uptime(id, start, end) {
|
||||
return axios.get('api/services/' + id + '/uptime_data?start=' + start + '&end=' + end).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_heatmap(id, start, end, group) {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
import Api from "../../API";
|
||||
|
||||
export default {
|
||||
name: 'Checkins',
|
|
@ -36,10 +36,12 @@
|
|||
</td>
|
||||
<td class="text-right">
|
||||
<div v-if="$store.state.admin" class="btn-group">
|
||||
<a @click.prevent="editGroup(group, edit)" href="#" class="btn btn-outline-secondary"><font-awesome-icon icon="chart-area" /> Edit</a>
|
||||
<a @click.prevent="deleteGroup(group)" href="#" class="btn btn-danger">
|
||||
<button @click.prevent="editGroup(group, edit)" href="#" class="btn btn-sm btn-outline-secondary">
|
||||
<font-awesome-icon icon="edit" />
|
||||
</button>
|
||||
<button @click.prevent="deleteGroup(group)" href="#" class="btn btn-sm btn-danger">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<h2>{{service.name}} Failures
|
||||
<span class="btn btn-outline-danger float-right">Delete All</span></h2>
|
||||
<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 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>
|
||||
|
@ -39,7 +43,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
import Api from "../../API";
|
||||
|
||||
export default {
|
||||
name: 'Failures',
|
||||
|
@ -72,13 +76,21 @@ export default {
|
|||
await this.gotoPage(1)
|
||||
},
|
||||
methods: {
|
||||
async deleteFailures() {
|
||||
const c = confirm('Are you sure you want to delete all failures?')
|
||||
if (c) {
|
||||
await Api.service_failures_delete(this.service)
|
||||
this.service = await Api.service(this.service.id)
|
||||
this.total = 0
|
||||
await this.load()
|
||||
}
|
||||
},
|
||||
async gotoPage(page) {
|
||||
this.page = page;
|
||||
|
||||
this.offset = (page-1) * this.limit;
|
||||
|
||||
window.console.log('page', this.page, this.limit, this.offset);
|
||||
|
||||
await this.load()
|
||||
},
|
||||
async load() {
|
||||
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
import Api from "../../API";
|
||||
import FormIncidentUpdates from "@/forms/IncidentUpdates";
|
||||
|
||||
export default {
|
|
@ -2,7 +2,9 @@
|
|||
<div class="row">
|
||||
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
|
||||
<span class="braker mt-1 mb-3"></span>
|
||||
<h6>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h6>
|
||||
<h6>Incident: {{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"/>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
|
||||
<span class="col-2 badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
|
||||
<span class="col-10">{{update.message}}</span>
|
||||
<span class="col-12 font-1 float-right text-black-50">{{ago(update.created_at)}} ago</span>
|
||||
<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 text-black-50 mt-2">{{ago(update.created_at)}} ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="service-chart-container">
|
||||
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../../API";
|
||||
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
|
||||
|
||||
export default {
|
||||
name: 'AdvancedChart',
|
||||
props: {
|
||||
service: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
start: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
end: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
updated: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
main_data: null,
|
||||
expanded_data: null,
|
||||
main_chart_options: {
|
||||
noData: {
|
||||
text: "Loading...",
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
offsetX: 0,
|
||||
offsetY: -20,
|
||||
style: {
|
||||
color: "#bababa",
|
||||
fontSize: '27px'
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
id: 'mainchart',
|
||||
events: {
|
||||
dataPointSelection: (event, chartContext, config) => {
|
||||
window.console.log('slect')
|
||||
window.console.log(event)
|
||||
},
|
||||
updated: (chartContext, config) => {
|
||||
window.console.log('updated')
|
||||
},
|
||||
beforeZoom: (chartContext, { xaxis }) => {
|
||||
const start = (xaxis.min / 1000).toFixed(0)
|
||||
const end = (xaxis.max / 1000).toFixed(0)
|
||||
window.console.log(start, end)
|
||||
this.updated(this.fromUnix(start), this.fromUnix(end))
|
||||
return {
|
||||
xaxis: {
|
||||
min: this.fromUnix(start),
|
||||
max: this.fromUnix(end)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrolled: (chartContext, { xaxis }) => {
|
||||
window.console.log(xaxis)
|
||||
},
|
||||
},
|
||||
height: 500,
|
||||
width: "100%",
|
||||
type: "area",
|
||||
animations: {
|
||||
enabled: false,
|
||||
initialAnimation: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
enabled: true
|
||||
},
|
||||
zoom: {
|
||||
enabled: true
|
||||
},
|
||||
toolbar: {
|
||||
show: true
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
curve: 'stepline',
|
||||
lineCap: 'butt',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
size: undefined,
|
||||
sizeOffset: 0
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: false,
|
||||
enabled: true,
|
||||
custom: function ({ 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>Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
|
||||
},
|
||||
fixed: {
|
||||
enabled: true,
|
||||
position: 'topRight',
|
||||
offsetX: -30,
|
||||
offsetY: 40,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
formatter: undefined,
|
||||
title: {
|
||||
formatter: (seriesName) => seriesName,
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
floating: true,
|
||||
axisTicks: {
|
||||
show: true
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
fill: {
|
||||
colors: ["#48d338"],
|
||||
opacity: 1,
|
||||
type: 'solid'
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
curve: 'smooth',
|
||||
lineCap: 'butt',
|
||||
colors: ["#3aa82d"],
|
||||
width: 2,
|
||||
}
|
||||
},
|
||||
expanded_chart_options: {
|
||||
chart: {
|
||||
id: "chart1",
|
||||
height: 130,
|
||||
type: "bar",
|
||||
foreColor: "#ccc",
|
||||
brush: {
|
||||
target: "chart2",
|
||||
enabled: true
|
||||
},
|
||||
selection: {
|
||||
enabled: true,
|
||||
fill: {
|
||||
color: "#fff",
|
||||
opacity: 0.4
|
||||
},
|
||||
xaxis: {
|
||||
min: new Date("27 Jul 2017 10:00:00").getTime(),
|
||||
max: new Date("14 Aug 2999 10:00:00").getTime()
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ["#FF0080"],
|
||||
stroke: {
|
||||
width: 2
|
||||
},
|
||||
grid: {
|
||||
borderColor: "#444"
|
||||
},
|
||||
markers: {
|
||||
size: 0
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.update_data();
|
||||
},
|
||||
computed: {
|
||||
main_chart () {
|
||||
return [{
|
||||
name: this.service.name,
|
||||
...this.convertToChartData(this.main_data)
|
||||
}]
|
||||
},
|
||||
expanded_chart () {
|
||||
return this.toBarData(this.expanded_data)
|
||||
},
|
||||
params () {
|
||||
return {start: this.toUnix(new Date(this.start)), end: this.toUnix(new Date(this.end))}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
start: function(n, o) {
|
||||
this.update_data()
|
||||
},
|
||||
end: function(n, o) {
|
||||
this.update_data()
|
||||
},
|
||||
group: function(n, o) {
|
||||
this.update_data()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update_data() {
|
||||
this.loading = true
|
||||
await this.chartHits()
|
||||
// await this.expanded_hits()
|
||||
this.loading = false
|
||||
},
|
||||
async expanded_hits() {
|
||||
this.expanded_data = await this.load_hits(0, 99999999999, "24h")
|
||||
},
|
||||
async chartHits() {
|
||||
this.main_data = await this.load_hits()
|
||||
},
|
||||
async load_hits(start=this.params.start, end=this.params.end, group=this.group) {
|
||||
return await Api.service_hits(this.service.id, start, end, group, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -49,12 +49,9 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-4 col-6 float-right">
|
||||
<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">
|
||||
<Analytics title="Last Failure" value="417 Days ago"/>
|
||||
<button v-if="!expanded" @click="setService" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
||||
View Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -133,6 +130,10 @@ export default {
|
|||
this.track_service = this.in_service
|
||||
},
|
||||
methods: {
|
||||
async setService() {
|
||||
await this.$store.commit('setService', this.service)
|
||||
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}})
|
||||
},
|
||||
async showMoreStats() {
|
||||
this.expanded = !this.expanded;
|
||||
|
||||
|
@ -174,7 +175,6 @@ export default {
|
|||
if (!this.timer_func) {
|
||||
this.timer_func = setInterval(async () => {
|
||||
this.track_service = await Api.service(this.service.id)
|
||||
window.console.log(this.track_service.name)
|
||||
}, this.track_service.check_interval * 1000)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,23 +67,23 @@
|
|||
<div class="card-footer">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-3">
|
||||
<div class="col-12 col-md-3 mb-2 mb-md-0">
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id} }" class="btn btn-block btn-white incident">
|
||||
Incidents
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="col-12 col-md-3 mb-2 mb-md-0">
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id} }" class="btn btn-block btn-white checkins">
|
||||
Checkins
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="col-12 col-md-3 mb-2 mb-md-0">
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/failures`, params: {id: service.id} }" class="btn btn-block btn-white failures">
|
||||
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3 pt-2">
|
||||
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
|
||||
<div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
|
||||
<span class="text-black-50 float-md-right">{{service.online_7_days}}% Uptime</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -155,13 +155,6 @@
|
|||
this.set2 = await this.getHits(24, "1h")
|
||||
this.set2_name = this.calc(this.set2)
|
||||
this.loaded = true
|
||||
},
|
||||
async deleteFailures() {
|
||||
const c = confirm('Are you sure you want to delete all failures?')
|
||||
if (c) {
|
||||
await Api.service_failures_delete(this.service)
|
||||
this.service = await Api.service(this.service.id)
|
||||
}
|
||||
},
|
||||
Tab(name) {
|
||||
if (this.openTab === name) {
|
||||
|
|
|
@ -42,6 +42,9 @@ export default Vue.mixin({
|
|||
dur(t1, t2) {
|
||||
return formatDistance(t1, t2)
|
||||
},
|
||||
format(val, type="EEEE, MMM do h:mma") {
|
||||
return format(val, type)
|
||||
},
|
||||
niceDate(val) {
|
||||
return format(parseISO(val), "EEEE, MMM do h:mma")
|
||||
},
|
||||
|
@ -119,6 +122,13 @@ export default Vue.mixin({
|
|||
return "bars"
|
||||
}
|
||||
},
|
||||
toBarData(data = []) {
|
||||
let newSet = [];
|
||||
data.forEach((f) => {
|
||||
newSet.push([this.toUnix(this.parseISO(f.timeframe)), f.amount])
|
||||
})
|
||||
return newSet
|
||||
},
|
||||
convertToChartData(data = [], multiplier=1, asInt=false) {
|
||||
if (!data) {
|
||||
return {data: []}
|
||||
|
|
|
@ -21,69 +21,42 @@
|
|||
</div>
|
||||
|
||||
<div class="row mt-5 mb-4">
|
||||
<span class="col-6 font-2">
|
||||
<flatPickr v-model="start_time" type="text" name="start_time" class="form-control form-control-plaintext" id="start_time" value="0001-01-01T00:00:00Z" required />
|
||||
</span>
|
||||
<span class="col-6 font-2">
|
||||
<flatPickr v-model="end_time" type="text" name="end_time" class="form-control form-control-plaintext" id="end_time" value="0001-01-01T00:00:00Z" required />
|
||||
</span>
|
||||
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
|
||||
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
|
||||
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
|
||||
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
|
||||
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
|
||||
<option value="1m">1 Minute</option>
|
||||
<option value="5m">5 Minutes</option>
|
||||
<option value="15m">15 Minute</option>
|
||||
<option value="30m">30 Minutes</option>
|
||||
<option value="1h">1 Hour</option>
|
||||
<option value="3h">3 Hours</option>
|
||||
<option value="6h">6 Hours</option>
|
||||
<option value="12h">12 Hours</option>
|
||||
<option value="24h">1 Day</option>
|
||||
<option value="168h">7 Days</option>
|
||||
<option value="360h">15 Days</option>
|
||||
</select>
|
||||
<small class="d-block d-md-none d-block">Increment Timeframe</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="series" class="service-chart-container">
|
||||
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
|
||||
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
|
||||
|
||||
<div v-if="!loading" class="col-12">
|
||||
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
|
||||
</div>
|
||||
|
||||
<div class="service-chart-heatmap mt-5 mb-4">
|
||||
<ServiceHeatmap :service="service"/>
|
||||
</div>
|
||||
|
||||
<div v-if="load_timedata" class="col-12">
|
||||
|
||||
<apexchart width="100%" height="420" type="rangeBar" :options="timeRangeOptions" :series="rangeSeries"></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>
|
||||
<a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a>
|
||||
<a @click="tab='response'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Response</a>
|
||||
</nav>
|
||||
|
||||
|
||||
<div v-if="service.failures" class="tab-content">
|
||||
<div class="tab-pane fade active show">
|
||||
<ServiceFailures :service="service"/>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" :class="{active: tab === 'incidents'}" id="incidents">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" :class="{show: tab === 'checkins'}" id="checkins">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Checkin :service="service"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" :class="{show: tab === 'response'}" id="response">
|
||||
<div class="col-12 mt-4">
|
||||
<h3>Last Response</h3>
|
||||
<textarea rows="8" class="form-control" readonly>invalid route</textarea>
|
||||
<div class="form-group row mt-2">
|
||||
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" id="last_status_code" class="form-control" value="200" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -99,6 +72,7 @@
|
|||
import store from '../store'
|
||||
import flatPickr from 'vue-flatpickr-component';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
import AdvancedChart from "@/components/Service/AdvancedChart";
|
||||
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
|
||||
|
||||
const axisOptions = {
|
||||
|
@ -128,6 +102,7 @@
|
|||
export default {
|
||||
name: 'Service',
|
||||
components: {
|
||||
AdvancedChart,
|
||||
ServiceTopStats,
|
||||
ServiceHeatmap,
|
||||
ServiceFailures,
|
||||
|
@ -137,16 +112,18 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
tab: "failures",
|
||||
authenticated: false,
|
||||
ready: true,
|
||||
group: "1h",
|
||||
data: null,
|
||||
uptime_data: null,
|
||||
loading: true,
|
||||
messages: [],
|
||||
failures: [],
|
||||
start_time: this.nowSubtract(84600 * 30),
|
||||
end_time: new Date(),
|
||||
timedata: [],
|
||||
end_time: this.nowSubtract(0),
|
||||
timedata: null,
|
||||
load_timedata: false,
|
||||
dailyRangeOpts: {
|
||||
chart: {
|
||||
|
@ -157,8 +134,21 @@ export default {
|
|||
},
|
||||
timeRangeOptions: {
|
||||
chart: {
|
||||
height: 200,
|
||||
type: 'rangeBar'
|
||||
id: 'uptime',
|
||||
height: 120,
|
||||
type: 'rangeBar',
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
enabled: true
|
||||
},
|
||||
zoom: {
|
||||
enabled: true
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
|
@ -170,16 +160,10 @@ export default {
|
|||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: (val, opts) => {
|
||||
var label = opts.w.globals.labels[opts.dataPointIndex]
|
||||
var a = this.parseISO(val[0])
|
||||
var b = this.parseISO(val[1])
|
||||
return label
|
||||
},
|
||||
style: {
|
||||
colors: ['#f3f4f5', '#fff']
|
||||
}
|
||||
enabled: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime'
|
||||
|
@ -195,12 +179,32 @@ export default {
|
|||
}
|
||||
},
|
||||
chartOptions: {
|
||||
noData: {
|
||||
text: "Loading...",
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
offsetX: 0,
|
||||
offsetY: -20,
|
||||
style: {
|
||||
color: "#bababa",
|
||||
fontSize: '27px'
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
id: 'mainchart',
|
||||
events: {
|
||||
beforeZoom: async (chartContext, { xaxis }) => {
|
||||
dataPointSelection: (event, chartContext, config) => {
|
||||
window.console.log('slect')
|
||||
window.console.log(event)
|
||||
},
|
||||
updated: (chartContext, config) => {
|
||||
window.console.log('updated')
|
||||
},
|
||||
beforeZoom: (chartContext, { xaxis }) => {
|
||||
const start = (xaxis.min / 1000).toFixed(0)
|
||||
const end = (xaxis.max / 1000).toFixed(0)
|
||||
await this.chartHits(start, end, "10m")
|
||||
this.start_time = this.fromUnix(start)
|
||||
this.end_time = this.fromUnix(end)
|
||||
return {
|
||||
xaxis: {
|
||||
min: this.fromUnix(start),
|
||||
|
@ -208,6 +212,9 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
scrolled: (chartContext, { xaxis }) => {
|
||||
window.console.log(xaxis)
|
||||
},
|
||||
},
|
||||
height: 500,
|
||||
width: "100%",
|
||||
|
@ -233,20 +240,28 @@ export default {
|
|||
lineCap: 'butt',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true
|
||||
}
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
strokeWidth: 0,
|
||||
hover: {
|
||||
size: undefined,
|
||||
sizeOffset: 0
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: false,
|
||||
enabled: true,
|
||||
|
@ -259,7 +274,7 @@ export default {
|
|||
} 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>`
|
||||
return `<div class="chartmarker"><span>Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
|
||||
},
|
||||
fixed: {
|
||||
enabled: true,
|
||||
|
@ -268,9 +283,8 @@ export default {
|
|||
offsetY: 40,
|
||||
},
|
||||
x: {
|
||||
show: false,
|
||||
format: 'dd MMM',
|
||||
formatter: undefined,
|
||||
show: true,
|
||||
|
||||
},
|
||||
y: {
|
||||
formatter: undefined,
|
||||
|
@ -320,63 +334,84 @@ export default {
|
|||
core () {
|
||||
return this.$store.getters.core
|
||||
},
|
||||
uptime_data() {
|
||||
const data = this.timedata.series.filter(g => g.online)
|
||||
const offData = this.timedata.series.filter(g => !g.online)
|
||||
let arr = [];
|
||||
data.forEach((d) => {
|
||||
arr.push({
|
||||
name: "Online", data: {
|
||||
x: 'Online',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#0db407'
|
||||
}
|
||||
})
|
||||
})
|
||||
offData.forEach((d) => {
|
||||
arr.push({
|
||||
name: "offline", data: {
|
||||
x: 'Offline',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#b40707'
|
||||
}
|
||||
})
|
||||
})
|
||||
return arr
|
||||
params () {
|
||||
return {start: this.toUnix(new Date(this.start_time)), end: this.toUnix(new Date(this.end_time))}
|
||||
},
|
||||
rangeSeries() {
|
||||
return [{data: this.time_chart_data}]
|
||||
id () {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
uptimeSeries () {
|
||||
return this.timedata.series
|
||||
},
|
||||
mainChart () {
|
||||
return [{
|
||||
name: this.service.name,
|
||||
...this.convertToChartData(this.data)
|
||||
}]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
service: function(n, o) {
|
||||
this.chartHits()
|
||||
this.fetchUptime()
|
||||
this.onnn()
|
||||
},
|
||||
load_timedata: function(n, o) {
|
||||
this.chartHits()
|
||||
this.onnn()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.id = this.$route.params.id;
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.$store.getters.service) {
|
||||
const s = await Api.service(this.id)
|
||||
this.$store.commit('setService', s)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updated_chart(start, end) {
|
||||
this.start_time = start
|
||||
this.end_time = end
|
||||
this.loading = false
|
||||
},
|
||||
async onnn() {
|
||||
this.loading = true
|
||||
await this.chartHits()
|
||||
await this.fetchUptime()
|
||||
this.loading = false
|
||||
},
|
||||
async fetchUptime() {
|
||||
this.timedata = await Api.service_uptime(this.id)
|
||||
this.load_timedata = true
|
||||
},
|
||||
async get() {
|
||||
const s = this.$store.getters.serviceByAll(this.id)
|
||||
window.console.log("service: ", s)
|
||||
this.getService(this.service)
|
||||
this.messages = this.$store.getters.serviceMessages(this.service.id)
|
||||
},
|
||||
const uptime = await Api.service_uptime(this.id, this.params.start, this.params.end)
|
||||
window.console.log(uptime)
|
||||
this.uptime_data = this.parse_uptime(uptime)
|
||||
},
|
||||
parse_uptime(timedata) {
|
||||
const data = timedata.series.filter((g) => g.online) || []
|
||||
const offData = timedata.series.filter((g) => !g.online) || []
|
||||
let arr = [];
|
||||
window.console.log(data)
|
||||
if (data) {
|
||||
data.forEach((d) => {
|
||||
arr.push({
|
||||
x: 'Online',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#0db407'
|
||||
})
|
||||
})
|
||||
}
|
||||
if (offData) {
|
||||
offData.forEach((d) => {
|
||||
arr.push({
|
||||
x: 'Offline',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#b40707'
|
||||
})
|
||||
})
|
||||
}
|
||||
return [{data: arr}]
|
||||
},
|
||||
messageInRange(message) {
|
||||
const start = this.isBetween(new Date(), message.start_on)
|
||||
const end = this.isBetween(message.end_on, new Date())
|
||||
|
@ -387,31 +422,15 @@ export default {
|
|||
await this.serviceFailures()
|
||||
},
|
||||
async serviceFailures() {
|
||||
let tt = this.startEndTimes()
|
||||
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
|
||||
this.failures = await Api.service_failures(this.service.id, this.params.start, this.params.end)
|
||||
},
|
||||
async chartHits(start=0, end=99999999999, group="30m") {
|
||||
let tt = {};
|
||||
if (start === 0) {
|
||||
tt = this.startEndTimes()
|
||||
} else {
|
||||
tt = {start, end}
|
||||
}
|
||||
|
||||
this.data = await Api.service_hits(this.service.id, tt.start, tt.end, group, false)
|
||||
if (this.data.length === 0 && group !== "1h") {
|
||||
async chartHits(start=0, end=99999999999) {
|
||||
this.data = await Api.service_hits(this.service.id, this.params.start, this.params.end, this.group, false)
|
||||
if (this.data.length === 0 && this.group !== "1h") {
|
||||
this.group = "1h"
|
||||
await this.chartHits("1h")
|
||||
}
|
||||
this.series = [{
|
||||
name: this.service.name,
|
||||
...this.convertToChartData(this.data)
|
||||
}]
|
||||
this.ready = true
|
||||
},
|
||||
startEndTimes() {
|
||||
const start = this.toUnix(this.service.stats.first_hit)
|
||||
const end = this.toUnix(new Date())
|
||||
return {start, end}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import VueRouter from "vue-router";
|
|||
import Setup from "./forms/Setup";
|
||||
|
||||
import Api from "./API";
|
||||
import Incidents from "@/pages/Incidents";
|
||||
import Checkins from "@/pages/Checkins";
|
||||
import Failures from "@/pages/Failures";
|
||||
import Incidents from "@/components/Dashboard/Incidents";
|
||||
import Checkins from "@/components/Dashboard/Checkins";
|
||||
import Failures from "@/components/Dashboard/Failures";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ export default new Vuex.Store({
|
|||
core: {},
|
||||
token: null,
|
||||
services: [],
|
||||
service: null,
|
||||
groups: [],
|
||||
messages: [],
|
||||
users: [],
|
||||
|
@ -36,6 +37,7 @@ export default new Vuex.Store({
|
|||
core: state => state.core,
|
||||
token: state => state.token,
|
||||
services: state => state.services,
|
||||
service: state => state.service,
|
||||
groups: state => state.groups,
|
||||
messages: state => state.messages,
|
||||
incidents: state => state.incidents,
|
||||
|
@ -104,6 +106,9 @@ export default new Vuex.Store({
|
|||
setToken (state, token) {
|
||||
state.token = token
|
||||
},
|
||||
setService (state, service) {
|
||||
state.service = service
|
||||
},
|
||||
setServices (state, services) {
|
||||
state.services = services
|
||||
},
|
||||
|
|
|
@ -76,9 +76,6 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if c.Domain != app.Domain {
|
||||
app.Domain = c.Domain
|
||||
}
|
||||
if c.Timezone != app.Timezone {
|
||||
app.Timezone = c.Timezone
|
||||
}
|
||||
app.OAuth = c.OAuth
|
||||
app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
|
||||
app.AllowReports = null.NewNullBool(c.AllowReports.Bool)
|
||||
|
|
|
@ -88,8 +88,6 @@ func (s Storage) List() map[string]Item {
|
|||
|
||||
//Get a cached content by key
|
||||
func (s Storage) Get(key string) []byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
item := s.items[key]
|
||||
if item.Expired() {
|
||||
CacheStorage.Delete(key)
|
||||
|
|
|
@ -9,8 +9,6 @@ import (
|
|||
"github.com/statping/statping/types/services"
|
||||
"github.com/statping/statping/utils"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serviceOrder struct {
|
||||
|
@ -185,131 +183,37 @@ func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
allFailures := service.AllFailures()
|
||||
allHits := service.AllHits()
|
||||
|
||||
tMap := make(map[time.Time]bool)
|
||||
for _, v := range allHits.List() {
|
||||
tMap[v.CreatedAt] = true
|
||||
}
|
||||
for _, v := range allFailures.List() {
|
||||
tMap[v.CreatedAt] = false
|
||||
groupHits, err := database.ParseQueries(r, service.AllHits())
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var servs []ser
|
||||
for t, v := range tMap {
|
||||
s := ser{
|
||||
Time: t,
|
||||
Online: v,
|
||||
}
|
||||
servs = append(servs, s)
|
||||
groupFailures, err := database.ParseQueries(r, service.AllFailures())
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(ByTime(servs))
|
||||
var allFailures []*failures.Failure
|
||||
var allHits []*hits.Hit
|
||||
|
||||
var allTimes []series
|
||||
online := servs[0].Online
|
||||
thisTime := servs[0].Time
|
||||
for i := 0; i < len(servs); i++ {
|
||||
v := servs[i]
|
||||
|
||||
if v.Online != online {
|
||||
s := series{
|
||||
Start: thisTime,
|
||||
End: v.Time,
|
||||
Duration: v.Time.Sub(thisTime).Milliseconds(),
|
||||
Online: online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
thisTime = v.Time
|
||||
online = v.Online
|
||||
}
|
||||
if err := groupHits.Find(&allHits); err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
first := servs[0].Time
|
||||
last := servs[len(servs)-1].Time
|
||||
|
||||
if !service.Online {
|
||||
s := series{
|
||||
Start: allTimes[len(allTimes)-1].End,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(last).Milliseconds(),
|
||||
Online: service.Online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
} else {
|
||||
l := allTimes[len(allTimes)-1]
|
||||
allTimes[len(allTimes)-1] = series{
|
||||
Start: l.Start,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(l.Start).Milliseconds(),
|
||||
Online: true,
|
||||
}
|
||||
if err := groupFailures.Find(&allFailures); err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
jj := uptimeSeries{
|
||||
Start: first,
|
||||
End: last,
|
||||
Uptime: addDurations(allTimes, true),
|
||||
Downtime: addDurations(allTimes, false),
|
||||
Series: allTimes,
|
||||
uptimeData, err := service.UptimeData(allHits, allFailures)
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
returnJson(jj, w, r)
|
||||
}
|
||||
|
||||
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] }
|
||||
|
||||
func addDurations(s []series, on bool) int64 {
|
||||
var dur int64
|
||||
for _, v := range s {
|
||||
if v.Online == on {
|
||||
dur += v.Duration
|
||||
}
|
||||
}
|
||||
return dur
|
||||
}
|
||||
|
||||
type ser struct {
|
||||
Time time.Time
|
||||
Online bool
|
||||
}
|
||||
|
||||
func findNextFailure(m map[time.Time]bool, after time.Time, online bool) time.Time {
|
||||
for k, v := range m {
|
||||
if k.After(after) && v == online {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
//func calculateDuration(m map[time.Time]bool, on bool) time.Duration {
|
||||
// var t time.Duration
|
||||
// for t, v := range m {
|
||||
// if v == on {
|
||||
// t.
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
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 series struct {
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
Duration int64 `json:"duration"`
|
||||
Online bool `json:"online"`
|
||||
returnJson(uptimeData, w, r)
|
||||
}
|
||||
|
||||
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -32,7 +32,6 @@ type Core struct {
|
|||
Setup bool `gorm:"-" json:"setup"`
|
||||
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
|
||||
UseCdn null.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
|
||||
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
|
||||
LoggedIn bool `gorm:"-" json:"logged_in"`
|
||||
IsAdmin bool `gorm:"-" json:"admin"`
|
||||
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"`
|
||||
|
|
|
@ -3,11 +3,15 @@ package services
|
|||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/statping/statping/types"
|
||||
"github.com/statping/statping/types/failures"
|
||||
"github.com/statping/statping/types/hits"
|
||||
"github.com/statping/statping/types/null"
|
||||
"github.com/statping/statping/utils"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -19,6 +23,145 @@ func (s *Service) Duration() time.Duration {
|
|||
return time.Duration(s.Interval) * time.Second
|
||||
}
|
||||
|
||||
// Start will create a channel for the service checking go routine
|
||||
func (s *Service) UptimeData(hits []*hits.Hit, fails []*failures.Failure) (*UptimeSeries, error) {
|
||||
if len(hits) == 0 {
|
||||
return nil, errors.New("service does not have any successful hits")
|
||||
}
|
||||
// if theres no failures, then its been online 100%,
|
||||
// return a series from created time, to current.
|
||||
if len(fails) == 0 {
|
||||
fistHit := hits[0]
|
||||
duration := utils.Now().Sub(fistHit.CreatedAt).Milliseconds()
|
||||
set := []series{
|
||||
{
|
||||
Start: fistHit.CreatedAt,
|
||||
End: utils.Now(),
|
||||
Duration: duration,
|
||||
Online: true,
|
||||
},
|
||||
}
|
||||
out := &UptimeSeries{
|
||||
Start: fistHit.CreatedAt,
|
||||
End: utils.Now(),
|
||||
Uptime: duration,
|
||||
Downtime: 0,
|
||||
Series: set,
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
tMap := make(map[time.Time]bool)
|
||||
|
||||
for _, v := range hits {
|
||||
tMap[v.CreatedAt] = true
|
||||
}
|
||||
for _, v := range fails {
|
||||
tMap[v.CreatedAt] = false
|
||||
}
|
||||
|
||||
var servs []ser
|
||||
for t, v := range tMap {
|
||||
s := ser{
|
||||
Time: t,
|
||||
Online: v,
|
||||
}
|
||||
servs = append(servs, s)
|
||||
}
|
||||
if len(servs) == 0 {
|
||||
return nil, errors.New("error generating uptime data structure")
|
||||
}
|
||||
sort.Sort(ByTime(servs))
|
||||
|
||||
var allTimes []series
|
||||
online := servs[0].Online
|
||||
thisTime := servs[0].Time
|
||||
for i := 0; i < len(servs); i++ {
|
||||
v := servs[i]
|
||||
if v.Online != online {
|
||||
s := series{
|
||||
Start: thisTime,
|
||||
End: v.Time,
|
||||
Duration: v.Time.Sub(thisTime).Milliseconds(),
|
||||
Online: online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
thisTime = v.Time
|
||||
online = v.Online
|
||||
}
|
||||
}
|
||||
if len(allTimes) == 0 {
|
||||
return nil, errors.New("error generating uptime series structure")
|
||||
}
|
||||
|
||||
first := servs[0].Time
|
||||
last := servs[len(servs)-1].Time
|
||||
if !s.Online {
|
||||
s := series{
|
||||
Start: allTimes[len(allTimes)-1].End,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(last).Milliseconds(),
|
||||
Online: s.Online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
} else {
|
||||
l := allTimes[len(allTimes)-1]
|
||||
s := series{
|
||||
Start: l.Start,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(l.Start).Milliseconds(),
|
||||
Online: true,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
}
|
||||
|
||||
response := &UptimeSeries{
|
||||
Start: first,
|
||||
End: last,
|
||||
Uptime: addDurations(allTimes, true),
|
||||
Downtime: addDurations(allTimes, false),
|
||||
Series: allTimes,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func addDurations(s []series, on bool) int64 {
|
||||
var dur int64
|
||||
for _, v := range s {
|
||||
if v.Online == on {
|
||||
dur += v.Duration
|
||||
}
|
||||
}
|
||||
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() {
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.90.25
|
||||
0.90.26
|
||||
|
|
Loading…
Reference in New Issue