Merge pull request #496 from statping/dev

deployment updates
pull/508/head
Hunter Long 2020-04-15 02:44:36 -07:00 committed by GitHub
commit 2a4c3d6f8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1785 additions and 744 deletions

1
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
github: hunterlong
patreon: statping
custom: ['https://www.buymeacoffee.com/hunterlong']

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ snap
prime
stage
parts
releases
core/rice-box.go
config.yml
*.db

View File

@ -10,6 +10,7 @@ before_script:
branches:
only:
- master
- dev
env:
global:
- "PATH=$HOME/.local/bin:$PATH"

View File

@ -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.

123
Makefile
View File

@ -7,11 +7,13 @@ XGO=xgo -go $(GOVERSION) --dest=build
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)"
TRVIS_SECRET=O3/2KTOV8krv+yZ1EB/7D1RQRe6NdpFUEJNJkMS/ollYqmz3x2mCO7yIgIJKCKguLXZxjM6CxJcjlCrvUwibL+8BBp7xJe4XFIOrjkPvbbVPry4HkFZCf2GfcUK6o4AByQ+RYqsW2F17Fp9KLQ1rL3OT3eLTwCAGKx3tlY8y+an43zkmo5dN64V6sawx26fh6XTfww590ey+ltgQTjf8UPNup2wZmGvMo9Hwvh/bYR/47bR6PlBh6vhlKWyotKf2Fz1Bevbu0zc35pee5YlsrHR+oSF+/nNd/dOij34BhtqQikUR+zQVy9yty8SlmneVwD3yOENvlF+8roeKIXb6P6eZnSMHvelhWpAFTwDXq2N3d/FIgrQtLxsAFTI3nTHvZgs6OoTd6dA0wkhuIGLxaL3FOeztCdxP5J/CQ9GUcTvifh5ArGGwYxRxQU6rTgtebJcNtXFISP9CEUR6rwRtb6ax7h6f1SbjUGAdxt+r2LbEVEk4ZlwHvdJ2DtzJHT5DQtLrqq/CTUgJ8SJFMkrJMp/pPznKhzN4qvd8oQJXygSXX/gz92MvoX0xgpNeLsUdAn+PL9KketfR+QYosBz04d8k05E+aTqGaU7FUCHPTLwlOFvLD8Gbv0zsC/PWgSLXTBlcqLEz5PHwPVHTcVzspKj/IyYimXpCSbvu1YOIjyc=
PUBLISH_BODY='{ "request": { "branch": "master", "message": "Homebrew update version v${VERSION}", "config": { "env": { "VERSION": "${VERSION}", "COMMIT": "$(TRAVIS_COMMIT)" } } } }'
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "merge_mode": "replace", "language": "go", "install": true, "sudo": "required", "services": ["docker"], "env": { "secure": "${TRVIS_SECRET}" }, "before_deploy": ["git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "git tag v$(VERSION) --force"], "deploy": [{ "provider": "releases", "api_key": "$$GITHUB_TOKEN", "file_glob": true, "file": "build/*", "skip_cleanup": true, "on": { "branch": "master" } }], "before_script": ["rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable", "nvm install 10.17.0", "nvm use 10.17.0 --default", "npm install -g sass", "npm install -g cross-env"], "script": ["travis_wait 30 docker pull crazymax/xgo:${GOVERSION}", "make release"], "after_success": [], "after_deploy": ["make publish-homebrew"] } } }'
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "merge_mode": "replace", "language": "go", "install": true, "sudo": "required", "services": ["docker"], "env": { "secure": "${TRVIS_SECRET}" }, "before_deploy": ["git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "git tag v$(VERSION) --force"], "deploy": [{ "provider": "releases", "api_key": "$$GITHUB_TOKEN", "file_glob": true, "file": "build/*", "skip_cleanup": true, "on": { "branch": "master" } }], "before_script": ["rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable", "nvm install 10.17.0", "nvm use 10.17.0 --default", "npm install -g sass yarn cross-env", "pip install --user awscli"], "script": ["make release"], "after_success": [], "after_deploy": ["make post-release"] } } }'
TEST_DIR=$(GOPATH)/src/github.com/statping/statping
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
OS = darwin freebsd linux openbsd
ARCHS = 386 arm amd64 arm64
all: clean yarn-install compile docker-base docker-vue build-all compress
all: clean yarn-install compile docker-base docker-vue build-all
up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
@ -27,8 +29,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
@ -55,6 +57,9 @@ test-deps:
go get github.com/mattn/goveralls
go get github.com/GeertJohan/go.rice/rice
deps:
go get -d -v -t ./...
protoc:
cd types/proto && protoc --gofast_out=plugins=grpc:. statping.proto
@ -146,6 +151,34 @@ install-local: build
generate:
cd source && go generate
build-bin:
mkdir build
export PWD=`pwd`
@for arch in $(ARCHS);\
do \
for os in $(OS);\
do \
echo "Building $$os-$$arch"; \
mkdir -p releases/statping-$$os-$$arch/; \
GOOS=$$os GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-$$os-$$arch/statping ${PWD}/cmd; \
chmod +x releases/statping-$$os-$$arch/statping; \
tar -czf releases/statping-$$os-$$arch.tar.gz -C releases/statping-$$os-$$arch statping; \
done \
done
find ./releases/ -name "*.tar.gz" -type f -size +1M -exec mv "{}" build/ \;
build-win:
export PWD=`pwd`
@for arch in $(ARCHS);\
do \
echo "Building windows-$$arch"; \
mkdir -p releases/statping-windows-$$arch/; \
GOOS=windows GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-windows-$$arch/statping.exe ${PWD}/cmd; \
chmod +x releases/statping-windows-$$arch/statping.exe; \
zip -j releases/statping-windows-$$arch.zip releases/statping-windows-$$arch/statping.exe || true; \
done
find ./releases/ -name "*.zip" -type f -size +1M -exec mv "{}" build/ \;
# remove files for a clean compile/build
clean:
rm -rf ./{logs,assets,plugins,*.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,index.html,vendor}
@ -168,7 +201,7 @@ clean:
find . -name "*.out" -type f -delete
find . -name "*.cpu" -type f -delete
find . -name "*.mem" -type f -delete
rm -rf {build,tmp}
rm -rf {build,releases,tmp}
print_details:
@echo \==== Statping Development Instance ====
@ -186,7 +219,7 @@ print_details:
@echo \==== Monitoring and IDE ====
@echo \Grafana: http://localhost:3000 \(username: admin, password: admin\)
build-all: xgo-install compile build-mac build-linux build-windows build-linux build-alpine compress
build-all: clean compile build-bin build-win
coverage: test-deps
$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS)
@ -200,62 +233,23 @@ download-key:
wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg
# build Statping for Mac, 64 and 32 bit
build-mac:
mkdir build
$(XGO) $(BUILDVERSION) --targets=darwin/amd64,darwin/386 ./cmd
# build Statping for Linux 64, 32 bit, arm6/arm7
build-linux:
$(XGO) $(BUILDVERSION) --targets=linux/amd64,linux/386,linux/arm-7,linux/arm-6,linux/arm64 ./cmd
# build for windows 64 bit only
build-windows:
$(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd
# build Alpine linux binary (used in docker images)
build-alpine:
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd
# build :latest docker tag
docker-build-latest:
docker build --build-arg VERSION=${VERSION} -t statping/statping:latest --no-cache -f Dockerfile .
docker tag statping/statping:latest statping/statping:v${VERSION}
# compress built binaries into tar.gz and zip formats
compress:
cd build && mv alpine-linux-amd64 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-alpine.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-darwin-10.6-amd64 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-osx-x64.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-darwin-10.6-386 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-osx-x32.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-linux-amd64 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-x64.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-linux-386 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-x32.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-windows-6.0-amd64.exe $(BINARY_NAME).exe
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME).exe
cd build && zip $(BINARY_NAME)-windows-x64.zip $(BINARY_NAME).exe statping.asc && rm -f $(BINARY_NAME).exe statping.asc
cd build && mv cmd-linux-arm-7 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-arm7.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-linux-arm-6 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-arm6.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
cd build && mv cmd-linux-arm64 $(BINARY_NAME)
cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME)
cd build && tar -czvf $(BINARY_NAME)-linux-arm64.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc
# push the :dev docker tag using curl
publish-dev:
curl -H "Content-Type: application/json" --data '{"docker_tag": "dev"}' -X POST $(DOCKER)
publish-latest: publish-base
curl -H "Content-Type: application/json" --data '{"docker_tag": "latest"}' -X POST $(DOCKER)
publish-base:
curl -H "Content-Type: application/json" --data '{"docker_tag": "base"}' -X POST $(DOCKER)
post-release: frontend-build upload_to_s3 publish-homebrew publish-latest
# update the homebrew application to latest for mac
publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests
@ -286,5 +280,28 @@ sentry-release:
sentry-cli releases set-commits --auto v${VERSION}
sentry-cli releases finalize v${VERSION}
snapcraft: clean snapcraft-build snapcraft-release
snapcraft-build: build-all
PWD=$(shell pwd)
cp build/$(BINARY_NAME)-linux-x64.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=amd64"
cp build/$(BINARY_NAME)-linux-x32.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=i386"
cp build/$(BINARY_NAME)-linux-arm64.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=arm64"
cp build/$(BINARY_NAME)-linux-arm7.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=armhf"
rm -f build/$(BINARY_NAME)-linux.tar.gz
snapcraft-release:
snapcraft push statping_${VERSION}_arm64.snap --release stable
snapcraft push statping_${VERSION}_i386.snap --release stable
snapcraft push statping_${VERSION}_armhf.snap --release stable
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release
.SILENT: travis_s3_creds

View File

@ -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(),

View File

@ -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 %> <% }) %>

View File

@ -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) {

View File

@ -41,7 +41,7 @@
</template>
<script>
import Api from "../API";
import Api from "../../API";
export default {
name: 'Checkins',

View File

@ -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>

View File

@ -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)
}
}

View File

@ -48,7 +48,7 @@
</template>
<script>
import Api from "../API";
import Api from "../../API";
import FormIncidentUpdates from "@/forms/IncidentUpdates";
export default {

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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: []}

View File

@ -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}
}
}
}

View File

@ -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 = [
{

View File

@ -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
},

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -32,7 +32,6 @@ statping_get_tarball() {
else
tar xzf $tarball_tmp -C "$temp"
fi
statping_verify_integrity "$temp"/statping
printf "$green> Installing to $DEST/statping\n"
mv "$temp"/statping "$DEST"
newversion=`$DEST/statping version`
@ -50,47 +49,15 @@ statping_get_tarball() {
fi
}
# Verifies the GPG signature of the tarball
statping_verify_integrity() {
# Check if GPG is installed
if [[ -z "$(command -v gpg)" ]]; then
printf "$yellow> WARNING: GPG is not installed, integrity can not be verified!$reset\n"
return
fi
if [ "$statping_GPG" == "no" ]; then
printf "$cyan> WARNING: Skipping GPG integrity check!$reset\n"
return
fi
printf "$cyan> Verifying integrity with gpg key from $gpgurl...$reset\n"
# Grab the public key if it doesn't already exist
gpg --list-keys $gpg_key >/dev/null 2>&1 || (curl -sS -L $gpgurl | gpg --import)
if [ ! -f "$1.asc" ]; then
printf "$red> Could not download GPG signature for this Statping release. This means the release can not be verified!$reset\n"
statping_verify_or_quit "> Do you really want to continue?"
return
fi
# Actually perform the verification
if gpg --verify "$1.asc" $1 &> /dev/null; then
printf "$green> GPG signature looks good$reset\n"
else
printf "$red> GPG signature for this Statping release is invalid! This is BAD and may mean the release has been tampered with. It is strongly recommended that you report this to the Statping developers.$reset\n"
statping_verify_or_quit "> Do you really want to continue?"
fi
}
statping_reset() {
unset -f statping_install statping_reset statping_get_tarball statping_verify_integrity statping_verify_or_quit statping_brew_install getOS getArch
unset -f statping_install statping_reset statping_get_tarball statping_verify_or_quit statping_brew_install getOS getArch
}
statping_brew_install() {
if [[ -z "$(command -v brew --version)" ]]; then
printf "${white}Using Brew to install!$reset\n"
printf "${yellow}---> brew tap hunterlong/statping$reset\n"
brew tap hunterlong/statping
printf "${yellow}---> brew tap statping/statping$reset\n"
brew tap statping/statping
printf "${yellow}---> brew install statping$reset\n"
brew install statping
printf "${green}Brew installation is complete!$reset\n"
@ -104,7 +71,7 @@ statping_install() {
printf "${white}Installing Statping!$reset\n"
getOS
getArch
if [ "$OS" == "osx" ]; then
if [ "$OS" == "darwin" ]; then
statping_brew_install
else
statping_get_tarball $OS $ARCH
@ -136,6 +103,11 @@ getOS() {
DEST=/usr/local/bin
alias ls='ls -G'
;;
'OpenBSD')
OS='openbsd'
DEST=/usr/local/bin
alias ls='ls -G'
;;
'WindowsNT')
OS='windows'
DEST=/usr/local/bin
@ -149,11 +121,11 @@ getOS() {
DEST=/usr/local/bin
;;
'Darwin')
OS='osx'
OS='darwin'
DEST=/usr/local/bin
;;
'SunOS')
OS='solaris'
OS='linux'
DEST=/usr/local/bin
;;
'AIX') ;;
@ -165,9 +137,13 @@ getOS() {
getArch() {
MACHINE_TYPE=`uname -m`
if [ ${MACHINE_TYPE} == 'x86_64' ]; then
ARCH="x64"
ARCH="amd64"
elif [ ${MACHINE_TYPE} == 'arm' ]; then
ARCH="arm"
elif [ ${MACHINE_TYPE} == 'arm64' ] || [ ${MACHINE_TYPE} == 'aarch64' ] || [ ${MACHINE_TYPE} == 'armv8b' ] || [ ${MACHINE_TYPE} == 'armv8l' ] || [ ${MACHINE_TYPE} == 'aarch64_be' ]; then
ARCH="arm64"
else
ARCH="x32"
ARCH="386"
fi
}

View File

@ -1,7 +1,6 @@
package notifiers
import (
"github.com/prometheus/common/log"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/null"
"github.com/statping/statping/types/services"
@ -9,6 +8,8 @@ import (
"time"
)
var log = utils.Log.WithField("type", "notifier")
func InitNotifiers() {
Add(
slacker,

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ var testCheckin = &Checkin{
var testCheckinHits = []*CheckinHit{{
Checkin: 1,
From: "0.0.0.0.0",
From: "0.0.0.0",
CreatedAt: utils.Now().Add(-30 * time.Second),
}, {
Checkin: 2,

View File

@ -32,7 +32,9 @@ func All() []*Checkin {
}
func (c *Checkin) Create() error {
c.ApiKey = utils.RandomString(32)
if c.ApiKey == "" {
c.ApiKey = utils.RandomString(32)
}
q := db.Create(c)
c.Start()

View File

@ -2,12 +2,13 @@ package checkins
import (
"fmt"
"github.com/prometheus/common/log"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/utils"
"time"
)
var log = utils.Log.WithField("type", "checkin")
// RecheckCheckinFailure will check if a Service Checkin has been reported yet
func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) {
between := utils.Now().Sub(utils.Now()).Seconds()

View File

@ -7,11 +7,11 @@ import (
func Samples() error {
checkin1 := &Checkin{
Name: "Example Checkin 1",
Name: "Demo Checkin 1",
ServiceId: 1,
Interval: 300,
GracePeriod: 300,
ApiKey: utils.RandomString(7),
ApiKey: "demoCheckin123",
}
if err := checkin1.Create(); err != nil {
return err

View File

@ -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"`

View File

@ -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() {

View File

@ -2,7 +2,6 @@ package users
import (
"fmt"
"github.com/prometheus/common/log"
"golang.org/x/crypto/bcrypt"
"time"
)

View File

@ -1,12 +1,14 @@
package users
import (
"github.com/prometheus/common/log"
"github.com/statping/statping/database"
"github.com/statping/statping/utils"
)
var db database.Database
var (
db database.Database
log = utils.Log.WithField("type", "user")
)
func SetDB(database database.Database) {
db = database.Model(&User{})

View File

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/fatih/structs"
"github.com/getsentry/sentry-go"
"github.com/prometheus/common/log"
Logger "github.com/sirupsen/logrus"
"github.com/statping/statping/types/null"
"gopkg.in/natefinch/lumberjack.v2"
@ -47,7 +46,7 @@ func SentryInit(v *string, allow bool) {
Environment: goEnv,
Release: version,
}); err != nil {
log.Errorln(err)
Log.Errorln(err)
}
Log.Infoln("Error Reporting initiated, thank you!")
}

View File

@ -1 +1 @@
0.90.25
0.90.26