Merge branch 'master' into feature/tls-renegotiation

pull/482/head
Anže Jenšterle 2020-04-15 14:37:33 +02:00 committed by GitHub
commit 999037bb05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 2470 additions and 896 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
github: hunterlong github: hunterlong
custom: ['https://www.buymeacoffee.com/hunterlong'] patreon: statping
custom: ['https://www.nfoservers.com/donate.pl?force_recipient=1&recipient=info%40socialeck.com', 'https://www.buymeacoffee.com/hunterlong']

View File

@ -20,4 +20,4 @@ A clear and concise description of what you expected to happen.
### Screenshots ### Screenshots
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
[![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com/) [![GitHub release](https://img.shields.io/github/release/hunterlong/statping.svg)](https://github.com/statping/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping) [![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com/) [![GitHub release](https://img.shields.io/github/release/statping/statping.svg)](https://github.com/statping/statping/releases/latest) [![Build Status](https://travis-ci.com/statping/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping)

View File

@ -19,4 +19,4 @@ I'm always frustrated when [...]
### Additional context ### Additional context
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
[![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com/) [![GitHub release](https://img.shields.io/github/release/hunterlong/statping.svg)](https://github.com/statping/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping) [![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com/) [![GitHub release](https://img.shields.io/github/release/statping/statping.svg)](https://github.com/statping/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping)

1
.gitignore vendored
View File

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

View File

@ -10,6 +10,7 @@ before_script:
branches: branches:
only: only:
- master - master
- dev
env: env:
global: global:
- "PATH=$HOME/.local/bin:$PATH" - "PATH=$HOME/.local/bin:$PATH"
@ -17,6 +18,7 @@ env:
- DB_USER=travis - DB_USER=travis
- DB_PASS= - DB_PASS=
- DB_DATABASE=test - DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/statping/statping - STATPING_DIR=$GOPATH/src/github.com/statping/statping
go: 1.14 go: 1.14
go_import_path: github.com/statping/statping go_import_path: github.com/statping/statping
@ -29,7 +31,6 @@ install:
- "make test-deps yarn clean compile install" - "make test-deps yarn clean compile install"
language: go language: go
addons: addons:
chrome: stable
apt: apt:
packages: packages:
- libgconf-2-4 - libgconf-2-4

View File

@ -1,8 +1,17 @@
# 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 # 0.90.25
- Added string response on OnTest for Notifiers - Added string response on OnTest for Notifiers
- Modified UI to show user the response for a Notifier. - Modified UI to show user the response for a Notifier.
- Modified some Notifiers title's - Modified some Notifiers title's
- Added more Cypress e2e testing - Added more Cypress e2e testing
- Modified Incidents form and UX.
- Added /api/services/{id}/uptime_data API endpoint to show online/offline durations as a series for charts.
- Modified index page to automatically refresh Service details on interval
# 0.90.24 # 0.90.24
- Fixed login form from not showing - Fixed login form from not showing

129
Makefile
View File

@ -5,13 +5,15 @@ GOBUILD=go build -a
GOVERSION=1.14.0 GOVERSION=1.14.0
XGO=xgo -go $(GOVERSION) --dest=build XGO=xgo -go $(GOVERSION) --dest=build
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)"
TRVIS_SECRET=lRqWSt5BoekFK6+padJF+b77YkGdispPXEUKNuD7/Hxb7yJMoI8T/n8xZrTHtCZPdjtpy7wIlJCezNoYEZB3l2GnD6Y1QEZEXF7MIxP7hwsB/uSc5/lgdGW0ZLvTBfv6lwI/GjQIklPBW/4xcKJtj4s1YBP7xvqyIb/lDN7TiOqAKF4gqRVVfsxvlkm7j4TiPCXtz17hYQfU8kKBbd+vd3PuZgdWqs//5RwKk3Ld8QR8zoo9xXQVC5NthiyVbHznzczBsHy2cRZZoWxyi7eJM1HrDw8Jn/ivJONIHNv3RgFVn2rAoKu1X8F6FyuvPO0D2hWC62mdO/e0kt4X0mn9/6xlLSKwrHir67UgNVQe3tvlH0xNKh+yNZqR5x9t0V54vNks6Pgbhas5EfLHoWn5cF4kbJzqkXeHjt1msrsqpA3HKbmtwwjJr4Slotfiu22mAhqLSOV+xWV+IxrcNnrEq/Pa+JAzU12Uyxs8swaLJGPRAlWnJwzL9HK5aOpN0sGTuSEsTwj0WxeMMRx25YEq3+LZOgwOy3fvezmeDnKuBZa6MVCoMMpx1CRxMqAOlTGZXHjj+ZPmqDUUBpzAsFSzIdVRgcnDlLy7YRiz3tVWa1G5S07l/VcBN7ZgvCwOWZ0QgOH0MxkoDfhrfoMhNO6MBFDTRKCEl4TroPEhcInmXU8= 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)" } } } }' 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": "$$TAG_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", "go": 1.14, "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 TEST_DIR=$(GOPATH)/src/github.com/statping/statping
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH) 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: up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
@ -27,14 +29,14 @@ lite: clean
reup: down clean compose-build-full up reup: down clean compose-build-full up
test: clean test: clean compile
go test -v -p=4 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./... go test -v -p=1 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
# build all arch's and release Statping # build all arch's and release Statping
release: test-deps release: test-deps
wget -O statping.gpg $(SIGN_URL) wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg gpg --import statping.gpg
make build-all upload_to_s3 make build-all
test-ci: clean compile test-deps test-ci: clean compile test-deps
SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./... SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
@ -55,6 +57,9 @@ test-deps:
go get github.com/mattn/goveralls go get github.com/mattn/goveralls
go get github.com/GeertJohan/go.rice/rice go get github.com/GeertJohan/go.rice/rice
deps:
go get -d -v -t ./...
protoc: protoc:
cd types/proto && protoc --gofast_out=plugins=grpc:. statping.proto cd types/proto && protoc --gofast_out=plugins=grpc:. statping.proto
@ -146,6 +151,34 @@ install-local: build
generate: generate:
cd source && go 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/; \
GO111MODULE="on" 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/; \
GO111MODULE="on" 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 # remove files for a clean compile/build
clean: clean:
rm -rf ./{logs,assets,plugins,*.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,index.html,vendor} 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 "*.out" -type f -delete
find . -name "*.cpu" -type f -delete find . -name "*.cpu" -type f -delete
find . -name "*.mem" -type f -delete find . -name "*.mem" -type f -delete
rm -rf {build,tmp} rm -rf {build,releases,tmp}
print_details: print_details:
@echo \==== Statping Development Instance ==== @echo \==== Statping Development Instance ====
@ -186,76 +219,36 @@ print_details:
@echo \==== Monitoring and IDE ==== @echo \==== Monitoring and IDE ====
@echo \Grafana: http://localhost:3000 \(username: admin, password: admin\) @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 coverage: test-deps
$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS) $(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS)
# build Statping using a travis ci trigger # build Statping using a travis ci trigger
travis-build: travis_s3_creds travis-build:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(TRAVIS_BUILD_CMD) https://api.travis-ci.com/repo/statping%2Fstatping/requests curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(TRAVIS_BUILD_CMD) https://api.travis-ci.com/repo/statping%2Fstatping/requests
curl -H "Content-Type: application/json" --data '{"docker_tag": "latest"}' -X POST $(DOCKER)
download-key: download-key:
wget -O statping.gpg $(SIGN_URL) wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg 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 # build :latest docker tag
docker-build-latest: docker-build-latest:
docker build --build-arg VERSION=${VERSION} -t statping/statping:latest --no-cache -f Dockerfile . docker build --build-arg VERSION=${VERSION} -t statping/statping:latest --no-cache -f Dockerfile .
docker tag statping/statping:latest statping/statping:v${VERSION} 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 # push the :dev docker tag using curl
publish-dev: publish-dev:
curl -H "Content-Type: application/json" --data '{"docker_tag": "dev"}' -X POST $(DOCKER) 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 # update the homebrew application to latest for mac
publish-homebrew: 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 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
@ -276,15 +269,25 @@ sign-all:
valid-sign: valid-sign:
gpg --verify statping.asc gpg --verify statping.asc
# install xgo and pull the xgo docker image
xgo-install: clean
go get github.com/crazy-max/xgo
docker pull crazymax/xgo:${GOVERSION}
sentry-release: sentry-release:
sentry-cli releases new -p backend -p frontend v${VERSION} sentry-cli releases new -p backend -p frontend v${VERSION}
sentry-cli releases set-commits --auto v${VERSION} sentry-cli releases set-commits --auto v${VERSION}
sentry-cli releases finalize v${VERSION} sentry-cli releases finalize v${VERSION}
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft: clean compile build-bin
PWD=$(shell pwd)
snapcraft clean statping -s pull
docker run --rm -v ${PWD}/build/statping-linux-amd64.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=amd64"
snapcraft clean statping -s pull
docker run --rm -v ${PWD}/build/statping-linux-386.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=i386"
snapcraft clean statping -s pull
docker run --rm -v ${PWD}/build/statping-linux-arm64.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=arm64"
snapcraft clean statping -s pull
docker run --rm -v ${PWD}/build/statping-linux-arm.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=armhf"
snapcraft push statping_${VERSION}_amd64.snap --release stable
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 snapcraft build-bin build-win build-all
.SILENT: travis_s3_creds .SILENT: travis_s3_creds

View File

@ -2,6 +2,6 @@
"name": "Statping", "name": "Statping",
"description": "Statping Server Monitoring with Status Page", "description": "Statping Server Monitoring with Status Page",
"repository": "https://github.com/statping/statping", "repository": "https://github.com/statping/statping",
"logo": "https://raw.githubusercontent.com/hunterlong/statping/master/source/tmpl/banner.png", "logo": "https://raw.githubusercontent.com/statping/statping/master/source/tmpl/banner.png",
"keywords": ["statping", "server", "monitoring", "status page","golang", "go"] "keywords": ["statping", "server", "monitoring", "status page","golang", "go"]
} }

View File

@ -349,7 +349,7 @@ func HelpEcho() {
} }
func checkGithubUpdates() (githubResponse, error) { func checkGithubUpdates() (githubResponse, error) {
url := "https://api.github.com/repos/hunterlong/statping/releases/latest" url := "https://api.github.com/repos/statping/statping/releases/latest"
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true) contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true)
if err != nil { if err != nil {
return githubResponse{}, err return githubResponse{}, err

View File

@ -146,6 +146,45 @@ type isObject interface {
Db() Database 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) { func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
fields := parseGet(r) fields := parseGet(r)
grouping := fields.Get("group") grouping := fields.Get("group")
@ -169,6 +208,9 @@ func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
log.Errorln(err) log.Errorln(err)
groupDur = 1 * time.Hour groupDur = 1 * time.Hour
} }
if endField == 0 {
endField = utils.Now().Unix()
}
query := &GroupQuery{ query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(), Start: time.Unix(startField, 0).UTC(),

4
dev/Dockerfile.dev vendored
View File

@ -7,7 +7,7 @@ RUN npm install -g yarn
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \ RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass chmod +x /usr/local/bin/sass
WORKDIR /go/src/github.com/hunterlong/statping WORKDIR /go/src/github.com/statping/statping
ADD go.mod go.sum version.txt ./ ADD go.mod go.sum version.txt ./
@ -22,7 +22,7 @@ ADD frontend/package.json frontend/yarn.lock ./frontend/
RUN cd frontend && yarn install --pure-lockfile --network-timeout 1000000 && yarn cache clean RUN cd frontend && yarn install --pure-lockfile --network-timeout 1000000 && yarn cache clean
ENV IS_DOCKER=true ENV IS_DOCKER=true
ENV STATPING_DIR=/go/src/github.com/hunterlong/statping ENV STATPING_DIR=/go/src/github.com/statping/statping
EXPOSE 8585 EXPOSE 8585
EXPOSE 8888 EXPOSE 8888

View File

@ -21,7 +21,7 @@ services:
# COMMIT: DEV # COMMIT: DEV
# restart: on-failure # restart: on-failure
# volumes: # volumes:
# - ./:/go/src/github.com/hunterlong/statping # - ./:/go/src/github.com/statping/statping
# environment: # environment:
# VIRTUAL_HOST: local.statping.com # VIRTUAL_HOST: local.statping.com
# VIRTUAL_PORT: 8888 # VIRTUAL_PORT: 8888

View File

@ -4,19 +4,19 @@ services:
statping_dev: statping_dev:
container_name: statping_dev container_name: statping_dev
image: hunterlong/statping:dev image: statping/statping:dev
restart: on-failure restart: on-failure
volumes: volumes:
- ./cmd:/go/src/github.com/hunterlong/statping/cmd/ - ./cmd:/go/src/github.com/statping/statping/cmd/
- ./core:/go/src/github.com/hunterlong/statping/core/ - ./core:/go/src/github.com/statping/statping/core/
- ./database:/go/src/github.com/hunterlong/statping/database/ - ./database:/go/src/github.com/statping/statping/database/
- ./dev:/go/src/github.com/hunterlong/statping/dev/ - ./dev:/go/src/github.com/statping/statping/dev/
- ./frontend:/go/src/github.com/hunterlong/statping/frontend/ - ./frontend:/go/src/github.com/statping/statping/frontend/
- ./handlers:/go/src/github.com/hunterlong/statping/handlers/ - ./handlers:/go/src/github.com/statping/statping/handlers/
- ./notifiers:/go/src/github.com/hunterlong/statping/notifiers/ - ./notifiers:/go/src/github.com/statping/statping/notifiers/
- ./source:/go/src/github.com/hunterlong/statping/source/ - ./source:/go/src/github.com/statping/statping/source/
- ./types:/go/src/github.com/hunterlong/statping/types/ - ./types:/go/src/github.com/statping/statping/types/
- ./utils:/go/src/github.com/hunterlong/statping/utils/ - ./utils:/go/src/github.com/statping/statping/utils/
environment: environment:
DB_CONN: sqlite DB_CONN: sqlite
API_KEY: exampleapikey API_KEY: exampleapikey

View File

@ -612,5 +612,5 @@
"title": "Statping", "title": "Statping",
"uid": "6BzRjddmz", "uid": "6BzRjddmz",
"version": 25, "version": 25,
"description": "Monitor your websites and applications using Statping service. View more information at https://github.com/hunterlong/statping" "description": "Monitor your websites and applications using Statping service. View more information at https://github.com/statping/statping"
} }

8
dev/init.sh vendored
View File

@ -12,7 +12,7 @@
cd /home/ubuntu cd /home/ubuntu
source /home/ubuntu/.profile source /home/ubuntu/.profile
sudo rm -rf startup.sh > /dev/null sudo rm -rf startup.sh > /dev/null
sudo curl -o startup.sh -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/hunterlong/statping/master/dev/startup.sh > /dev/null sudo curl -o startup.sh -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/statping/statping/master/dev/startup.sh > /dev/null
sudo chmod +x startup.sh > /dev/null sudo chmod +x startup.sh > /dev/null
sudo rm -f docker-compose.yml > /dev/null sudo rm -f docker-compose.yml > /dev/null
@ -21,7 +21,7 @@ EC_IP=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)
if [ "$LETSENCRYPT_HOST" = "" ] if [ "$LETSENCRYPT_HOST" = "" ]
then then
sudo curl -o docker-compose.yml -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/hunterlong/statping/master/dev/docker-compose-single.yml > /dev/null sudo curl -o docker-compose.yml -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/statping/statping/master/dev/docker-compose-single.yml > /dev/null
else else
printf " \n\n\n\nDomain found for SSL certificate - $LETSENCRYPT_HOST\n" printf " \n\n\n\nDomain found for SSL certificate - $LETSENCRYPT_HOST\n"
printf "================================================================================================================\n" printf "================================================================================================================\n"
@ -33,7 +33,7 @@ else
printf " A $LETSENCRYPT_HOST => $EC_IP (or use A record if you are using an Elastic IP)\n" printf " A $LETSENCRYPT_HOST => $EC_IP (or use A record if you are using an Elastic IP)\n"
printf "================================================================================================================\n\n\n" printf "================================================================================================================\n\n\n"
sudo curl -o docker-compose.yml -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/hunterlong/statping/dev/docker-compose-ssl.yml > /dev/null sudo curl -o docker-compose.yml -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/statping/statping/dev/docker-compose-ssl.yml > /dev/null
fi fi
sudo service docker start > /dev/null sudo service docker start > /dev/null
@ -49,7 +49,7 @@ fi
sudo docker system prune -af > /dev/null sudo docker system prune -af > /dev/null
sudo curl https://raw.githubusercontent.com/hunterlong/statping/dev/init.sh > /home/ubuntu/init.sh sudo curl https://raw.githubusercontent.com/statping/statping/dev/init.sh > /home/ubuntu/init.sh
sudo chmod +x /home/ubuntu/init.sh > /dev/null sudo chmod +x /home/ubuntu/init.sh > /dev/null
printf "\n\n\n\n\n Statping Status Page on EC2\n" printf "\n\n\n\n\n Statping Status Page on EC2\n"

4
dev/install.sh vendored
View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
OS=osx OS=osx
ARCH=x64 ARCH=x64
REPO=github.com/hunterlong/statping REPO=github.com/statping/statping
VERSION=$(curl -s "https://$REPO/releases/latest" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}') VERSION=$(curl -s "https://$REPO/releases/latest" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}')
if [ `getconf LONG_BIT` = "64" ] if [ `getconf LONG_BIT` = "64" ]
then then
@ -24,4 +24,4 @@ curl -L -sS $FILE -o statping.tar.gz && tar xzf statping.tar.gz && rm statping.t
chmod +x statping chmod +x statping
echo "Installing Statping to directory: /usr/local/bin/" echo "Installing Statping to directory: /usr/local/bin/"
mv statping /usr/local/bin/ mv statping /usr/local/bin/
echo "Statping $VERSION has been successfully installed! Try 'statping version' to check it!" echo "Statping $VERSION has been successfully installed! Try 'statping version' to check it!"

4
dev/postman.json vendored
View File

@ -2164,7 +2164,7 @@
"response": [] "response": []
} }
], ],
"description": "Statping contains multiple notifiers that will send you a notification whenever a service become offline, or online. You can create your own 3rd party notifier by reading more on the [Notifiers Wiki](https://github.com/hunterlong/statping/wiki/Notifiers) on the Github repo.", "description": "Statping contains multiple notifiers that will send you a notification whenever a service become offline, or online. You can create your own 3rd party notifier by reading more on the [Notifiers Wiki](https://github.com/statping/statping/wiki/Notifiers) on the Github repo.",
"auth": { "auth": {
"type": "bearer", "type": "bearer",
"bearer": [ "bearer": [
@ -3471,4 +3471,4 @@
} }
], ],
"protocolProfileBehavior": {} "protocolProfileBehavior": {}
} }

View File

@ -1,13 +1,13 @@
https://demo.statping.com/ https://demo.statping.com/
https://github.com/hunterlong/statping https://github.com/statping/statping
https://statping.com https://statping.com
https://hub.docker.com/r/hunterlong/statping/ https://hub.docker.com/r/statping/statping/
https://godoc.org/github.com/hunterlong/statping https://godoc.org/github.com/statping/statping
https://hub.docker.com/r/hunterlong/statping/ https://hub.docker.com/r/statping/statping/
https://goreportcard.com/report/github.com/hunterlong/statping https://goreportcard.com/report/github.com/statping/statping
http://slack.statping.com http://slack.statping.com
https://www.google.com/search?q=do+a+barrel+roll https://www.google.com/search?q=do+a+barrel+roll
https://travis-ci.com/hunterlong/statping https://travis-ci.com/statping/statping
https://vue.statping.com https://vue.statping.com
http://gorm.io http://gorm.io
https://www.youtube.com/watch?v=mLOdar3jshI https://www.youtube.com/watch?v=mLOdar3jshI

4
doc.go
View File

@ -8,7 +8,7 @@
// the zip file from the latest releases link. // the zip file from the latest releases link.
// //
// // MacOS using homebrew // // MacOS using homebrew
// brew tap hunterlong/statping // brew tap statping/statping
// brew install statping // brew install statping
// //
// // Linux installation // // Linux installation
@ -18,7 +18,7 @@
// Docker // Docker
// //
// Statping can be built in many way, the best way is to use Docker! // Statping can be built in many way, the best way is to use Docker!
// docker run -it -p 8080:8080 hunterlong/statping // docker run -it -p 8080:8080 statping/statping
// //
// Enjoy Statping and tell me any issues you might be having on Github. https://github.com/hunterlong // Enjoy Statping and tell me any issues you might be having on Github. https://github.com/hunterlong
package statping package statping

View File

@ -4,7 +4,7 @@ services:
statping: statping:
container_name: statping container_name: statping
image: hunterlong/statping:latest image: statping/statping:latest
restart: always restart: always
volumes: volumes:
- statping_data:/app - statping_data:/app

View File

@ -8,6 +8,7 @@
"DB_PASS": "password123", "DB_PASS": "password123",
"GO_ENV": "production" "GO_ENV": "production"
}, },
"baseUrl": "http://localhost:8080",
"chromeWebSecurity": false, "chromeWebSecurity": false,
"defaultCommandTimeout": 30000, "defaultCommandTimeout": 30000,
"requestTimeout": 30000, "requestTimeout": 30000,

View File

@ -2,7 +2,7 @@
import "../support/commands" import "../support/commands"
context('Annoucements Tests', () => { context('Announcements Tests', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -10,9 +10,9 @@
<base href="{{BasePath}}"> <base href="{{BasePath}}">
{{if USE_CDN}} {{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico"> <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/vendor.css">
<link rel="stylesheet" href="https://assets.statping.com/css/base.css"> <link rel="stylesheet" href="https://assets.statping.com/style.css">
<link rel="stylesheet" href="https://assets.statping.com/font/all.css"> <link rel="stylesheet" href="https://assets.statping.com/main.css">
{{else}} {{else}}
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
{{if USING_ASSETS}} {{if USING_ASSETS}}
@ -33,11 +33,11 @@
<div id="app" class="statping_container"></div> <div id="app" class="statping_container"></div>
{{if USE_CDN}} {{if USE_CDN}}
<script src="https://assets.statping.com/js/bundle.js"></script> <script src="https://assets.statping.com/bundle.js"></script>
<script src="https://assets.statping.com/js/vendor.chunk.js"></script> <script src="https://assets.statping.com/vendor.chunk.js"></script>
<script src="https://assets.statping.com/js/polyfill.chunk.js"></script> <script src="https://assets.statping.com/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/js/style.chunk.js"></script> <script src="https://assets.statping.com/style.chunk.js"></script>
<script src="https://assets.statping.com/js/main.chunk.js"></script> <script src="https://assets.statping.com/main.chunk.js"></script>
{{else}} {{else}}
<% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %> <% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %>
<%= bodyTag %> <% }) %> <%= bodyTag %> <% }) %>

View File

@ -56,6 +56,10 @@ class Api {
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data)) return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).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) { async service_heatmap(id, start, end, group) {
return axios.get('api/services/' + id + '/heatmap').then(response => (response.data)) return axios.get('api/services/' + id + '/heatmap').then(response => (response.data))
} }
@ -118,7 +122,7 @@ class Api {
} }
async incident_updates(incident) { async incident_updates(incident) {
return axios.post('api/incidents/'+incident.id+'/updates', data).then(response => (response.data)) return axios.get('api/incidents/'+incident.id+'/updates').then(response => (response.data))
} }
async incident_update_create(update) { async incident_update_create(update) {
@ -129,12 +133,12 @@ class Api {
return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data)) return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data))
} }
async incidents_service(service) { async incidents_service(id) {
return axios.get('api/services/'+service.id+'/incidents').then(response => (response.data)) return axios.get('api/services/'+id+'/incidents').then(response => (response.data))
} }
async incident_create(service, data) { async incident_create(service_id, data) {
return axios.post('api/services/'+service.id+'/incidents', data).then(response => (response.data)) return axios.post('api/services/'+service_id+'/incidents', data).then(response => (response.data))
} }
async incident_delete(incident) { async incident_delete(incident) {

View File

@ -28,8 +28,8 @@ HTML,BODY {
background-color: white; background-color: white;
margin: 6px; margin: 6px;
height: 26px; height: 26px;
font-size: 10pt; font-size: 8pt;
padding: 3px 7px; padding: 5px 7px;
border: 1px solid #a7a7a7; border: 1px solid #a7a7a7;
border-radius: 4px !important; border-radius: 4px !important;
} }
@ -344,10 +344,33 @@ HTML,BODY {
background-color: #ffbbbb; background-color: #ffbbbb;
} }
.btn-white {
background-color: white;
border: 1px solid #d8d8d8;
color: #767676;
}
.btn-white:hover {
background-color: #fcfcfc;
border: 1px solid #bdbdbd;
color: #767676;
}
.braker {
border-top: 1px solid rgba(0, 0, 0, 0.10);
width: 98%;
display: block;
margin: 0 auto;
}
.text-dim {
color: #a0a0a0;
}
.card { .card {
background-color: $service-background; background-color: $service-background;
border: $service-border; border: $service-border;
//box-shadow: 0px 2px 11px 1px rgba(0, 0, 0, 0.13); box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
} }
.card-body { .card-body {

View File

@ -0,0 +1,105 @@
<template>
<div class="col-12">
<h2>{{service.name}} Checkins</h2>
<p class="mb-3">Tell your service to send a routine HTTP request to a Statping Checkin.</p>
<div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
<span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
<span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
<span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
<span class="d-block mt-2">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
<button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
</span>
</span>
</div>
<div class="col-12 alert alert-light">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import Api from "../../API";
export default {
name: 'Checkins',
data() {
return {
service: {},
ready: false,
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: 0
}
}
},
computed: {
checkins() {
return this.$store.getters.serviceCheckins(this.service.id)
},
core() {
return this.$store.getters.core
},
},
async created() {
if (this.$route.params) {
const id = this.$route.params.id
this.service = await Api.service(id)
this.checkin.service_id = this.service.id
this.ready = true
}
},
methods: {
fixInts() {
const c = this.checkin
this.checkin.interval = parseInt(c.interval)
this.checkin.grace = parseInt(c.grace)
return this.checkin
},
async saveCheckin() {
const c = this.fixInts()
await Api.checkin_create(c)
await this.updateCheckins()
},
async deleteCheckin(checkin) {
await Api.checkin_delete(checkin)
await this.updateCheckins()
},
async updateCheckins() {
const checkins = await Api.checkins()
this.$store.commit('setCheckins', checkins)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="col-12"> <div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4"> <div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Annoucements</div> <div class="card-header">Announcements</div>
<div class="card-body"> <div class="card-body pt-0">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>

View File

@ -3,16 +3,17 @@
<div class="card contain-card text-black-50 bg-white mb-4"> <div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Services <div class="card-header">Services
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-outline-success float-right"> <router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-outline-success float-right">
<font-awesome-icon icon="plus"/> Create <font-awesome-icon icon="plus"/> Create
</router-link></div> </router-link>
<div class="card-body"> </div>
<div class="card-body pt-0">
<ServicesList/> <ServicesList/>
</div> </div>
</div> </div>
<div class="card contain-card text-black-50 bg-white mb-4"> <div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Groups</div> <div class="card-header">Groups</div>
<div class="card-body"> <div class="card-body pt-0">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -35,10 +36,12 @@
</td> </td>
<td class="text-right"> <td class="text-right">
<div v-if="$store.state.admin" class="btn-group"> <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> <button @click.prevent="editGroup(group, edit)" href="#" class="btn btn-sm btn-outline-secondary">
<a @click.prevent="deleteGroup(group)" href="#" class="btn btn-danger"> <font-awesome-icon icon="edit" />
</button>
<button @click.prevent="deleteGroup(group)" href="#" class="btn btn-sm btn-danger">
<font-awesome-icon icon="times" /> <font-awesome-icon icon="times" />
</a> </button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -2,7 +2,7 @@
<div class="col-12"> <div class="col-12">
<div class="card contain-card text-black-50 bg-white mb-4"> <div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Users</div> <div class="card-header">Users</div>
<div class="card-body"> <div class="card-body pt-0">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>

View File

@ -0,0 +1,105 @@
<template>
<div class="col-12">
<h2>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-outline-danger float-right">Delete All</button></h2>
<div class="list-group mt-3 mb-4">
<div class="alert alert-info" v-if="failures.length===0">
You don't have any failures for {{service.name}}. Way to go!
</div>
<div v-for="(failure, index) in failures" :key="index" class="mb-2 list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{failure.issue}}</h5>
<small>{{niceDate(failure.created_at)}}</small>
</div>
<p class="mb-1">{{failure.issue}}</p>
</div>
<nav v-if="total > 4" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{'disabled': page===1}">
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
<li v-for="n in maxPages" class="page-item" :class="{'active': page === n}">
<a @click.prevent="gotoPage(n)" class="page-link" href="#">{{n}}</a>
</li>
<li class="page-item" :class="{'disabled': page===Math.floor(total / limit)}">
<a @click.prevent="gotoPage(page+1)" :disabled="page===Math.floor(total / limit)" class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
<div class="text-center">
<span class="text-black-50">{{total}} Total</span>
</div>
</nav>
</div>
</div>
</template>
<script>
import Api from "../../API";
export default {
name: 'Failures',
data() {
return {
service: {},
failures: [],
limit: 10,
offset: 0,
total: 0,
page: 1
}
},
computed: {
pages() {
return Math.floor(this.total / this.limit)
},
maxPages() {
const p = Math.floor(this.total / this.limit)
if (p > 16) {
return 16
} else {
return p
}
}
},
async created() {
this.service = await Api.service(this.$route.params.id)
this.total = this.service.stats.failures
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;
await this.load()
},
async load() {
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="col-12">
<div v-for="(incident, i) in incidents" class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
</button>
</div>
<FormIncidentUpdates :incident="incident"/>
<span class="font-2 p-2 pl-3">Created: {{niceDate(incident.created_at)}} | Last Update: {{niceDate(incident.updated_at)}}</span>
</div>
<div class="card contain-card text-black-50 bg-white">
<div class="card-header">Create Incident</div>
<div class="card-body">
<form @submit.prevent="createIncident">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input v-model="incident.title" type="text" name="title" class="form-control" id="title" placeholder="Incident Title" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea v-model="incident.description" rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="createIncident"
:disabled="!incident.title || !incident.description"
type="submit" class="btn btn-block btn-primary">
Create Incident
</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</template>
<script>
import Api from "../../API";
import FormIncidentUpdates from "@/forms/IncidentUpdates";
export default {
name: 'Incidents',
components: {FormIncidentUpdates},
data() {
return {
service_id: 0,
ready: false,
incidents: [],
incident: {
title: "",
description: "",
service: 0,
}
}
},
computed:{
theID() {
return this.$route.params.id
}
},
async mounted() {
await this.loadIncidents()
},
methods: {
async getIncidents() {
return await Api.incidents_service(this.theID)
},
async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) {
await Api.incident_delete(incident)
await this.loadIncidents()
}
},
async loadIncidents() {
this.incidents = await Api.incidents_service(this.service_id)
},
async createIncident() {
await Api.incident_create(this.theID, this.incident)
await this.loadIncidents()
this.incident = {
title: "",
description: "",
service: this.service_id,
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -9,7 +9,7 @@
</tr> </tr>
</thead> </thead>
<draggable id="services_list" tag="tbody" v-model="servicesList" handle=".drag_icon"> <draggable id="services_list" tag="tbody" v-model="servicesList" handle=".drag_icon">
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="service.id"> <tr v-for="(service, index) in servicesList" :key="service.id">
<td> <td>
<span v-if="$store.state.admin" class="drag_icon d-none d-md-inline"> <span v-if="$store.state.admin" class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" class="mr-3"/> <font-awesome-icon icon="bars" class="mr-3"/>

View File

@ -18,7 +18,7 @@
<router-link to="/dashboard/users" class="nav-link">Users</router-link> <router-link to="/dashboard/users" class="nav-link">Users</router-link>
</li> </li>
<li @click="navopen = !navopen" class="nav-item navbar-item"> <li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/messages" class="nav-link">Annoucements</router-link> <router-link to="/dashboard/messages" class="nav-link">Announcements</router-link>
</li> </li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item"> <li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">Settings</router-link> <router-link to="/dashboard/settings" class="nav-link">Settings</router-link>

View File

@ -4,8 +4,11 @@
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'mini_error': d.amount > 0, 'mini_success': d.amount === 0}"></div> <div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'mini_error': d.amount > 0, 'mini_success': d.amount === 0}"></div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-6 text-left font-2 text-muted">30 Days Ago</div> <div class="col-4 text-left font-2 text-muted">30 Days Ago</div>
<div class="col-6 text-right font-2 text-muted">Today</div> <div class="col-4 text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}">
{{service_txt}}
</div>
<div class="col-4 text-right font-2 text-muted">Today</div>
</div> </div>
</div> </div>
</template> </template>
@ -28,6 +31,14 @@ export default {
type: Object, type: Object,
required: true required: true
} }
},
computed: {
service_txt() {
if (!this.service.online) {
return `Offline for ${this.ago(this.service.last_success)}`
}
return `${this.service.online_24_hours}% Uptime`
}
}, },
mounted () { mounted () {
this.lastDaysFailures() this.lastDaysFailures()

View File

@ -1,27 +1,29 @@
<template> <template>
<div class="row"> <div class="row">
<div v-for="(incident, i) in incidents" class="col-12 mt-4"> <div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
<h5>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h5> <span class="braker mt-1 mb-3"></span>
{{incident.description}} <h6>Incident: {{incident.title}}
<div class="row"> <span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>
<div v-for="(update, i) in incident.updates.reverse()" v-bind:key="update.id" class="col-12 mt-3"> </h6>
<span class="col-2 badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span> <span class="font-2" v-html="incident.description"></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> <UpdatesBlock :incident="incident"/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Api from '../../API'; import Api from '../../API';
import UpdatesBlock from "@/components/Index/UpdatesBlock";
export default { export default {
name: 'IncidentsBlock', name: 'IncidentsBlock',
props: { components: {UpdatesBlock},
props: {
service: { service: {
type: Object type: Object,
required: true
} }
}, },
data() { data() {
@ -43,9 +45,13 @@ export default {
return "badge-danger" return "badge-danger"
} }
}, },
async getIncidents() { async getIncidents() {
this.incidents = await Api.incidents_service(this.service) this.incidents = await Api.incidents_service(this.service.id)
} },
async incident_updates(incident) {
await Api.incident_updates(incident).then((d) => {return d})
return o
}
} }
} }
</script> </script>

View File

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

View File

@ -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>
<div class="col-md-4 col-6 float-right"> <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="setService" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
<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> View Service
</div> </button>
<div v-if="expanded" class="row">
<Analytics title="Last Failure" value="417 Days ago"/>
</div> </div>
</div> </div>
@ -63,6 +60,7 @@
</template> </template>
<script> <script>
import Api from '../../API';
import Analytics from './Analytics'; import Analytics from './Analytics';
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats"; import ServiceTopStats from "@/components/Service/ServiceTopStats";
@ -72,13 +70,14 @@ export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: { Analytics, ServiceTopStats, ServiceChart}, components: { Analytics, ServiceTopStats, ServiceChart},
props: { props: {
service: { in_service: {
type: Object, type: Object,
required: true required: true
}, },
}, },
data() { data() {
return { return {
timer_func: null,
expanded: false, expanded: false,
visible: false, visible: false,
dropDownMenu: false, dropDownMenu: false,
@ -115,10 +114,26 @@ export default {
subtitle: "Last 7 Days", subtitle: "Last 7 Days",
value: 0, value: 0,
} }
} },
track_service: null,
} }
}, },
beforeDestroy() {
clearInterval(this.timer_func)
},
computed: {
service() {
return this.track_service
}
},
async created() {
this.track_service = this.in_service
},
methods: { methods: {
async setService() {
await this.$store.commit('setService', this.service)
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}})
},
async showMoreStats() { async showMoreStats() {
this.expanded = !this.expanded; this.expanded = !this.expanded;
@ -142,6 +157,7 @@ export default {
this.stats.low_ping.value = this.humanTime(pingData.low); this.stats.low_ping.value = this.humanTime(pingData.low);
}, },
smallText(s) { smallText(s) {
const incidents = s.incidents
if (s.online) { if (s.online) {
return `Online, last checked ${this.ago(s.last_success)}` return `Online, last checked ${this.ago(s.last_success)}`
} else { } else {
@ -155,6 +171,12 @@ export default {
visibleChart(isVisible, entry) { visibleChart(isVisible, entry) {
if (isVisible && !this.visible) { if (isVisible && !this.visible) {
this.visible = true this.visible = true
if (!this.timer_func) {
this.timer_func = setInterval(async () => {
this.track_service = await Api.service(this.service.id)
}, this.track_service.check_interval * 1000)
}
} }
} }
} }

View File

@ -9,10 +9,48 @@
</h4> </h4>
</div> </div>
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1"> <div class="card-body p-3 p-md-1 pt-md-1 pb-md-1">
<transition name="fade"> <transition name="fade">
<div v-if="loaded && service.online" class="col-12 pb-2"> <div v-if="loaded" class="col-12 pb-2">
<div v-if="false" class="row mb-4 align-content-center">
<div v-if="!service.online" class="col-3 text-left">
<span class="text-danger font-5 font-weight-bold">okko</span>
<span class="font-2 d-block">Current Downtime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{service.online_24_hours.toString()}}%
</span>
<span class="font-2 d-block">Total Uptime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
0
</span>
<span class="font-2 d-block">Downtime Today</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{(uptime.uptime / 10000).toFixed(0).toString()}}
</span>
<span class="font-2 d-block">Uptime Duration</span>
</div>
<div class="col-3 text-left">
<span class="text-danger font-5 font-weight-bold">
{{service.failures_24_hours}}
</span>
<span class="font-2 d-block">Failures last 24 hours</span>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3"> <div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/> <ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
@ -22,33 +60,6 @@
</div> </div>
</div> </div>
<div v-if="false" class="row mt-4 pt-1 mb-3 align-content-center">
<StatsGen :service="service"
title="Since Yesterday"
:start="this.toUnix(this.nowSubtract(86400 * 2))"
:end="this.toUnix(this.nowSubtract(86400))"
group="24h" expression="latencyPercent"/>
<StatsGen :service="service"
title="7 Day Change"
:start="this.toUnix(this.nowSubtract(86400 * 7))"
:end="this.toUnix(this.now())"
group="24h" expression="latencyPercent"/>
<StatsGen :service="service"
title="Max Latency"
:start="this.toUnix(this.nowSubtract(86400 * 2))"
:end="this.toUnix(this.nowSubtract(86400))"
group="24h" expression="latencyPercent"/>
<StatsGen :service="service"
title="Uptime"
:start="this.toUnix(this.nowSubtract(86400 * 2))"
:end="this.toUnix(this.nowSubtract(86400))"
group="24h" expression="latencyPercent"/>
</div>
</div> </div>
</transition> </transition>
</div> </div>
@ -56,32 +67,23 @@
<div class="card-footer"> <div class="card-footer">
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-12 col-md-3 mb-2 mb-md-0">
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary incident" :class="{'text-white btn-secondary': openTab==='incident'}" >Incidents</button> <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>
<div class="col-3"> <div class="col-12 col-md-3 mb-2 mb-md-0">
<button @click.prevent="Tab('checkin')" class="btn btn-block btn-outline-secondary checkin" :class="{'text-white btn-secondary': openTab==='checkin'}" >Checkins</button> <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>
<div class="col-3"> <div class="col-12 col-md-3 mb-2 mb-md-0">
<button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary failures" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}"> <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></button> Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
</router-link>
</div> </div>
<div class="col-3 pt-2"> <div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span> <span class="text-black-50 float-md-right">{{service.online_7_days}}% Uptime</span>
</div>
<div v-if="openTab === 'incident'" class="col-12 mt-4">
<FormIncident :service="service" />
</div>
<div v-if="openTab === 'checkin'" class="col-12 mt-4">
<Checkin :service="service" />
</div>
<div v-if="openTab === 'failures'" class="col-12 mt-4">
<button @click.prevent="deleteFailures" class="btn btn-block btn-outline-secondary delete_failures" :disabled="service.stats.failures === 0">Delete Failures</button>
<ServiceFailures :service="service"/>
</div> </div>
</div> </div>
@ -122,6 +124,7 @@
}, },
data() { data() {
return { return {
uptime: null,
openTab: "", openTab: "",
set1: [], set1: [],
set2: [], set2: [],
@ -139,22 +142,19 @@
async setVisible(isVisible, entry) { async setVisible(isVisible, entry) {
if (isVisible && !this.visible) { if (isVisible && !this.visible) {
await this.loadInfo() await this.loadInfo()
await this.getUptime()
this.visible = true this.visible = true
} }
}, },
async getUptime() {
this.uptime = await Api.service_uptime(this.service.id)
},
async loadInfo() { async loadInfo() {
this.set1 = await this.getHits(24 * 7, "6h") this.set1 = await this.getHits(24 * 7, "6h")
this.set1_name = this.calc(this.set1) this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24, "1h") this.set2 = await this.getHits(24, "1h")
this.set2_name = this.calc(this.set2) this.set2_name = this.calc(this.set2)
this.loaded = true 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) { Tab(name) {
if (this.openTab === name) { if (this.openTab === name) {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="col-3 text-center"> <div class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">{{value}}</span> <span class="text-success font-5 font-weight-bold">{{value}}</span>
<span class="font-2 d-block">{{title}}</span> <span class="font-2 d-block">{{title}}</span>
</div> </div>
@ -34,6 +34,9 @@
expression: { expression: {
type: String, type: String,
required: true required: true
},
in_value: {
required: false
} }
}, },
data() { data() {
@ -42,6 +45,9 @@
} }
}, },
async mounted() { async mounted() {
if (this.in_value) {
this.value = this.in_value
}
await this.latencyYesterday(); await this.latencyYesterday();
}, },
methods: { methods: {

View File

@ -1,61 +1,6 @@
<template> <template>
<div> <div>
<div v-for="(incident, i) in incidents" class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
</button>
</div>
<div class="card-body bg-light pt-3">
<div v-for="(update, i) in incident.updates" class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</span>
</div>
<FormIncidentUpdates :incident="incident"/>
<span class="font-2 mt-3">Created: {{niceDate(incident.created_at)}} | Last Update: {{niceDate(incident.updated_at)}}</span>
</div>
</div>
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">Create Incident for {{service.name}}</div>
<div class="card-body">
<form @submit.prevent="createIncident">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input v-model="incident.title" type="text" name="title" class="form-control" id="title" placeholder="Incident Title" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea v-model="incident.description" rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="createIncident"
:disabled="!incident.title || !incident.description"
type="submit" class="btn btn-block btn-primary">
Create Incident
</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div> </div>
</template> </template>
@ -89,29 +34,7 @@
await this.loadIncidents() await this.loadIncidents()
}, },
methods: { methods: {
async delete_update(update) {
await Api.incident_update_delete(update)
this.incidents = await Api.incidents_service(this.service)
},
async loadIncidents() {
this.incidents = await Api.incidents_service(this.service)
},
async createIncident() {
await Api.incident_create(this.service, this.incident)
await this.loadIncidents()
this.incident = {
title: "",
description: "",
service: this.service.id,
}
},
async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) {
await Api.incident_delete(incident)
await this.loadIncidents()
}
}
} }
} }
</script> </script>

View File

@ -1,6 +1,22 @@
<template> <template>
<form class="row" @submit.prevent="createIncidentUpdate"> <div class="card-body bg-light pt-3">
<div v-if="updates.length===0" class="alert alert-link text-danger">
No updates found, create a new Incident Update below.
</div>
<div v-for="(update, i) in updates">
<div class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
</div>
</div>
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-3"> <div class="col-3">
<select v-model="incident_update.type" class="form-control"> <select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option> <option value="Investigating">Investigating</option>
@ -21,6 +37,7 @@
</button> </button>
</div> </div>
</form> </form>
</div>
</template> </template>
<script> <script>
@ -35,12 +52,13 @@
}, },
props: { props: {
incident: { incident: {
type: Object type: Object,
required: true
} }
}, },
data () { data () {
return { return {
updates: [], updates: this.incident.updates,
incident_update: { incident_update: {
incident: this.incident.id, incident: this.incident.id,
message: "", message: "",
@ -48,16 +66,23 @@
} }
} }
}, },
async mounted () { beforeRouteUpdate (to, from, next) {
await this.loadUpdates()
},
async mounted() {
await this.loadUpdates()
},
methods: {
async delete_update(update) {
await Api.incident_update_delete(update)
await this.loadUpdates()
}, },
methods: {
async loadUpdates() {
this.updates = await Api.incident_updates(this.incident)
},
async createIncidentUpdate() { async createIncidentUpdate() {
await Api.incident_update_create(this.incident_update) await Api.incident_update_create(this.incident_update)
await this.loadUpdates() await this.loadUpdates()
},
async loadUpdates() {
this.updates = await Api.incident_updates(this.incident)
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="card contain-card text-black-50 bg-white mb-5"> <div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Annoucement"}} <div class="card-header">{{message.id ? `Update ${message.title}` : "Create Announcement"}}
<transition name="slide-fade"> <transition name="slide-fade">
<button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button> <button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button>

View File

@ -156,10 +156,10 @@
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Notifications</label> <label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left"> <span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications"> <input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
<label for="switch-notifications">Allow notifications to be sent for this service</label> <label for="switch-notifications">Allow notifications to be sent for this service</label>
</span> </span>
</div> </div>
</div> </div>
<div v-if="service.allow_notifications" class="form-group row"> <div v-if="service.allow_notifications" class="form-group row">

View File

@ -11,7 +11,7 @@
<div class="form-group"> <div class="form-group">
<label>Database Connection</label> <label>Database Connection</label>
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control"> <select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
<option value="sqlite">Sqlite</option> <option value="sqlite">SQLite</option>
<option value="postgres">Postgres</option> <option value="postgres">Postgres</option>
<option value="mysql">MySQL</option> <option value="mysql">MySQL</option>
</select> </select>

View File

@ -42,6 +42,9 @@ export default Vue.mixin({
dur(t1, t2) { dur(t1, t2) {
return formatDistance(t1, t2) return formatDistance(t1, t2)
}, },
format(val, type="EEEE, MMM do h:mma") {
return format(val, type)
},
niceDate(val) { niceDate(val) {
return format(parseISO(val), "EEEE, MMM do h:mma") return format(parseISO(val), "EEEE, MMM do h:mma")
}, },
@ -119,6 +122,13 @@ export default Vue.mixin({
return "bars" 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) { convertToChartData(data = [], multiplier=1, asInt=false) {
if (!data) { if (!data) {
return {data: []} return {data: []}

View File

@ -26,7 +26,7 @@
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
<div v-for="(service, index) in services" :ref="service.id" v-bind:key="index"> <div v-for="(service, index) in services" :ref="service.id" v-bind:key="index">
<ServiceBlock :service=service /> <ServiceBlock :in_service=service />
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="service" class="container col-md-7 col-sm-12 mt-md-5 bg-light"> <div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-12 mb-4"> <div class="col-12 mb-4">
@ -8,7 +8,7 @@
</span> </span>
<h4 class="mt-2"> <h4 class="mt-2">
<router-link to="/" class="text-black-50 text-decoration-none">{{$store.getters.core.name}}</router-link> - <span class="text-muted">{{service.name}}</span> <router-link to="/" class="text-black-50 text-decoration-none">{{core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
@ -21,64 +21,42 @@
</div> </div>
<div class="row mt-5 mb-4"> <div class="row mt-5 mb-4">
<span class="col-6 font-2"> <div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
<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 /> <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 />
</span> <small class="d-block">From {{this.format(new Date(start_time))}}</small>
<span class="col-6 font-2"> </div>
<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 /> <div class="col-12 col-md-5 font-2 mb-3 mb-md-0">
</span> <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>
<div v-if="series" class="service-chart-container"> <AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
<div v-if="!loading" class="col-12">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div> </div>
<div class="service-chart-heatmap mt-5 mb-4"> <div class="service-chart-heatmap mt-5 mb-4">
<ServiceHeatmap :service="service"/> <ServiceHeatmap :service="service"/>
</div> </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>
</div> </div>
@ -94,6 +72,7 @@
import store from '../store' import store from '../store'
import flatPickr from 'vue-flatpickr-component'; import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css'; 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 timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = { const axisOptions = {
@ -123,6 +102,7 @@
export default { export default {
name: 'Service', name: 'Service',
components: { components: {
AdvancedChart,
ServiceTopStats, ServiceTopStats,
ServiceHeatmap, ServiceHeatmap,
ServiceFailures, ServiceFailures,
@ -132,15 +112,19 @@ export default {
}, },
data() { data() {
return { return {
id: this.$route.params.id,
tab: "failures", tab: "failures",
authenticated: false, authenticated: false,
ready: true, ready: true,
group: "1h",
data: null, data: null,
uptime_data: null,
loading: true,
messages: [], messages: [],
failures: [], failures: [],
start_time: this.nowSubtract(84600 * 30), start_time: this.nowSubtract(84600 * 30),
end_time: new Date(), end_time: this.nowSubtract(0),
timedata: null,
load_timedata: false,
dailyRangeOpts: { dailyRangeOpts: {
chart: { chart: {
height: 500, height: 500,
@ -148,13 +132,79 @@ export default {
type: "area", type: "area",
} }
}, },
timeRangeOptions: {
chart: {
id: 'uptime',
height: 120,
type: 'rangeBar',
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
selection: {
enabled: true
},
zoom: {
enabled: true
},
plotOptions: {
bar: {
horizontal: true,
distributed: true,
dataLabels: {
hideOverflowingLabels: false
}
}
},
dataLabels: {
enabled: false
},
tooltip: {
enabled: false,
},
xaxis: {
type: 'datetime'
},
yaxis: {
show: false
},
grid: {
row: {
colors: ['#f3f4f5', '#fff'],
opacity: 1
}
}
},
chartOptions: { chartOptions: {
noData: {
text: "Loading...",
align: 'center',
verticalAlign: 'middle',
offsetX: 0,
offsetY: -20,
style: {
color: "#bababa",
fontSize: '27px'
}
},
chart: { chart: {
id: 'mainchart',
events: { 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 start = (xaxis.min / 1000).toFixed(0)
const end = (xaxis.max / 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 { return {
xaxis: { xaxis: {
min: this.fromUnix(start), min: this.fromUnix(start),
@ -162,6 +212,9 @@ export default {
} }
} }
}, },
scrolled: (chartContext, { xaxis }) => {
window.console.log(xaxis)
},
}, },
height: 500, height: 500,
width: "100%", width: "100%",
@ -187,20 +240,28 @@ export default {
lineCap: 'butt', lineCap: 'butt',
}, },
}, },
xaxis: { xaxis: {
type: "datetime", type: "datetime",
labels: { labels: {
show: true show: true
},
tooltip: {
enabled: true
}
}, },
yaxis: { tooltip: {
labels: { enabled: false
show: true }
}, },
yaxis: {
labels: {
show: true
}, },
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: { tooltip: {
theme: false, theme: false,
enabled: true, enabled: true,
@ -213,7 +274,7 @@ export default {
} else { } else {
val = (val * 0.01).toFixed(0) + " microseconds" 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: { fixed: {
enabled: true, enabled: true,
@ -222,9 +283,8 @@ export default {
offsetY: 40, offsetY: 40,
}, },
x: { x: {
show: false, show: true,
format: 'dd MMM',
formatter: undefined,
}, },
y: { y: {
formatter: undefined, formatter: undefined,
@ -268,64 +328,109 @@ export default {
} }
}, },
computed: { computed: {
service () { service () {
return this.$store.getters.serviceByAll(this.id) return this.$store.getters.serviceByAll(this.id)
} },
core () {
return this.$store.getters.core
},
params () {
return {start: this.toUnix(new Date(this.start_time)), end: this.toUnix(new Date(this.end_time))}
},
id () {
return this.$route.params.id;
},
uptimeSeries () {
return this.timedata.series
},
mainChart () {
return [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
}
}, },
watch: { watch: {
service: function(n, o) { service: function(n, o) {
this.chartHits() this.onnn()
} },
}, load_timedata: function(n, o) {
created() { this.onnn()
}
},
mounted() {
}, },
async mounted() {
if (!this.$store.getters.service) {
const s = await Api.service(this.id)
this.$store.commit('setService', s)
}
},
methods: { methods: {
async get() { async updated_chart(start, end) {
const s = store.getters.serviceByAll(this.id) this.start_time = start
window.console.log("service: ", s) this.end_time = end
this.getService(this.service) this.loading = false
this.messages = this.$store.getters.serviceMessages(this.service.id) },
}, async onnn() {
this.loading = true
await this.chartHits()
await this.fetchUptime()
this.loading = false
},
async fetchUptime() {
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) { messageInRange(message) {
const start = this.isBetween(new Date(), message.start_on) const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date()) const end = this.isBetween(message.end_on, new Date())
return start && end return start && end
}, },
async getService(s) { async getService() {
await this.chartHits() await this.chartHits()
await this.serviceFailures() await this.serviceFailures()
}, },
async serviceFailures() { async serviceFailures() {
let tt = this.startEndTimes() this.failures = await Api.service_failures(this.service.id, this.params.start, this.params.end)
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
}, },
async chartHits(start=0, end=99999999999, group="30m") { async chartHits(start=0, end=99999999999) {
let tt = {}; this.data = await Api.service_hits(this.service.id, this.params.start, this.params.end, this.group, false)
if (start === 0) { if (this.data.length === 0 && this.group !== "1h") {
tt = this.startEndTimes() this.group = "1h"
} 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") {
await this.chartHits("1h") await this.chartHits("1h")
} }
this.series = [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
this.ready = true 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,6 +13,9 @@ import VueRouter from "vue-router";
import Setup from "./forms/Setup"; import Setup from "./forms/Setup";
import Api from "./API"; import Api from "./API";
import Incidents from "@/components/Dashboard/Incidents";
import Checkins from "@/components/Dashboard/Checkins";
import Failures from "@/components/Dashboard/Failures";
const routes = [ const routes = [
{ {
@ -59,9 +62,27 @@ const routes = [
},{ },{
path: 'edit_service/:id', path: 'edit_service/:id',
component: EditService, component: EditService,
meta: { meta: {
requiresAuth: true requiresAuth: true
} }
},{
path: 'service/:id/incidents',
component: Incidents,
meta: {
requiresAuth: true
}
},{
path: 'service/:id/checkins',
component: Checkins,
meta: {
requiresAuth: true
}
},{
path: 'service/:id/failures',
component: Failures,
meta: {
requiresAuth: true
}
},{ },{
path: 'messages', path: 'messages',
component: DashboardMessages, component: DashboardMessages,

View File

@ -22,6 +22,7 @@ export default new Vuex.Store({
core: {}, core: {},
token: null, token: null,
services: [], services: [],
service: null,
groups: [], groups: [],
messages: [], messages: [],
users: [], users: [],
@ -36,6 +37,7 @@ export default new Vuex.Store({
core: state => state.core, core: state => state.core,
token: state => state.token, token: state => state.token,
services: state => state.services, services: state => state.services,
service: state => state.service,
groups: state => state.groups, groups: state => state.groups,
messages: state => state.messages, messages: state => state.messages,
incidents: state => state.incidents, incidents: state => state.incidents,
@ -104,6 +106,9 @@ export default new Vuex.Store({
setToken (state, token) { setToken (state, token) {
state.token = token state.token = token
}, },
setService (state, service) {
state.service = service
},
setServices (state, services) { setServices (state, services) {
state.services = services state.services = services
}, },

1
go.mod
View File

@ -1,5 +1,6 @@
module github.com/statping/statping module github.com/statping/statping
// +heroku goVersion go1.14
go 1.14 go 1.14
require ( require (

View File

@ -76,9 +76,6 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
if c.Domain != app.Domain { if c.Domain != app.Domain {
app.Domain = c.Domain app.Domain = c.Domain
} }
if c.Timezone != app.Timezone {
app.Timezone = c.Timezone
}
app.OAuth = c.OAuth app.OAuth = c.OAuth
app.UseCdn = null.NewNullBool(c.UseCdn.Bool) app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
app.AllowReports = null.NewNullBool(c.AllowReports.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 //Get a cached content by key
func (s Storage) Get(key string) []byte { func (s Storage) Get(key string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
item := s.items[key] item := s.items[key]
if item.Expired() { if item.Expired() {
CacheStorage.Delete(key) CacheStorage.Delete(key)

View File

@ -111,6 +111,7 @@ func Router() *mux.Router {
api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET") api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET")
api.Handle("/api/services/{id}/failure_data", cached("30s", "application/json", apiServiceFailureDataHandler)).Methods("GET") api.Handle("/api/services/{id}/failure_data", cached("30s", "application/json", apiServiceFailureDataHandler)).Methods("GET")
api.Handle("/api/services/{id}/ping_data", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET") api.Handle("/api/services/{id}/ping_data", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET")
api.Handle("/api/services/{id}/uptime_data", http.HandlerFunc(apiServiceTimeDataHandler)).Methods("GET")
//api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET") //api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
// API INCIDENTS Routes // API INCIDENTS Routes
@ -120,7 +121,7 @@ func Router() *mux.Router {
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE") api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
// API INCIDENTS UPDATES Routes // API INCIDENTS UPDATES Routes
api.Handle("/api/incidents/{id}/updates", authenticated(apiIncidentUpdatesHandler, false)).Methods("GET") api.Handle("/api/incidents/{id}/updates", http.HandlerFunc(apiIncidentUpdatesHandler)).Methods("GET")
api.Handle("/api/incidents/{id}/updates", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST") api.Handle("/api/incidents/{id}/updates", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST")
api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE") api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE")

View File

@ -176,6 +176,46 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
returnJson(objs, w, r) returnJson(objs, w, r)
} }
func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupHits, err := database.ParseQueries(r, service.AllHits())
if err != nil {
sendErrorJson(err, w, r)
return
}
groupFailures, err := database.ParseQueries(r, service.AllFailures())
if err != nil {
sendErrorJson(err, w, r)
return
}
var allFailures []*failures.Failure
var allHits []*hits.Hit
if err := groupHits.Find(&allHits); err != nil {
sendErrorJson(err, w, r)
return
}
if err := groupFailures.Find(&allFailures); err != nil {
sendErrorJson(err, w, r)
return
}
uptimeData, err := service.UptimeData(allHits, allFailures)
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(uptimeData, w, r)
}
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) { func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r) service, err := serviceByID(r)
if err != nil { if err != nil {

View File

@ -32,7 +32,6 @@ statping_get_tarball() {
else else
tar xzf $tarball_tmp -C "$temp" tar xzf $tarball_tmp -C "$temp"
fi fi
statping_verify_integrity "$temp"/statping
printf "$green> Installing to $DEST/statping\n" printf "$green> Installing to $DEST/statping\n"
mv "$temp"/statping "$DEST" mv "$temp"/statping "$DEST"
newversion=`$DEST/statping version` newversion=`$DEST/statping version`
@ -50,47 +49,15 @@ statping_get_tarball() {
fi 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() { 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() { statping_brew_install() {
if [[ -z "$(command -v brew --version)" ]]; then if [[ -z "$(command -v brew --version)" ]]; then
printf "${white}Using Brew to install!$reset\n" printf "${white}Using Brew to install!$reset\n"
printf "${yellow}---> brew tap hunterlong/statping$reset\n" printf "${yellow}---> brew tap statping/statping$reset\n"
brew tap hunterlong/statping brew tap statping/statping
printf "${yellow}---> brew install statping$reset\n" printf "${yellow}---> brew install statping$reset\n"
brew install statping brew install statping
printf "${green}Brew installation is complete!$reset\n" printf "${green}Brew installation is complete!$reset\n"
@ -104,7 +71,7 @@ statping_install() {
printf "${white}Installing Statping!$reset\n" printf "${white}Installing Statping!$reset\n"
getOS getOS
getArch getArch
if [ "$OS" == "osx" ]; then if [ "$OS" == "darwin" ]; then
statping_brew_install statping_brew_install
else else
statping_get_tarball $OS $ARCH statping_get_tarball $OS $ARCH
@ -136,6 +103,11 @@ getOS() {
DEST=/usr/local/bin DEST=/usr/local/bin
alias ls='ls -G' alias ls='ls -G'
;; ;;
'OpenBSD')
OS='openbsd'
DEST=/usr/local/bin
alias ls='ls -G'
;;
'WindowsNT') 'WindowsNT')
OS='windows' OS='windows'
DEST=/usr/local/bin DEST=/usr/local/bin
@ -149,11 +121,11 @@ getOS() {
DEST=/usr/local/bin DEST=/usr/local/bin
;; ;;
'Darwin') 'Darwin')
OS='osx' OS='darwin'
DEST=/usr/local/bin DEST=/usr/local/bin
;; ;;
'SunOS') 'SunOS')
OS='solaris' OS='linux'
DEST=/usr/local/bin DEST=/usr/local/bin
;; ;;
'AIX') ;; 'AIX') ;;
@ -165,9 +137,13 @@ getOS() {
getArch() { getArch() {
MACHINE_TYPE=`uname -m` MACHINE_TYPE=`uname -m`
if [ ${MACHINE_TYPE} == 'x86_64' ]; then 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 else
ARCH="x32" ARCH="386"
fi fi
} }

View File

@ -11,6 +11,7 @@ import (
) )
func TestCommandNotifier(t *testing.T) { func TestCommandNotifier(t *testing.T) {
t.SkipNow()
db, err := database.OpenTester() db, err := database.OpenTester()
require.Nil(t, err) require.Nil(t, err)
db.AutoMigrate(&notifications.Notification{}) db.AutoMigrate(&notifications.Notification{})
@ -45,7 +46,7 @@ func TestCommandNotifier(t *testing.T) {
}) })
t.Run("Command Test", func(t *testing.T) { t.Run("Command Test", func(t *testing.T) {
err := Command.OnTest() _, err := Command.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

@ -57,7 +57,7 @@ func TestDiscordNotifier(t *testing.T) {
}) })
t.Run("discord Test", func(t *testing.T) { t.Run("discord Test", func(t *testing.T) {
err := Discorder.OnTest() _, err := Discorder.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

@ -92,7 +92,7 @@ func TestEmailNotifier(t *testing.T) {
}) })
t.Run("email Test", func(t *testing.T) { t.Run("email Test", func(t *testing.T) {
err := email.OnTest() _, err := email.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

@ -65,7 +65,7 @@ func TestMobileNotifier(t *testing.T) {
t.Run("Mobile Test", func(t *testing.T) { t.Run("Mobile Test", func(t *testing.T) {
t.SkipNow() t.SkipNow()
err := Mobile.OnTest() _, err := Mobile.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

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

View File

@ -1,85 +0,0 @@
package notifiers
import (
"github.com/statping/statping/types/services"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestAllNotifiers(t *testing.T) {
notifiers := []notifierTest{
{
Notifier: Command,
RequiredENV: nil,
},
{
Notifier: Discorder,
RequiredENV: []string{"DISCORD_URL"},
},
{
Notifier: email,
RequiredENV: []string{"EMAIL_HOST", "EMAIL_USER", "EMAIL_PASS", "EMAIL_OUTGOING", "EMAIL_SEND_TO", "EMAIL_PORT"},
},
{
Notifier: Mobile,
RequiredENV: []string{"MOBILE_ID", "MOBILE_NUMBER"},
},
{
Notifier: Pushover,
RequiredENV: []string{"PUSHOVER_TOKEN", "PUSHOVER_API"},
},
{
Notifier: slacker,
RequiredENV: []string{"SLACK_URL"},
},
{
Notifier: Telegram,
RequiredENV: []string{"TELEGRAM_TOKEN", "TELEGRAM_CHANNEL"},
},
{
Notifier: Twilio,
RequiredENV: []string{"TWILIO_SID", "TWILIO_SECRET", "TWILIO_FROM", "TWILIO_TO"},
},
{
Notifier: Webhook,
RequiredENV: nil,
},
}
for _, n := range notifiers {
if !getEnvs(n.RequiredENV) {
t.Skip()
continue
}
Add(n.Notifier)
err := n.Notifier.OnSuccess(exampleService)
assert.Nil(t, err)
err = n.Notifier.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
err = n.Notifier.OnTest()
assert.Nil(t, err)
}
}
func getEnvs(env []string) bool {
for _, v := range env {
if os.Getenv(v) == "" {
return false
}
}
return true
}
type notifierTest struct {
Notifier services.ServiceNotifier
RequiredENV []string
}

View File

@ -54,7 +54,7 @@ func TestPushoverNotifier(t *testing.T) {
}) })
t.Run("Pushover Test", func(t *testing.T) { t.Run("Pushover Test", func(t *testing.T) {
err := Pushover.OnTest() _, err := Pushover.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

@ -64,7 +64,7 @@ func TestTelegramNotifier(t *testing.T) {
}) })
t.Run("Telegram Test", func(t *testing.T) { t.Run("Telegram Test", func(t *testing.T) {
err := Telegram.OnTest() _, err := Telegram.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

View File

@ -68,7 +68,7 @@ func TestTwilioNotifier(t *testing.T) {
}) })
t.Run("Twilio Test", func(t *testing.T) { t.Run("Twilio Test", func(t *testing.T) {
err := Twilio.OnTest() _, err := Twilio.OnTest()
assert.Nil(t, err) assert.Nil(t, err)
}) })

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,6 @@ type Core struct {
Setup bool `gorm:"-" json:"setup"` Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn null.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,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"` LoggedIn bool `gorm:"-" json:"logged_in"`
IsAdmin bool `gorm:"-" json:"admin"` IsAdmin bool `gorm:"-" json:"admin"`
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"` AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"`

View File

@ -1 +0,0 @@
package types

View File

@ -3,11 +3,15 @@ package services
import ( import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"github.com/statping/statping/types" "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/types/null"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"net/url" "net/url"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -19,6 +23,145 @@ func (s *Service) Duration() time.Duration {
return time.Duration(s.Interval) * time.Second 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 // Start will create a channel for the service checking go routine
func (s *Service) Start() { func (s *Service) Start() {
if s.IsRunning() { if s.IsRunning() {

View File

@ -26,7 +26,7 @@ type Service struct {
Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"` Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"`
Expected null.NullString `gorm:"column:expected" json:"expected" scope:"user,admin"` Expected null.NullString `gorm:"column:expected" json:"expected" scope:"user,admin"`
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status" scope:"user,admin"` ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status" scope:"user,admin"`
Interval int `gorm:"default:30;column:check_interval" json:"check_interval" scope:"user,admin"` Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
Type string `gorm:"column:check_type" json:"type" scope:"user,admin"` Type string `gorm:"column:check_type" json:"type" scope:"user,admin"`
Method string `gorm:"column:method" json:"method" scope:"user,admin"` Method string `gorm:"column:method" json:"method" scope:"user,admin"`
PostData null.NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"` PostData null.NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"`

View File

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

View File

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

View File

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

View File

@ -27,10 +27,10 @@ func TestDir(t *testing.T) {
} }
func TestCommand(t *testing.T) { func TestCommand(t *testing.T) {
in, out, err := Command("/bin/echo", "\"statping testing\"") t.SkipNow()
_, out, err := Command("/bin/echo", "\"statping testing\"")
assert.Nil(t, err) assert.Nil(t, err)
assert.Contains(t, in, "statping") assert.Contains(t, out, "statping")
assert.Empty(t, out)
} }
func TestReplaceTemplate(t *testing.T) { func TestReplaceTemplate(t *testing.T) {

View File

@ -1 +1 @@
0.90.25 0.90.26