Merge pull request #429 from statping/vue

Vue to master
pull/443/head
Hunter Long 2020-03-18 18:54:15 -07:00 committed by GitHub
commit d4367cc9c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
415 changed files with 45167 additions and 36373 deletions

View File

@ -18,3 +18,14 @@ dev
config.yml
*.db
tmp
frontend/node_modules
.next
node_modules
Dockerfile
Dockerfile.base
!dev/dev-env.sh
!dev/modd.conf
!dev/grafana
!dev/prometheus.yml
docker
statping.db

View File

@ -4,20 +4,20 @@ about: If you're having an issue or see an error
---
**Describe the bug**
### Describe the bug
Try to explain what issue your'e having in detail. You can copy and paste the issue from the log file in your root directory `/logs/statup.log`.
**To Reproduce**
### To Reproduce
Steps to reproduce the behavior:
1. I'm using version: '...'
2. I went to '....'
3. Press this button '....'
4. And things did '....'
**Expected Behavior**
### Expected Behavior
A clear and concise description of what you expected to happen.
**Screenshots**
### Screenshots
If applicable, add screenshots to help explain your problem.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) [![GitHub release](https://img.shields.io/github/release/hunterlong/statup.svg)](https://github.com/hunterlong/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statup.svg?branch=master)](https://travis-ci.com/hunterlong/statup)
[![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)

View File

@ -4,19 +4,19 @@ about: Suggest a feature and let's see what others say
---
**What would you like on Statping?**
### What would you like on Statping?
A clear and concise description of what you want to happen.
**Describe the solution you'd like**
### Describe the solution you'd like
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
**Is your feature request related to a problem? Please describe.**
### Is your feature request related to a problem? Please describe.
I'm always frustrated when [...]
**Additional context**
### Additional context
Add any other context or screenshots about the feature request here.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) [![GitHub release](https://img.shields.io/github/release/hunterlong/statup.svg)](https://github.com/hunterlong/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statup.svg?branch=master)](https://travis-ci.com/hunterlong/statup)
[![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)

View File

@ -4,20 +4,20 @@ about: If you're having an issue or see an error
---
**Describe the bug**
### Describe the bug
A clear and concise description of what the bug is.
**To Reproduce**
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
### Expected behavior
A clear and concise description of what you expected to happen.
**Screenshots**
### Screenshots
If applicable, add screenshots to help explain your problem.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) [![GitHub release](https://img.shields.io/github/release/hunterlong/statup.svg)](https://github.com/hunterlong/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statup.svg?branch=master)](https://travis-ci.com/hunterlong/statup)
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statup-app/general) [![GitHub release](https://img.shields.io/github/release/hunterlong/statup.svg)](https://github.com/statping/statping/releases/latest) [![Build Status](https://travis-ci.com/hunterlong/statup.svg?branch=master)](https://travis-ci.com/hunterlong/statup)

21
.github/workflows/go-test.yml vendored Normal file
View File

@ -0,0 +1,21 @@
on: [push, pull_request]
name: Golang Test
jobs:
test:
env:
GOPATH: ${{ github.workspace }}
GO111MODULE: on
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Checkout code
uses: actions/checkout@v2
with:
path: ./src/github.com/${{ github.repository }}
- name: Go Mod
run: go mod download
- name: Test
run: go test -p=1 ./...

22
.github/workflows/sentry.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: ReleaseWorkflow
on:
release:
types: [published, prereleased]
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Create a Sentry.io release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: Statping
SENTRY_PROJECT: golang
with:
tagName: ${{ github.ref }}
environment: qa

14
.github/workflows/slack-notify.yml vendored Normal file
View File

@ -0,0 +1,14 @@
on: push
name: Slack Notification
jobs:
slackNotification:
name: Slack Notification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2.0.0
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_URL }}
SLACK_CHANNEL: dev
SLACK_USERNAME: Github

8
.gitignore vendored
View File

@ -14,12 +14,11 @@ dev/plugin/*.so
html/scss/.sass-cache
dart-sass
.sass-cache
public
assets
*.log
.env
logs
tmp
source/dist
/dev/test/node_modules
dev/test/cypress/videos
dev/test/cypress/screenshots
@ -29,3 +28,8 @@ sass
.DS_Store
source/css/base.css.map
dev/test/node_modules
source/scss
databases
statping
docker
tmp

View File

@ -2,13 +2,13 @@ os:
- linux
language: go
go: 1.13.5
go_import_path: github.com/hunterlong/statping
go_import_path: github.com/statping/statping
cache:
directories:
- "~/.npm"
- "~/.cache"
- "$GOPATH/src/github.com/hunterlong/statping/tmp"
- "$GOPATH/src/github.com/hunterlong/statping/vendor"
- "$GOPATH/src/github.com/statping/statping/tmp"
- "$GOPATH/src/github.com/statping/statping/vendor"
sudo: required
services:
- docker
@ -23,7 +23,7 @@ env:
- DB_PASS=
- DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/hunterlong/statping
- STATPING_DIR=$GOPATH/src/github.com/statping/statping
matrix:
allow_failures:
- go: master

View File

@ -1,33 +1,22 @@
FROM golang:1.13.5-alpine as base
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
RUN apk add --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq libsass
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
WORKDIR /go/src/github.com/hunterlong/statping
ADD Makefile go.mod /go/src/github.com/hunterlong/statping/
RUN go mod vendor && \
make dev-deps
ADD . /go/src/github.com/hunterlong/statping
RUN make install
FROM statping/statping:base AS base
# Statping :latest Docker Image
# Statping main Docker image that contains all required libraries
FROM alpine:latest
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
RUN apk --no-cache add libgcc libstdc++ curl jq
COPY --from=base /go/bin/statping /usr/local/bin/
COPY --from=base /usr/local/bin/sass /usr/local/bin/
COPY --from=base /usr/local/share/ca-certificates /usr/local/share/
WORKDIR /app
ARG VERSION
ENV IS_DOCKER=true
ENV STATPING_DIR=/app
ENV PORT=8080
RUN apk --no-cache add curl jq libsass
COPY --from=base /usr/local/bin/sass /usr/local/bin/sass
COPY --from=base /go/bin/statping /usr/local/bin/statping
WORKDIR /app
VOLUME /app
EXPOSE $PORT
HEALTHCHECK --interval=60s --timeout=10s --retries=3 CMD curl -s "http://localhost:$PORT/health" | jq -r -e ".online==true"
CMD statping -port $PORT

34
Dockerfile.base Normal file
View File

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

413
Makefile
View File

@ -2,248 +2,131 @@ VERSION=$(shell cat version.txt)
SIGN_KEY=B76D61FAA6DB759466E83D9964B9C6AAE2D55278
BINARY_NAME=statping
GOBUILD=go build -a
GOVERSION=1.13.5
GOVERSION=1.14
XGO=xgo -go $(GOVERSION) --dest=build
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=
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": { "os": [ "linux" ], "language": "go", "go": [ "${GOVERSION}" ], "go_import_path": "github.com/hunterlong/statping", "install": true, "sudo": "required", "services": [ "docker" ], "env": { "VERSION": "${VERSION}", "secure": "${TRVIS_SECRET}" }, "matrix": { "allow_failures": [ { "go": "master" } ], "fast_finish": true }, "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"} } ], "notifications": { "email": false }, "before_script": ["gem install sass"], "script": [ "travis_wait 30 docker pull crazymax/xgo:$(GOVERSION)", "make release" ], "after_success": [], "after_deploy": [ "make publish-homebrew" ] } } }'
TEST_DIR=$(GOPATH)/src/github.com/hunterlong/statping
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "os": [ "linux" ], "language": "go", "go": [ "${GOVERSION}" ], "go_import_path": "github.com/statping/statping", "install": true, "sudo": "required", "services": [ "docker" ], "env": { "VERSION": "${VERSION}", "secure": "${TRVIS_SECRET}" }, "matrix": { "allow_failures": [ { "go": "master" } ], "fast_finish": true }, "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"} } ], "notifications": { "email": false }, "before_script": ["gem install sass"], "script": [ "travis_wait 30 docker pull crazymax/xgo:$(GOVERSION)", "make release" ], "after_success": [], "after_deploy": [ "make publish-homebrew" ] } } }'
TEST_DIR=$(GOPATH)/src/github.com/statping/statping
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
# build all arch's and release Statping
release: dev-deps
wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg
make build-all
all: clean yarn-install compile docker-base docker-vue build-all compress
# build and push the images to docker hub
docker: docker-build-all docker-publish-all
up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
make print_details
# test all versions of Statping, golang testing and then cypress UI testing
test-all: dev-deps test
down:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml down --volumes --remove-orphans
# test all versions of Statping, golang testing and then cypress UI testing
test-ui: dev-deps docker-build-dev cypress-test
lite: clean
docker build -t hunterlong/statping:dev -f dev/Dockerfile.dev .
docker-compose -f dev/docker-compose.lite.yml down
docker-compose -f dev/docker-compose.lite.yml up --remove-orphans
# testing to be ran on travis ci
travis-test: dev-deps cypress-install test coverage
reup: down clean compose-build-full up
# build and compile all arch's for Statping
build-all: build-mac build-linux build-windows build-alpine compress
test: clean
go test -v -p=4 -ldflags="-X main.VERSION=testing" -coverprofile=coverage.out ./...
# build all docker tags
docker-build-all: docker-build-latest
test-ci: clean compile test-deps
SASS=`which sass` STATPING_DIR=${GOPATH}/src/github.com/statping/statping go test -v -covermode=count -coverprofile=coverage.out -p=4 ./...
goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS
bash <(curl -s https://codecov.io/bash)
# push all docker tags built
docker-publish-all: docker-push-latest
snapcraft: clean snapcraft-build snapcraft-release
# build Statping for local arch
build: compile
go mod vendor
$(GOBUILD) $(BUILDVERSION) -o $(BINARY_NAME) -v ./cmd
# build Statping plugins
build-plugin:
$(GOBUILD) $(BUILDVERSION) -buildmode=plugin -o ./dev/plugin/example.so -v ./dev/plugin
test-plugin: clean
mkdir plugins
$(GOBUILD) $(BUILDVERSION) -buildmode=plugin -o ./dev/plugin/example.so -v ./dev/plugin
mv ./dev/plugin/example.so ./plugins/example.so
STATPING_DIR=$(TEST_DIR) go test -v -p=1 $(BUILDVERSION) -coverprofile=coverage.out ./plugin
# build Statping debug app
build-debug: compile
$(GOBUILD) $(BUILDVERSION) -tags debug -o $(BINARY_NAME) -v ./cmd
# install Statping for local arch and move binary to gopath/src/bin/statping
install: build
mv $(BINARY_NAME) $(GOPATH)/bin/$(BINARY_NAME)
$(GOPATH)/bin/$(BINARY_NAME) version
# run Statping from local arch
run: build
./$(BINARY_NAME) --ip 0.0.0.0 --port 8080
# run Statping with Delve for debugging
rundlv:
lsof -ti:8080 | xargs kill
DB_CONN=sqlite DB_HOST=localhost DB_DATABASE=sqlite DB_PASS=none DB_USER=none GO_ENV=test \
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./statping
killdlv:
lsof -ti:2345 | xargs kill
builddlv:
$(GOBUILD) -gcflags "all=-N -l" -o ./$(BINARY_NAME) -v ./cmd
watch:
find . -print | grep -i '.*\.\(go\|gohtml\)' | justrun -v -c \
'go build -v -gcflags "all=-N -l" -o statping ./cmd && make rundlv &' \
-delay 10s -stdin \
-i="Makefile,statping,statup.db,statup.db-journal,handlers/graphql/generated.go"
# compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go
compile: generate
sass source/scss/base.scss source/css/base.css
cd source && rice embed-go
rm -rf .sass-cache
# benchmark testing
benchmark:
cd handlers && go test -v -run=^$ -bench=. -benchtime=5s -memprofile=prof.mem -cpuprofile=prof.cpu
# view benchmark testing using pprof
benchmark-view:
go tool pprof handlers/handlers.test handlers/prof.cpu > top20
# test Statping golang tetsing files
test: clean compile install build-plugin
STATPING_DIR=$(TEST_DIR) go test -v -p=1 $(BUILDVERSION) -coverprofile=coverage.out ./...
gocov convert coverage.out > coverage.json
test-api:
DB_CONN=sqlite DB_HOST=localhost DB_DATABASE=sqlite DB_PASS=none DB_USER=none statping &
sleep 300 && newman run source/tmpl/postman.json -e dev/postman_environment.json --delay-request 500
# report coverage to Coveralls
coverage:
$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS)
# generate documentation for Statping functions
docs:
rm -f dev/README.md
printf "# Statping Dev Documentation\n" > dev/README.md
printf "This readme is automatically generated from the Golang documentation. [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/hunterlong/statping)\n\n" > dev/README.md
godocdown github.com/hunterlong/statping >> dev/README.md
godocdown github.com/hunterlong/statping/cmd >> dev/README.md
godocdown github.com/hunterlong/statping/core >> dev/README.md
godocdown github.com/hunterlong/statping/handlers >> dev/README.md
godocdown github.com/hunterlong/statping/notifiers >> dev/README.md
godocdown github.com/hunterlong/statping/plugin >> dev/README.md
godocdown github.com/hunterlong/statping/source >> dev/README.md
godocdown github.com/hunterlong/statping/types >> dev/README.md
godocdown github.com/hunterlong/statping/utils >> dev/README.md
gocov-html coverage.json > dev/COVERAGE.html
revive -formatter stylish > dev/LINT.md
#
# Build binary for Statping
#
# build Statping for Mac, 64 and 32 bit
build-mac: compile
mkdir build
$(XGO) $(BUILDVERSION) --targets=darwin/amd64,darwin/386 ./cmd
# build Statping for Linux 64, 32 bit, arm6/arm7
build-linux: compile
$(XGO) $(BUILDVERSION) --targets=linux/amd64,linux/386,linux/arm-7,linux/arm-6,linux/arm64 ./cmd
# build for windows 64 bit only
build-windows: compile
$(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd
# build Alpine linux binary (used in docker images)
build-alpine: compile
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd
#
# Docker Makefile commands
#
docker-test:
docker-compose -f docker-compose.test.yml -p statping build
docker-compose -f docker-compose.test.yml -p statping up -d
docker logs -f statping_sut_1
docker wait statping_sut_1
# build :latest docker tag
docker-build-latest:
docker build --build-arg VERSION=${VERSION} -t hunterlong/statping:latest --no-cache -f Dockerfile .
docker tag hunterlong/statping:latest hunterlong/statping:v${VERSION}
# build :dev docker tag
docker-build-dev:
docker build --build-arg VERSION=${VERSION} -t hunterlong/statping:latest --no-cache -f Dockerfile .
docker tag hunterlong/statping:dev hunterlong/statping:dev-v${VERSION}
# build Cypress UI testing :cypress docker tag
docker-build-cypress: clean
GOPATH=$(GOPATH) xgo -out statping -go $(GOVERSION) -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" --targets=linux/amd64 ./cmd
docker build -t hunterlong/statping:cypress -f dev/Dockerfile-cypress .
rm -f statping
# run hunterlong/statping:latest docker image
docker-run: docker-build-latest
docker run -it -p 8080:8080 hunterlong/statping:latest
# run hunterlong/statping:dev docker image
docker-run-dev: docker-build-dev
docker run -t -p 8080:8080 hunterlong/statping:dev
# run Cypress UI testing, hunterlong/statping:cypress docker image
docker-run-cypress: docker-build-cypress
docker run -t hunterlong/statping:cypress
# push the :base and :base-v{VERSION} tag to Docker hub
docker-push-base:
docker tag hunterlong/statping:base hunterlong/statping:base-v${VERSION}
docker push hunterlong/statping:base
docker push hunterlong/statping:base-v${VERSION}
# push the :dev tag to Docker hub
docker-push-dev:
docker push hunterlong/statping:dev
docker push hunterlong/statping:dev-v${VERSION}
# push the :cypress tag to Docker hub
docker-push-cypress:
docker push hunterlong/statping:cypress
# push the :latest tag to Docker hub
docker-push-latest:
docker tag hunterlong/statping hunterlong/statping:dev
docker push hunterlong/statping:latest
docker push hunterlong/statping:dev
docker push hunterlong/statping:v${VERSION}
docker-run-mssql:
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=PaSsW0rD123' -p 1433:1433 -d microsoft/mssql-server-linux
# create Postgres, and MySQL instance using Docker (used for testing)
databases:
docker run --name statping_postgres -p 5432:5432 -e POSTGRES_PASSWORD=password123 -e POSTGRES_USER=root -e POSTGRES_DB=root -d postgres
docker run --name statping_mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password123 -e MYSQL_DATABASE=root -d mysql
sleep 30
# install all required golang dependecies
dev-deps:
go get github.com/stretchr/testify/assert
test-deps:
go get golang.org/x/tools/cmd/cover
go get github.com/mattn/goveralls
go install github.com/mattn/goveralls
go get github.com/rendon/testcli
go get github.com/robertkrimen/godocdown/godocdown
go get github.com/crazy-max/xgo
go get github.com/GeertJohan/go.rice
go get github.com/GeertJohan/go.rice/rice
go install github.com/GeertJohan/go.rice/rice
go get github.com/axw/gocov/gocov
go get github.com/matm/gocov-html
go get github.com/fatih/structs
go get github.com/ararog/timeago
go get gopkg.in/natefinch/lumberjack.v2
go get golang.org/x/crypto/bcrypt
yarn-serve:
cd frontend && yarn serve
yarn-install:
cd frontend && rm -rf node_modules && yarn
go-run:
go run ./cmd
start:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml start
stop:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml stop
logs:
docker logs statping --follow
db-up:
docker-compose -f dev/docker-compose.db.yml up -d --remove-orphans
db-down:
docker-compose -f dev/docker-compose.full.yml down --remove-orphans
console:
docker exec -t -i statping /bin/sh
compose-build-full: docker-base
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml build --parallel --build-arg VERSION=${VERSION}
docker-base:
docker build -t statping/statping:base -f Dockerfile.base --build-arg VERSION=${VERSION} .
docker-latest: docker-base
docker build -t statping/statping:latest --build-arg VERSION=${VERSION} .
docker-vue:
docker build -t statping/statping:vue --build-arg VERSION=${VERSION} .
docker-test:
docker-compose -f docker-compose.test.yml up --remove-orphans
push-base: clean compile docker-base
docker push statping/statping:base
push-vue: clean compile docker-base docker-vue
docker push statping/statping:base
docker push statping/statping:vue
modd:
modd -f ./dev/modd.conf
top:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml top
frontend-build:
rm -rf source/dist && rm -rf frontend/dist
cd frontend && yarn build
cp -r frontend/dist source/ && cp -r frontend/src/assets/scss source/dist/
cp -r source/tmpl/*.* source/dist/
frontend-copy:
cp -r source/tmpl/*.* source/dist/
# compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go
compile: generate frontend-build
rm -f source/rice-box.go
cd source && rice embed-go
embed:
cd source && rice embed-go
build:
$(GOBUILD) $(BUILDVERSION) -o $(BINARY_NAME) ./cmd
install: build
mv $(BINARY_NAME) $(GOPATH)/bin/$(BINARY_NAME)
generate:
cd source && go generate
# remove files for a clean compile/build
clean:
rm -rf ./{logs,assets,plugins,*.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,index.html,vendor}
rm -rf cmd/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,*.html,*.json}
rm -rf core/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf types/notifications/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf handlers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf notifiers/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf source/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
@ -259,18 +142,54 @@ clean:
find . -name "*.out" -type f -delete
find . -name "*.cpu" -type f -delete
find . -name "*.mem" -type f -delete
rm -rf build
rm -rf {build,tmp}
# tag version using git
tag:
git tag v${VERSION} --force
print_details:
@echo \==== Statping Development Instance ====
@echo \Statping Vue Frontend: http://localhost:8888
@echo \Statping Backend API: http://localhost:8585
@echo \==== Statping Instances ====
@echo \Statping SQLite: http://localhost:4000
@echo \Statping MySQL: http://localhost:4005
@echo \Statping Postgres: http://localhost:4010
@echo \==== Databases ====
@echo \PHPMyAdmin: http://localhost:6000 \(MySQL database management\)
@echo \SQLite Web: http://localhost:6050 \(SQLite database management\)
@echo \PGAdmin: http://localhost:7000 \(Postgres database management \| email: admin@admin.com password: admin\)
@echo \Prometheus: http://localhost:7050 \(Prometheus Web UI\)
@echo \==== Monitoring and IDE ====
@echo \Grafana: http://localhost:3000 \(username: admin, password: admin\)
generate:
cd source && go generate
cd handlers/graphql && go generate
build-all: xgo-install build-mac build-linux build-windows build-linux build-alpine compress
download-key:
wget -O statping.gpg $(KEY_URL)
gpg --import statping.gpg
# build Statping for Mac, 64 and 32 bit
build-mac:
mkdir build
$(XGO) $(BUILDVERSION) --targets=darwin/amd64,darwin/386 ./cmd
# build Statping for Linux 64, 32 bit, arm6/arm7
build-linux:
$(XGO) $(BUILDVERSION) --targets=linux/amd64,linux/386,linux/arm-7,linux/arm-6,linux/arm64 ./cmd
# build for windows 64 bit only
build-windows:
$(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd
# build Alpine linux binary (used in docker images)
build-alpine:
$(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd
# build :latest docker tag
docker-build-latest:
docker build --build-arg VERSION=${VERSION} -t hunterlong/statping:latest --no-cache -f Dockerfile .
docker tag hunterlong/statping:latest hunterlong/statping:v${VERSION}
# compress built binaries into tar.gz and zip formats
compress:
compress: download-key
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
@ -307,14 +226,6 @@ publish-dev:
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/hunterlong%2Fhomebrew-statping/requests
# install NPM reuqirements for cypress testing
cypress-install:
cd dev/test && npm install
# run Cypress UI testing
cypress-test: clean cypress-install
cd dev/test && npm test
upload_to_s3:
aws s3 cp ./source/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
@ -331,27 +242,6 @@ travis-build: travis_s3_creds upload_to_s3
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/hunterlong%2Fstatping/requests
curl -H "Content-Type: application/json" --data '{"docker_tag": "latest"}' -X POST $(DOCKER)
snapcraft-build: build-all
PWD=$(shell pwd)
cp build/$(BINARY_NAME)-linux-x64.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=amd64"
cp build/$(BINARY_NAME)-linux-x32.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=i386"
cp build/$(BINARY_NAME)-linux-arm64.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=arm64"
cp build/$(BINARY_NAME)-linux-arm7.tar.gz build/$(BINARY_NAME)-linux.tar.gz
snapcraft clean statping -s pull
docker run --rm -v ${PWD}:/build -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=armhf"
rm -f build/$(BINARY_NAME)-linux.tar.gz
snapcraft-release:
snapcraft push statping_${VERSION}_arm64.snap --release stable
snapcraft push statping_${VERSION}_i386.snap --release stable
snapcraft push statping_${VERSION}_armhf.snap --release stable
sign-all:
gpg --default-key $SIGN_KEY --detach-sign --armor statpinger
@ -363,13 +253,6 @@ xgo-install: clean
go get github.com/crazy-max/xgo
docker pull crazy-max/xgo:${GOVERSION}
heroku:
git push heroku master
heroku container:push web
heroku container:release web
checkall:
golangci-lint run ./...
.PHONY: all build build-all build-alpine test-all test test-api docker
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite
.SILENT: travis_s3_creds

View File

@ -3,21 +3,19 @@
</p>
<p align="center">
<b>Statping - Web and App Status Monitoring for Any Type of Project</b><br>
<a href="https://github.com/hunterlong/statping/wiki">View Wiki</a> | <a href="https://demo.statping.com">Demo</a> | <a href="https://itunes.apple.com/us/app/apple-store/id1445513219">iPhone</a> | <a href="https://play.google.com/store/apps/details?id=com.statping">Android</a> <br> <a href="https://github.com/hunterlong/statping/wiki/API">API</a> | <a href="https://github.com/hunterlong/statping/wiki/Docker">Docker</a> | <a href="https://github.com/hunterlong/statping/wiki/AWS-EC2">EC2</a> | <a href="https://github.com/hunterlong/statping/wiki/Mac">Mac</a> | <a href="https://github.com/hunterlong/statping/wiki/Linux">Linux</a> | <a href="https://github.com/hunterlong/statping/wiki/Windows">Windows</a> | <a href="https://github.com/hunterlong/statping/wiki/Statping-Plugins">Plugins</a>
<a href="https://github.com/statping/statping/wiki">View Wiki</a> | <a href="https://demo.statping.com">Demo</a> | <a href="https://itunes.apple.com/us/app/apple-store/id1445513219">iPhone</a> | <a href="https://play.google.com/store/apps/details?id=com.statping">Android</a> <br> <a href="https://github.com/statping/statping/wiki/API">API</a> | <a href="https://github.com/statping/statping/wiki/Docker">Docker</a> | <a href="https://github.com/statping/statping/wiki/AWS-EC2">EC2</a> | <a href="https://github.com/statping/statping/wiki/Mac">Mac</a> | <a href="https://github.com/statping/statping/wiki/Linux">Linux</a> | <a href="https://github.com/statping/statping/wiki/Windows">Windows</a> | <a href="https://github.com/statping/statping/wiki/Statping-Plugins">Plugins</a>
</p>
# Statping - Status Page & Monitoring Server
An easy to use Status Page for your websites and applications. Statping will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page. This Status Page generator allows you to use MySQL, Postgres, or SQLite on multiple operating systems.
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/hunterlong/statping) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statping/general) [![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/statping/statping) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statping/general) [![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/)
## A Future-Proof Status Page
<img align="left" width="320" height="235" src="https://img.cjx.io/statupsiterun.gif">
<h2>A Future-Proof Status Page</h2>
Statping strives to remain future-proof and remain intact if a failure is created. Your Statping service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.
<p align="center">
<img width="80%" src="https://img.cjx.io/statupsiterun.gif">
</p>
## Lightweight and Fast
Statping is a very lightweight application and is available for Linux, Mac, and Windows. The Docker image is only ~16Mb so you know that this application won't be filling up your hard drive space.
The Status binary for all other OS's is ~17Mb at most.
@ -26,9 +24,9 @@ The Status binary for all other OS's is ~17Mb at most.
Statping is built in Go Language so all you need is the precompile binary based on your operating system. You won't need to install anything extra once you have the Statping binary installed. You can even run Statping on a Raspberry Pi.
<p align="center">
<a href="https://github.com/hunterlong/statping/wiki/Linux"><img width="5%" src="https://img.cjx.io/linux.png"></a>
<a href="https://github.com/hunterlong/statping/wiki/Mac"><img width="5%" src="https://img.cjx.io/apple.png"></a>
<a href="https://github.com/hunterlong/statping/wiki/Windows"><img width="5%" src="https://img.cjx.io/windows.png"></a>
<a href="https://github.com/statping/statping/wiki/Linux"><img width="5%" src="https://img.cjx.io/linux.png"></a>
<a href="https://github.com/statping/statping/wiki/Mac"><img width="5%" src="https://img.cjx.io/apple.png"></a>
<a href="https://github.com/statping/statping/wiki/Windows"><img width="5%" src="https://img.cjx.io/windows.png"></a>
<a href="https://play.google.com/store/apps/details?id=com.statping"><img width="5%" src="https://img.cjx.io/android.png"></a>
<a href="https://itunes.apple.com/us/app/apple-store/id1445513219"><img width="5%" src="https://img.cjx.io/appstore.png"></a>
<a href="https://hub.docker.com/r/hunterlong/statping"><img width="5%" src="https://img.cjx.io/dockericon.png"></a>
@ -48,7 +46,7 @@ The Statping app is available on the App Store and Google Play for free. The app
</p>
## Run on Any Server
Whether you're a Docker fan-boy or a [AWS EC2](https://github.com/hunterlong/statping/wiki/AWS-EC2) master, Statping gives you multiple options to simply get running. Our Amazon AMI image is only 8Gb and will automatically update to the most stable version of Statping.
Whether you're a Docker fan-boy or a [AWS EC2](https://github.com/statping/statping/wiki/AWS-EC2) master, Statping gives you multiple options to simply get running. Our Amazon AMI image is only 8Gb and will automatically update to the most stable version of Statping.
Running on an EC2 server might be the most cost effective way to host your own Statping Status Page. The server runs on the smallest EC2 instance (t2.nano) AWS has to offer, which only costs around $4.60 USD a month for your dedicated Status Page.
Want to run it on your own Docker server? Awesome! Statping has multiple docker-compose.yml files to work with. Statping can automatically create a SSL Certification for your status page.
@ -63,8 +61,8 @@ Statping will allow you to completely customize your Status Page using SASS styl
Statping includes email notification via SMTP and Slack integration using [Incoming Webhook](https://api.slack.com/incoming-webhooks). Insert the webhook URL into the Settings page in Statping and enable the Slack integration. Anytime a service fails, you're channel that you specified on Slack will receive a message.
## User Created Plugins and Notifiers
View the [Plugin Wiki](https://github.com/hunterlong/statping/wiki/Statping-Plugins) to see detailed information about Golang Plugins. Statping isn't just another Status Page for your applications, it's a framework that allows you to create your own plugins to interact with every element of your status page. [Notifier's](https://github.com/hunterlong/statping/wiki/Notifiers) can also be create with only 1 golang file.
Plugin are created in Golang using the [statping/plugin](https://github.com/hunterlong/statping/tree/master/plugin) golang package. The plugin package has a list of interfaces/events to accept into your own plugin application.
View the [Plugin Wiki](https://github.com/statping/statping/wiki/Statping-Plugins) to see detailed information about Golang Plugins. Statping isn't just another Status Page for your applications, it's a framework that allows you to create your own plugins to interact with every element of your status page. [Notifier's](https://github.com/statping/statping/wiki/Notifiers) can also be create with only 1 golang file.
Plugin are created in Golang using the [statping/plugin](https://github.com/statping/statping/tree/master/plugin) golang package. The plugin package has a list of interfaces/events to accept into your own plugin application.
<p align="center">
<img width="100%" src="https://img.cjx.io/statupsc2.png">
@ -85,7 +83,7 @@ statping export
###### `index.html` will be created in the current directory with CDN URL's for assets.
## Run on Docker
Use the [Statping Docker Image](https://hub.docker.com/r/hunterlong/statping) to create a status page in seconds. Checkout the [Docker Wiki](https://github.com/hunterlong/statping/wiki/Docker) to view more details on how to get started using Docker.
Use the [Statping Docker Image](https://hub.docker.com/r/hunterlong/statping) to create a status page in seconds. Checkout the [Docker Wiki](https://github.com/statping/statping/wiki/Docker) to view more details on how to get started using Docker.
```bash
docker run -it -p 8080:8080 hunterlong/statping
```
@ -107,7 +105,7 @@ LETSENCRYPT_HOST=mydomain.com \
Once your instance has started, it will take a moment to get your SSL certificate. Make sure you have a A or CNAME record on your domain that points to the IP/DNS of your server running Statping.
## Prometheus Exporter
Statping includes a [Prometheus Exporter](https://github.com/hunterlong/statping/wiki/Prometheus-Exporter) so you can have even more monitoring power with your services. The Prometheus exporter can be seen on `/metrics`, simply create another exporter in your prometheus config. Use your Statping API Secret for the Authorization Bearer header, the `/metrics` URL is dedicated for Prometheus and requires the correct API Secret has `Authorization` header.
Statping includes a [Prometheus Exporter](https://github.com/statping/statping/wiki/Prometheus-Exporter) so you can have even more monitoring power with your services. The Prometheus exporter can be seen on `/metrics`, simply create another exporter in your prometheus config. Use your Statping API Secret for the Authorization Bearer header, the `/metrics` URL is dedicated for Prometheus and requires the correct API Secret has `Authorization` header.
```yaml
scrape_configs:
- job_name: 'statping'
@ -117,7 +115,7 @@ scrape_configs:
```
## Run on EC2 Server
Running Statping on the smallest EC2 server is very quick using the AWS AMI Image. Checkout the [AWS Wiki](https://github.com/hunterlong/statping/wiki/AWS-EC2) to see a step by step guide on how to get your EC2 Statping service online.
Running Statping on the smallest EC2 server is very quick using the AWS AMI Image. Checkout the [AWS Wiki](https://github.com/statping/statping/wiki/AWS-EC2) to see a step by step guide on how to get your EC2 Statping service online.
##### Create Security Groups
Create the AWS Security Groups with the commands below, Statping will expose port 80 and 443.
@ -158,11 +156,11 @@ aws ec2 run-instances \
```
## Contributing
Statping accepts Push Requests! Feel free to add your own features and notifiers. You probably want to checkout the [Notifier Wiki](https://github.com/hunterlong/statping/wiki/Notifiers) to get a better understanding on how to create your own notification methods for failing/successful services. Testing on Statping will test each function on MySQL, Postgres, and SQLite. I recommend you run a MySQL and a Postgres Docker image for testing.
Statping accepts Push Requests! Feel free to add your own features and notifiers. You probably want to checkout the [Notifier Wiki](https://github.com/statping/statping/wiki/Notifiers) to get a better understanding on how to create your own notification methods for failing/successful services. Testing on Statping will test each function on MySQL, Postgres, and SQLite. I recommend you run a MySQL and a Postgres Docker image for testing.
[![Go Report Card](https://goreportcard.com/badge/github.com/hunterlong/statping)](https://goreportcard.com/report/github.com/hunterlong/statping)
[![Go Report Card](https://goreportcard.com/badge/github.com/statping/statping)](https://goreportcard.com/report/github.com/statping/statping)
[![Build Status](https://travis-ci.com/hunterlong/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping) [![Cypress.io tests](https://img.shields.io/badge/cypress.io-tests-green.svg?style=flat-square)](https://dashboard.cypress.io/#/projects/bi8mhr/runs)
[![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/) [![Godoc](https://godoc.org/github.com/hunterlong/statping?status.svg)](https://godoc.org/github.com/hunterlong/statping)[![Coverage Status](https://coveralls.io/repos/github/hunterlong/statping/badge.svg?branch=master)](https://coveralls.io/github/hunterlong/statping?branch=master)
[![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/) [![Godoc](https://godoc.org/github.com/statping/statping?status.svg)](https://godoc.org/github.com/statping/statping)[![Coverage Status](https://coveralls.io/repos/github/hunterlong/statping/badge.svg?branch=master)](https://coveralls.io/github/hunterlong/statping?branch=master)
<p align="center">

View File

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

30
cmd/assets.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"fmt"
"github.com/statping/statping/source"
"github.com/statping/statping/utils"
"net"
)
// UsingAssets will return true if /assets folder is present
func UsingAssets() bool {
return source.UsingAssets(utils.Directory)
}
// GetLocalIP returns the non loopback local IP of the host
func GetLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "http://localhost"
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return fmt.Sprintf("http://%v", ipnet.IP.String())
}
}
}
return "http://localhost"
}

View File

@ -2,7 +2,7 @@
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
// https://github.com/statping/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
@ -16,18 +16,20 @@
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers"
"github.com/hunterlong/statping/plugin"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/statping/statping/handlers"
"github.com/statping/statping/source"
"github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"io/ioutil"
"net/http/httptest"
"os"
"strings"
"time"
)
@ -36,7 +38,6 @@ func catchCLI(args []string) error {
dir := utils.Directory
runLogs := utils.InitLogs
runAssets := source.Assets
loadDotEnvs()
switch args[0] {
case "version":
@ -65,40 +66,33 @@ func catchCLI(args []string) error {
if err := runAssets(); err != nil {
return err
}
if err := source.CompileSASS(dir); err != nil {
if err := source.CompileSASS(source.DefaultScss...); err != nil {
return err
}
return errors.New("end")
case "update":
updateDisplay()
return errors.New("end")
case "test":
cmd := args[1]
switch cmd {
case "plugins":
plugin.LoadPlugins()
}
return errors.New("end")
case "static":
var err error
if err = runLogs(); err != nil {
return err
}
if err = runAssets(); err != nil {
return err
}
fmt.Printf("Statping v%v Exporting Static 'index.html' page...\n", VERSION)
if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil {
log.Errorln("config.yml file not found")
return err
}
indexSource := ExportIndexHTML()
//core.CloseDB()
if err = utils.SaveFile(dir+"/index.html", indexSource); err != nil {
log.Errorln(err)
return err
}
log.Infoln("Exported Statping index page: 'index.html'")
//var err error
//if err = runLogs(); err != nil {
// return err
//}
//if err = runAssets(); err != nil {
// return err
//}
//fmt.Printf("Statping v%v Exporting Static 'index.html' page...\n", VERSION)
//if _, err = core.LoadConfigFile(dir); err != nil {
// log.Errorln("config.yml file not found")
// return err
//}
//indexSource := ExportIndexHTML()
////core.CloseDB()
//if err = utils.SaveFile(dir+"/index.html", indexSource); err != nil {
// log.Errorln(err)
// return err
//}
//log.Infoln("Exported Statping index page: 'index.html'")
case "help":
HelpEcho()
return errors.New("end")
@ -111,19 +105,24 @@ func catchCLI(args []string) error {
if err = runAssets(); err != nil {
return err
}
if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil {
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = core.CoreApp.Connect(false, dir); err != nil {
if err = configs.ConnectConfigs(config); err != nil {
return err
}
if data, err = core.ExportSettings(); err != nil {
if _, err := services.SelectAllServices(false); err != nil {
return err
}
if data, err = handlers.ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
//core.CloseDB()
if err = utils.SaveFile(dir+"/statping-export.json", data); err != nil {
filename := fmt.Sprintf("%s/statping-%s.json", dir, time.Now().Format("01-02-2006-1504"))
if err = utils.SaveFile(filename, data); err != nil {
return fmt.Errorf("could not write file statping-export.json: %v", err.Error())
}
log.Infoln("Statping export file saved to ", filename)
return errors.New("end")
case "import":
var err error
@ -135,10 +134,75 @@ func catchCLI(args []string) error {
if data, err = ioutil.ReadFile(filename); err != nil {
return err
}
var exportData core.ExportData
var exportData handlers.ExportData
if err = json.Unmarshal(data, &exportData); err != nil {
return err
}
log.Printf("=== %s ===\n", exportData.Core.Name)
log.Printf("Services: %d\n", len(exportData.Services))
log.Printf("Checkins: %d\n", len(exportData.Checkins))
log.Printf("Groups: %d\n", len(exportData.Groups))
log.Printf("Messages: %d\n", len(exportData.Messages))
log.Printf("Users: %d\n", len(exportData.Users))
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = configs.ConnectConfigs(config); err != nil {
return err
}
if data, err = handlers.ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
if ask("Import Core settings?") {
c := exportData.Core
if err := c.Update(); err != nil {
return err
}
}
for _, s := range exportData.Groups {
if ask(fmt.Sprintf("Import Group '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Services {
if ask(fmt.Sprintf("Import Service '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Checkins {
if ask(fmt.Sprintf("Import Checkin '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Messages {
if ask(fmt.Sprintf("Import Message '%s'?", s.Title)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Users {
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
log.Infof("Import complete")
return errors.New("end")
case "run":
if err := runLogs(); err != nil {
@ -174,31 +238,42 @@ func catchCLI(args []string) error {
return errors.New("end")
}
// ExportIndexHTML returns the HTML of the index page as a string
func ExportIndexHTML() []byte {
source.Assets()
core.CoreApp.Connect(false, utils.Directory)
core.CoreApp.SelectAllServices(false)
core.CoreApp.UseCdn = types.NewNullBool(true)
for _, srv := range core.CoreApp.Services {
service := srv.(*core.Service)
service.Check(true)
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
handlers.ExecuteResponse(w, r, "index.gohtml", nil, nil)
return w.Body.Bytes()
func ask(format string) bool {
fmt.Printf(fmt.Sprintf(format + " [y/N]: "))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
text = strings.Replace(text, "\n", "", -1)
return strings.ToLower(text) == "y"
}
// ExportIndexHTML returns the HTML of the index page as a string
//func ExportIndexHTML() []byte {
// source.Assets()
// core.CoreApp.Connect(core.CoreApp., utils.Directory)
// core.SelectAllServices(false)
// core.CoreApp.UseCdn = types.NewNullBool(true)
// for _, srv := range core.Services() {
// core.CheckService(srv, true)
// }
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/", nil)
// handlers.ExecuteResponse(w, r, "index.gohtml", nil, nil)
// return w.Body.Bytes()
//}
func updateDisplay() error {
var err error
var gitCurrent githubResponse
if gitCurrent, err = checkGithubUpdates(); err != nil {
fmt.Printf("Issue connecting to https://github.com/hunterlong/statping\n%v\n", err)
return err
gitCurrent, err := checkGithubUpdates()
if err != nil {
return errors.Wrap(err, "Issue connecting to https://github.com/statping/statping")
}
if gitCurrent.TagName == "" {
return nil
}
if len(gitCurrent.TagName) < 2 {
return nil
}
if VERSION != gitCurrent.TagName[1:] {
fmt.Printf("\nNew Update %v Available!\n", gitCurrent.TagName[1:])
fmt.Printf("New Update %v Available!\n", gitCurrent.TagName[1:])
fmt.Printf("Update Command:\n")
fmt.Printf("curl -o- -L https://statping.com/install.sh | bash\n\n")
}
@ -206,27 +281,30 @@ func updateDisplay() error {
}
// runOnce will initialize the Statping application and check each service 1 time, will not run HTTP server
func runOnce() {
var err error
core.CoreApp.Config, err = core.LoadConfigFile(utils.Directory)
func runOnce() error {
config, err := configs.LoadConfigs()
if err != nil {
log.Errorln("config.yml file not found")
return errors.Wrap(err, "config.yml file not found")
}
err = core.CoreApp.Connect(false, utils.Directory)
err = configs.ConnectConfigs(config)
if err != nil {
log.Errorln(err)
return errors.Wrap(err, "issue connecting to database")
}
core.CoreApp, err = core.SelectCore()
c, err := core.Select()
if err != nil {
fmt.Println("Core database was not found, Statping is not setup yet.")
return errors.Wrap(err, "core database was not found or setup")
}
_, err = core.CoreApp.SelectAllServices(true)
core.App = c
_, err = services.SelectAllServices(true)
if err != nil {
log.Errorln(err)
return errors.Wrap(err, "could not select all services")
}
for _, out := range core.CoreApp.Services {
out.Check(true)
for _, srv := range services.Services() {
srv.CheckService(true)
}
return nil
}
// HelpEcho prints out available commands and flags for Statping
@ -276,7 +354,7 @@ func HelpEcho() {
fmt.Println(" AUTH_PASSWORD - HTTP Basic Authentication password")
fmt.Println(" BASE_PATH - Set the base URL prefix (set to 'monitor' if URL is domain.com/monitor)")
fmt.Println(" * You can insert environment variables into a '.env' file in root directory.")
fmt.Println("Give Statping a Star at https://github.com/hunterlong/statping")
fmt.Println("Give Statping a Star at https://github.com/statping/statping")
}
func checkGithubUpdates() (githubResponse, error) {

View File

@ -2,7 +2,7 @@
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
// https://github.com/statping/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
@ -16,10 +16,12 @@
package main
import (
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/utils"
"fmt"
"github.com/getsentry/sentry-go"
"github.com/rendon/testcli"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"os/exec"
"testing"
@ -32,7 +34,15 @@ var (
func init() {
dir = utils.Directory
core.SampleHits = 480
//core.SampleHits = 480
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: "testing",
}); err != nil {
fmt.Println(err)
}
}
func TestStartServerCommand(t *testing.T) {
@ -84,6 +94,7 @@ func TestExportCommand(t *testing.T) {
}
func TestUpdateCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "version")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
@ -93,13 +104,23 @@ func TestUpdateCommand(t *testing.T) {
}
func TestAssetsCommand(t *testing.T) {
t.SkipNow()
c := testcli.Command("statping", "assets")
c.Run()
t.Log(c.Stdout())
t.Log("Directory for Assets: ", dir)
time.Sleep(1 * time.Second)
err := utils.DeleteDirectory(dir + "/assets")
require.Nil(t, err)
assert.FileExists(t, dir+"/assets/robots.txt")
assert.FileExists(t, dir+"/assets/scss/base.scss")
assert.FileExists(t, dir+"/assets/scss/main.scss")
assert.FileExists(t, dir+"/assets/scss/variables.scss")
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
assert.FileExists(t, dir+"/assets/css/style.css")
err = utils.DeleteDirectory(dir + "/assets")
require.Nil(t, err)
}
func TestRunCommand(t *testing.T) {
@ -127,13 +148,19 @@ func TestVersionCLI(t *testing.T) {
func TestAssetsCLI(t *testing.T) {
catchCLI([]string{"assets"})
//assert.EqualError(t, run, "end")
assert.FileExists(t, dir+"/assets/css/base.css")
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/style.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
assert.FileExists(t, dir+"/assets/scss/base.scss")
assert.FileExists(t, dir+"/assets/scss/mobile.scss")
assert.FileExists(t, dir+"/assets/scss/variables.scss")
}
func TestSassCLI(t *testing.T) {
catchCLI([]string{"sass"})
assert.FileExists(t, dir+"/assets/css/base.css")
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/style.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
}
func TestUpdateCLI(t *testing.T) {
@ -146,12 +173,6 @@ func TestUpdateCLI(t *testing.T) {
assert.Contains(t, gg, "version")
}
func TestTestPackageCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"test", "plugins"})
assert.EqualError(t, run, "end")
}
func TestHelpCLI(t *testing.T) {
run := catchCLI([]string{"help"})
assert.EqualError(t, run, "end")

22
cmd/database.go Normal file
View File

@ -0,0 +1,22 @@
package main
import (
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/types/incidents"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users"
)
var (
// DbSession stores the Statping database session
DbModels []interface{}
)
func init() {
DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}}
}

View File

@ -23,5 +23,5 @@
// docker pull karalabe/xgo-latest
// build-all
//
// More info on: https://github.com/hunterlong/statping
// More info on: https://github.com/statping/statping
package main

26
cmd/init.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"github.com/statping/statping/database"
"github.com/statping/statping/notifiers"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/services"
)
func InitApp() error {
if _, err := core.Select(); err != nil {
return err
}
if _, err := services.SelectAllServices(true); err != nil {
return err
}
go services.CheckServices()
notifiers.InitNotifiers()
database.StartMaintenceRoutine()
core.App.Setup = true
return nil
}

View File

@ -2,7 +2,7 @@
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
// https://github.com/statping/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
@ -16,18 +16,21 @@
package main
import (
"github.com/hunterlong/statping/utils"
"flag"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers"
"github.com/hunterlong/statping/plugin"
"github.com/hunterlong/statping/source"
"github.com/joho/godotenv"
"github.com/getsentry/sentry-go"
"os"
"os/signal"
"syscall"
"time"
"github.com/statping/statping/source"
"github.com/pkg/errors"
"github.com/statping/statping/handlers"
"github.com/statping/statping/types/configs"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
)
var (
@ -40,44 +43,55 @@ var (
verboseMode int
port int
log = utils.Log.WithField("type", "cmd")
httpServer = make(chan bool)
confgs *configs.DbConfig
)
func init() {
core.VERSION = VERSION
}
// parseFlags will parse the application flags
// -ip = 0.0.0.0 IP address for outgoing HTTP server
// -port = 8080 Port number for outgoing HTTP server
// environment variables WILL overwrite flags
func parseFlags() {
flag.StringVar(&ipAddress, "ip", "0.0.0.0", "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", 8080, "Port to run the HTTP server")
flag.IntVar(&verboseMode, "verbose", 2, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
envPort := utils.Getenv("PORT", 8080).(int)
envIpAddress := utils.Getenv("IP", "0.0.0.0").(string)
envVerbose := utils.Getenv("VERBOSE", 2).(int)
if os.Getenv("PORT") != "" {
port = int(utils.ToInt(os.Getenv("PORT")))
}
if os.Getenv("IP") != "" {
ipAddress = os.Getenv("IP")
}
if os.Getenv("VERBOSE") != "" {
verboseMode = int(utils.ToInt(os.Getenv("VERBOSE")))
}
flag.StringVar(&ipAddress, "ip", envIpAddress, "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", envPort, "Port to run the HTTP server")
flag.IntVar(&verboseMode, "verbose", envVerbose, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
}
func exit(err error) {
sentry.CaptureException(err)
log.Fatalln(err)
Close()
os.Exit(2)
}
// main will run the Statping application
func main() {
var err error
go sigterm()
parseFlags()
loadDotEnvs()
source.Assets()
if err := source.Assets(); err != nil {
exit(err)
}
utils.VerboseMode = verboseMode
if err := utils.InitLogs(); err != nil {
log.Errorf("Statping Log Error: %v\n", err)
}
args := flag.Args()
if len(args) >= 1 {
@ -85,75 +99,143 @@ func main() {
if err != nil {
if err.Error() == "end" {
os.Exit(0)
return
}
fmt.Println(err)
os.Exit(1)
exit(err)
}
}
log.Info(fmt.Sprintf("Starting Statping v%v", VERSION))
updateDisplay()
configs, err := core.LoadConfigFile(utils.Directory)
if err != nil {
if err := updateDisplay(); err != nil {
log.Warnln(err)
}
errorEnv := utils.Getenv("GO_ENV", "production").(string)
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: errorEnv,
}); err != nil {
log.Errorln(err)
core.SetupMode = true
writeAble, err := utils.DirWritable(utils.Directory)
if err != nil {
log.Fatalln(err)
}
if !writeAble {
log.Fatalf("Statping does not have write permissions at: %v\nYou can change this directory by setting the STATPING_DIR environment variable to a dedicated path before starting.", utils.Directory)
}
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
}
confgs, err = configs.LoadConfigs()
if err != nil {
if err := SetupMode(); err != nil {
exit(err)
}
}
core.CoreApp.Config = configs
if err = configs.ConnectConfigs(confgs); err != nil {
exit(err)
}
exists := confgs.Db.HasTable("core")
if !exists {
var srvs int64
confgs.Db.Model(&services.Service{}).Count(&srvs)
if srvs > 0 {
exit(errors.Wrap(err, "there are already services setup."))
}
if err := confgs.DropDatabase(); err != nil {
exit(errors.Wrap(err, "error dropping database"))
}
if err := confgs.CreateDatabase(); err != nil {
exit(errors.Wrap(err, "error creating database"))
}
if err := configs.CreateAdminUser(confgs); err != nil {
exit(errors.Wrap(err, "error creating default admin user"))
}
if err := configs.TriggerSamples(); err != nil {
exit(errors.Wrap(err, "error creating database"))
}
}
if err = confgs.DatabaseChanges(); err != nil {
exit(err)
}
if err := confgs.MigrateDatabase(); err != nil {
exit(err)
}
//log.Infoln("Migrating Notifiers...")
//if err := notifier.Migrate(); err != nil {
// exit(errors.Wrap(err, "error migrating notifiers"))
//}
//log.Infoln("Notifiers Migrated")
if err := mainProcess(); err != nil {
log.Fatalln(err)
exit(err)
}
}
// Close will gracefully stop the database connection, and log file
func Close() {
core.CloseDB()
sentry.Flush(3 * time.Second)
utils.CloseLogs()
confgs.Close()
}
func SetupMode() error {
return handlers.RunHTTPServer(ipAddress, port)
}
// sigterm will attempt to close the database connections gracefully
func sigterm() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
fmt.Println("Shutting down Statping")
Close()
os.Exit(1)
}
// loadDotEnvs attempts to load database configs from a '.env' file in root directory
func loadDotEnvs() error {
err := godotenv.Load(envFile)
if err == nil {
log.Infoln("Environment file '.env' Loaded")
}
return err
}
// mainProcess will initialize the Statping application and run the HTTP server
func mainProcess() error {
dir := utils.Directory
var err error
err = core.CoreApp.Connect(false, dir)
if err != nil {
log.Errorln(fmt.Sprintf("could not connect to database: %v", err))
if err := services.ServicesFromEnvFile(); err != nil {
errStr := "error 'SERVICE' environment variable"
log.Errorln(errStr)
return errors.Wrap(err, errStr)
}
if err := InitApp(); err != nil {
return err
}
core.CoreApp.MigrateDatabase()
core.InitApp()
if !core.SetupMode {
plugin.LoadPlugins()
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
return errors.Wrap(err, "http server")
}
return nil
}
func StartHTTPServer() {
httpServer = make(chan bool)
go httpServerProcess(httpServer)
}
func StopHTTPServer() {
}
func httpServerProcess(process <-chan bool) {
for {
select {
case <-process:
fmt.Println("HTTP Server has stopped")
return
default:
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Errorln(err)
exit(err)
}
}
}
return err
}
const errorReporter = "https://2bedd272821643e1b92c774d3fdf28e7@sentry.statping.com/2"

View File

@ -1,267 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sort"
"time"
)
type Checkin struct {
*types.Checkin
}
type CheckinHit struct {
*types.CheckinHit
}
// Select returns a *types.Checkin
func (c *Checkin) Select() *types.Checkin {
return c.Checkin
}
// Routine for checking if the last Checkin was within its interval
func (c *Checkin) Routine() {
if c.Last() == nil {
return
}
reCheck := c.Period()
CheckinLoop:
for {
select {
case <-c.Running:
log.Infoln(fmt.Sprintf("Stopping checkin routine: %v", c.Name))
c.Failing = false
break CheckinLoop
case <-time.After(reCheck):
log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())))
if c.Expected().Seconds() <= 0 {
issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, c.Last().CreatedAt)
log.Errorln(issue)
c.CreateFailure()
}
reCheck = c.Period()
}
continue
}
}
// String will return a Checkin API string
func (c *Checkin) String() string {
return c.ApiKey
}
// ReturnCheckin converts *types.Checking to *core.Checkin
func ReturnCheckin(c *types.Checkin) *Checkin {
return &Checkin{Checkin: c}
}
// ReturnCheckinHit converts *types.checkinHit to *core.checkinHit
func ReturnCheckinHit(c *types.CheckinHit) *CheckinHit {
return &CheckinHit{CheckinHit: c}
}
func (c *Checkin) Service() *Service {
return SelectService(c.ServiceId)
}
func (c *Checkin) CreateFailure() (int64, error) {
service := c.Service()
c.Failing = true
fail := &Failure{&types.Failure{
Issue: fmt.Sprintf("Checkin %v was not reported %v ago, it expects a request every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())),
Method: "checkin",
MethodId: c.Id,
Service: service.Id,
Checkin: c.Id,
PingTime: c.Expected().Seconds(),
}}
row := failuresDB().Create(&fail)
sort.Sort(types.FailSort(c.Failures))
c.Failures = append(c.Failures, fail)
if len(c.Failures) > limitedFailures {
c.Failures = c.Failures[1:]
}
return fail.Id, row.Error
}
// LimitedHits will return the last amount of successful hits from a checkin
func (c *Checkin) LimitedHits(amount int64) []*types.CheckinHit {
var hits []*types.CheckinHit
checkinHitsDB().Where("checkin = ?", c.Id).Order("id desc").Limit(amount).Find(&hits)
return hits
}
// AllCheckins returns all checkin in system
func AllCheckins() []*Checkin {
var checkins []*Checkin
checkinDB().Find(&checkins)
return checkins
}
// SelectCheckin will find a Checkin based on the API supplied
func SelectCheckin(api string) *Checkin {
for _, s := range Services() {
for _, c := range s.Select().Checkins {
if c.Select().ApiKey == api {
return c.(*Checkin)
}
}
}
return nil
}
// Period will return the duration of the Checkin interval
func (c *Checkin) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval))
return duration
}
// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response)
func (c *Checkin) Grace() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod))
return duration
}
// Expected returns the duration of when the serviec should receive a Checkin
func (c *Checkin) Expected() time.Duration {
last := c.Last().CreatedAt
now := utils.Now()
lastDir := now.Sub(last)
sub := time.Duration(c.Period() - lastDir)
return sub
}
// Last returns the last checkinHit for a Checkin
func (c *Checkin) Last() *CheckinHit {
var hit CheckinHit
checkinHitsDB().Where("checkin = ?", c.Id).Last(&hit)
return &hit
}
func (c *Checkin) Link() string {
return fmt.Sprintf("%v/checkin/%v", CoreApp.Domain, c.ApiKey)
}
// AllHits returns all of the CheckinHits for a given Checkin
func (c *Checkin) AllHits() []*types.CheckinHit {
var checkins []*types.CheckinHit
checkinHitsDB().Where("checkin = ?", c.Id).Order("id DESC").Find(&checkins)
return checkins
}
// Hits returns all of the CheckinHits for a given Checkin
func (c *Checkin) LimitedFailures(amount int64) []types.FailureInterface {
var failures []*Failure
var failInterfaces []types.FailureInterface
col := failuresDB().Where("checkin = ?", c.Id).Where("method = 'checkin'").Limit(amount).Order("id desc")
col.Find(&failures)
for _, f := range failures {
failInterfaces = append(failInterfaces, f)
}
return failInterfaces
}
// Hits returns all of the CheckinHits for a given Checkin
func (c *Checkin) AllFailures() []*types.Failure {
var failures []*types.Failure
col := failuresDB().Where("checkin = ?", c.Id).Where("method = 'checkin'").Order("id desc")
col.Find(&failures)
return failures
}
// Create will create a new Checkin
func (c *Checkin) Delete() error {
c.Close()
i := c.index()
service := c.Service()
slice := service.Checkins
service.Checkins = append(slice[:i], slice[i+1:]...)
row := checkinDB().Delete(&c)
return row.Error
}
// index returns a checkin index int for updating the *checkin.Service slice
func (c *Checkin) index() int {
for k, checkin := range c.Service().Checkins {
if c.Id == checkin.Select().Id {
return k
}
}
return 0
}
// Create will create a new Checkin
func (c *Checkin) Create() (int64, error) {
c.ApiKey = utils.RandomString(7)
row := checkinDB().Create(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
}
service := SelectService(c.ServiceId)
service.Checkins = append(service.Checkins, c)
c.Start()
go c.Routine()
return c.Id, row.Error
}
// Update will update a Checkin
func (c *Checkin) Update() (int64, error) {
row := checkinDB().Update(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
}
return c.Id, row.Error
}
// Create will create a new successful checkinHit
func (c *CheckinHit) Create() (int64, error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = utils.Now()
}
row := checkinHitsDB().Create(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
}
return c.Id, row.Error
}
// Ago returns the duration of time between now and the last successful checkinHit
func (c *CheckinHit) Ago() string {
got, _ := timeago.TimeAgoWithTime(utils.Now(), c.CreatedAt)
return got
}
// RecheckCheckinFailure will check if a Service Checkin has been reported yet
func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) {
between := utils.Now().Sub(utils.Now()).Seconds()
if between > float64(c.Interval) {
fmt.Println("rechecking every 15 seconds!")
time.Sleep(15 * time.Second)
guard <- struct{}{}
c.RecheckCheckinFailure(guard)
} else {
fmt.Println("i recovered!!")
}
<-guard
}

View File

@ -1,207 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"errors"
"fmt"
"github.com/go-yaml/yaml"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"io/ioutil"
"os"
)
// ErrorResponse is used for HTTP errors to show to User
type ErrorResponse struct {
Error string
}
// LoadConfigFile will attempt to load the 'config.yml' file in a specific directory
func LoadConfigFile(directory string) (*types.DbConfig, error) {
var configs *types.DbConfig
if os.Getenv("DB_CONN") != "" {
log.Warnln("DB_CONN environment variable was found, waiting for database...")
return LoadUsingEnv()
}
log.Debugln("attempting to read config file at: " + directory + "/config.yml")
file, err := ioutil.ReadFile(directory + "/config.yml")
if err != nil {
return nil, errors.New("config.yml file not found at " + directory + "/config.yml - starting in setup mode")
}
err = yaml.Unmarshal(file, &configs)
if err != nil {
return nil, err
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml")
CoreApp.Config = configs
return configs, err
}
// LoadUsingEnv will attempt to load database configs based on environment variables. If DB_CONN is set if will force this function.
func LoadUsingEnv() (*types.DbConfig, error) {
Configs, err := EnvToConfig()
if err != nil {
return Configs, err
}
CoreApp.Name = os.Getenv("NAME")
if Configs.Domain == "" {
CoreApp.Domain = Configs.LocalIP
} else {
CoreApp.Domain = os.Getenv("DOMAIN")
}
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
err = CoreApp.Connect(true, utils.Directory)
if err != nil {
log.Errorln(err)
return nil, err
}
CoreApp.SaveConfig(Configs)
exists := DbSession.HasTable("core")
if !exists {
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))
CoreApp.DropDatabase()
CoreApp.CreateDatabase()
CoreApp, err = CoreApp.InsertCore(Configs)
if err != nil {
log.Errorln(err)
}
username := os.Getenv("ADMIN_USER")
if username == "" {
username = "admin"
}
password := os.Getenv("ADMIN_PASSWORD")
if password == "" {
password = "admin"
}
admin := ReturnUser(&types.User{
Username: username,
Password: password,
Email: "info@admin.com",
Admin: types.NewNullBool(true),
})
_, err := admin.Create()
SampleData()
return Configs, err
}
return Configs, nil
}
// defaultPort accepts a database type and returns its default port
func defaultPort(db string) int64 {
switch db {
case "mysql":
return 3306
case "postgres":
return 5432
case "mssql":
return 1433
default:
return 0
}
}
// EnvToConfig converts environment variables to a DbConfig variable
func EnvToConfig() (*types.DbConfig, error) {
var err error
if os.Getenv("DB_CONN") == "" {
return nil, errors.New("Missing DB_CONN environment variable")
}
if os.Getenv("DB_CONN") != "sqlite" {
if os.Getenv("DB_HOST") == "" {
return nil, errors.New("Missing DB_HOST environment variable")
}
if os.Getenv("DB_USER") == "" {
return nil, errors.New("Missing DB_USER environment variable")
}
if os.Getenv("DB_PASS") == "" {
return nil, errors.New("Missing DB_PASS environment variable")
}
if os.Getenv("DB_DATABASE") == "" {
return nil, errors.New("Missing DB_DATABASE environment variable")
}
}
port := utils.ToInt(os.Getenv("DB_PORT"))
if port == 0 {
port = defaultPort(os.Getenv("DB_PORT"))
}
name := os.Getenv("NAME")
if name == "" {
name = "Statping"
}
description := os.Getenv("DESCRIPTION")
if description == "" {
description = "Statping Monitoring Sample Data"
}
adminUser := os.Getenv("ADMIN_USER")
if adminUser == "" {
adminUser = "admin"
}
adminPass := os.Getenv("ADMIN_PASS")
if adminPass == "" {
adminPass = "admin"
}
configs := &types.DbConfig{
DbConn: os.Getenv("DB_CONN"),
DbHost: os.Getenv("DB_HOST"),
DbUser: os.Getenv("DB_USER"),
DbPass: os.Getenv("DB_PASS"),
DbData: os.Getenv("DB_DATABASE"),
DbPort: port,
Project: name,
Description: description,
Domain: os.Getenv("DOMAIN"),
Email: "",
Username: adminUser,
Password: adminPass,
Error: nil,
Location: utils.Directory,
SqlFile: os.Getenv("SQL_FILE"),
}
CoreApp.Config = configs
return configs, err
}
// SampleData runs all the sample data for a new Statping installation
func SampleData() error {
if err := InsertSampleData(); err != nil {
log.Errorln(err)
return err
}
if err := InsertSampleHits(); err != nil {
log.Errorln(err)
return err
}
return nil
}
// DeleteConfig will delete the 'config.yml' file
func DeleteConfig() error {
log.Debugln("deleting config yaml file", utils.Directory+"/config.yml")
err := utils.DeleteFile(utils.Directory + "/config.yml")
if err != nil {
log.Errorln(err)
return err
}
return nil
}

View File

@ -1,208 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"errors"
"fmt"
"github.com/hunterlong/statping/core/integrations"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/notifiers"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net"
"os"
"time"
)
type PluginJSON types.PluginJSON
type PluginRepos types.PluginRepos
type Core struct {
*types.Core
}
var (
CoreApp *Core // CoreApp is a global variable that contains many elements
SetupMode bool // SetupMode will be true if Statping does not have a database connection
VERSION string // VERSION is set on build automatically by setting a -ldflag
log = utils.Log.WithField("type", "core")
)
func init() {
CoreApp = NewCore()
}
// NewCore return a new *core.Core struct
func NewCore() *Core {
CoreApp = &Core{&types.Core{
Started: time.Now().UTC(),
},
}
return CoreApp
}
// ToCore will convert *core.Core to *types.Core
func (c *Core) ToCore() *types.Core {
return c.Core
}
// InitApp will initialize Statping
func InitApp() {
SelectCore()
InsertNotifierDB()
CoreApp.SelectAllServices(true)
checkServices()
AttachNotifiers()
CoreApp.Notifications = notifier.AllCommunications
CoreApp.Integrations = integrations.Integrations
go DatabaseMaintence()
SetupMode = false
}
// InsertNotifierDB inject the Statping database instance to the Notifier package
func InsertNotifierDB() error {
if DbSession == nil {
err := CoreApp.Connect(false, utils.Directory)
if err != nil {
return errors.New("database connection has not been created")
}
}
notifier.SetDB(DbSession, CoreApp.Timezone)
return nil
}
// UpdateCore will update the CoreApp variable inside of the 'core' table in database
func UpdateCore(c *Core) (*Core, error) {
db := coreDB().Update(&c)
return c, db.Error
}
// CurrentTime will return the current local time
func (c Core) CurrentTime() string {
t := time.Now().UTC()
current := utils.Timezoner(t, c.Timezone)
ansic := "Monday 03:04:05 PM"
return current.Format(ansic)
}
// Messages will return the current local time
func (c Core) Messages() []*Message {
var message []*Message
messagesDb().Where("service = ?", 0).Limit(10).Find(&message)
return message
}
// UsingAssets will return true if /assets folder is present
func (c Core) UsingAssets() bool {
return source.UsingAssets(utils.Directory)
}
// SassVars opens the file /assets/scss/variables.scss to be edited in Theme
func (c Core) SassVars() string {
if !source.UsingAssets(utils.Directory) {
return ""
}
return source.OpenAsset(utils.Directory, "scss/variables.scss")
}
// BaseSASS is the base design , this opens the file /assets/scss/base.scss to be edited in Theme
func (c Core) BaseSASS() string {
if !source.UsingAssets(utils.Directory) {
return ""
}
return source.OpenAsset(utils.Directory, "scss/base.scss")
}
// MobileSASS is the -webkit responsive custom css designs. This opens the
// file /assets/scss/mobile.scss to be edited in Theme
func (c Core) MobileSASS() string {
if !source.UsingAssets(utils.Directory) {
return ""
}
return source.OpenAsset(utils.Directory, "scss/mobile.scss")
}
// AllOnline will be true if all services are online
func (c Core) AllOnline() bool {
for _, s := range CoreApp.Services {
if !s.Select().Online {
return false
}
}
return true
}
// SelectCore will return the CoreApp global variable and the settings/configs for Statping
func SelectCore() (*Core, error) {
if DbSession == nil {
log.Traceln("database has not been initiated yet.")
return nil, errors.New("database has not been initiated yet.")
}
exists := DbSession.HasTable("core")
if !exists {
log.Errorf("core database has not been setup yet, does not have the 'core' table")
return nil, errors.New("core database has not been setup yet.")
}
db := coreDB().First(&CoreApp)
if db.Error != nil {
return nil, db.Error
}
CoreApp.Version = VERSION
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
return CoreApp, db.Error
}
// GetLocalIP returns the non loopback local IP of the host
func GetLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "http://localhost"
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return fmt.Sprintf("http://%v", ipnet.IP.String())
}
}
}
return "http://localhost"
}
// AttachNotifiers will attach all the notifier's into the system
func AttachNotifiers() error {
return notifier.AddNotifiers(
notifiers.Command,
notifiers.Discorder,
notifiers.Emailer,
notifiers.LineNotify,
notifiers.Mobile,
notifiers.Slacker,
notifiers.Telegram,
notifiers.Twilio,
notifiers.Webhook,
)
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []types.ServiceInterface
// Sort interface for resroting the Services in order
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].(*Service).Order < c[j].(*Service).Order }

View File

@ -1,149 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)
var (
dir string
skipNewDb bool
)
func init() {
dir = utils.Directory
utils.InitLogs()
source.Assets()
skipNewDb = false
SampleHits = 480
}
func TestNewCore(t *testing.T) {
err := TmpRecords("core.db")
require.Nil(t, err)
require.NotNil(t, CoreApp)
}
func TestDbConfig_Save(t *testing.T) {
t.SkipNow()
//if skipNewDb {
// t.SkipNow()
//}
//var err error
//Configs = &DbConfig{
// DbConn: "sqlite",
// Project: "Tester",
// Location: dir,
//}
//Configs, err = Configs.Save()
//assert.Nil(t, err)
//assert.Equal(t, "sqlite", Configs.DbConn)
//assert.NotEmpty(t, Configs.ApiKey)
//assert.NotEmpty(t, Configs.ApiSecret)
}
func TestLoadDbConfig(t *testing.T) {
Configs, err := LoadConfigFile(dir)
assert.Nil(t, err)
assert.Equal(t, "sqlite", Configs.DbConn)
}
func TestDbConnection(t *testing.T) {
err := CoreApp.Connect(false, dir)
assert.Nil(t, err)
}
func TestDropDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
err := CoreApp.DropDatabase()
assert.Nil(t, err)
}
func TestSeedSchemaDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
err := CoreApp.CreateDatabase()
assert.Nil(t, err)
}
func TestMigrateDatabase(t *testing.T) {
t.SkipNow()
err := CoreApp.MigrateDatabase()
assert.Nil(t, err)
}
func TestSeedDatabase(t *testing.T) {
t.SkipNow()
err := InsertLargeSampleData()
assert.Nil(t, err)
}
func TestReLoadDbConfig(t *testing.T) {
err := CoreApp.Connect(false, dir)
assert.Nil(t, err)
assert.Equal(t, "sqlite", CoreApp.Config.DbConn)
}
func TestSelectCore(t *testing.T) {
core, err := SelectCore()
assert.Nil(t, err)
assert.Equal(t, "Statping Sample Data", core.Name)
}
func TestInsertNotifierDB(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
err := InsertNotifierDB()
assert.Nil(t, err)
}
func TestEnvToConfig(t *testing.T) {
os.Setenv("DB_CONN", "sqlite")
os.Setenv("DB_USER", "")
os.Setenv("DB_PASS", "")
os.Setenv("DB_DATABASE", "")
os.Setenv("NAME", "Testing")
os.Setenv("DOMAIN", "http://localhost:8080")
os.Setenv("DESCRIPTION", "Testing Statping")
os.Setenv("ADMIN_USER", "admin")
os.Setenv("ADMIN_PASS", "admin123")
os.Setenv("VERBOSE", "1")
config, err := EnvToConfig()
assert.Nil(t, err)
assert.Equal(t, config.DbConn, "sqlite")
assert.Equal(t, config.Domain, "http://localhost:8080")
assert.Equal(t, config.Description, "Testing Statping")
assert.Equal(t, config.Username, "admin")
assert.Equal(t, config.Password, "admin123")
}
func TestGetLocalIP(t *testing.T) {
ip := GetLocalIP()
assert.Contains(t, ip, "http://")
}

View File

@ -1,417 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/go-yaml/yaml"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"os"
"path/filepath"
"time"
)
var (
// DbSession stores the Statping database session
DbSession *gorm.DB
DbModels []interface{}
)
func init() {
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}}
gorm.NowFunc = func() time.Time {
return time.Now().UTC()
}
}
// DbConfig stores the config.yml file for the statup configuration
type DbConfig types.DbConfig
// failuresDB returns the 'failures' database column
func failuresDB() *gorm.DB {
return DbSession.Model(&types.Failure{})
}
// hitsDB returns the 'hits' database column
func hitsDB() *gorm.DB {
return DbSession.Model(&types.Hit{})
}
// servicesDB returns the 'services' database column
func servicesDB() *gorm.DB {
return DbSession.Model(&types.Service{})
}
// coreDB returns the single column 'core'
func coreDB() *gorm.DB {
return DbSession.Table("core").Model(&CoreApp)
}
// usersDB returns the 'users' database column
func usersDB() *gorm.DB {
return DbSession.Model(&types.User{})
}
// checkinDB returns the Checkin records for a service
func checkinDB() *gorm.DB {
return DbSession.Model(&types.Checkin{})
}
// checkinHitsDB returns the Checkin Hits records for a service
func checkinHitsDB() *gorm.DB {
return DbSession.Model(&types.CheckinHit{})
}
// messagesDb returns the Checkin records for a service
func messagesDb() *gorm.DB {
return DbSession.Model(&types.Message{})
}
// messagesDb returns the Checkin records for a service
func groupsDb() *gorm.DB {
return DbSession.Model(&types.Group{})
}
// incidentsDB returns the 'incidents' database column
func incidentsDB() *gorm.DB {
return DbSession.Model(&types.Incident{})
}
// incidentsUpdatesDB returns the 'incidents updates' database column
func incidentsUpdatesDB() *gorm.DB {
return DbSession.Model(&types.IncidentUpdate{})
}
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return hitsDB().Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
} else {
return hitsDB().Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME_DAY), t2.UTC().Format(types.TIME_DAY))
}
}
// CloseDB will close the database connection if available
func CloseDB() {
if DbSession != nil {
DbSession.DB().Close()
}
}
//// AfterFind for Core will set the timezone
//func (c *Core) AfterFind() (err error) {
// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone)
// c.UpdatedAt = utils.Timezoner(c.UpdatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for Service will set the timezone
//func (s *Service) AfterFind() (err error) {
// s.CreatedAt = utils.Timezoner(s.CreatedAt, CoreApp.Timezone)
// s.UpdatedAt = utils.Timezoner(s.UpdatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for Hit will set the timezone
//func (h *Hit) AfterFind() (err error) {
// h.CreatedAt = utils.Timezoner(h.CreatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for Failure will set the timezone
//func (f *Failure) AfterFind() (err error) {
// f.CreatedAt = utils.Timezoner(f.CreatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for USer will set the timezone
//func (u *User) AfterFind() (err error) {
// u.CreatedAt = utils.Timezoner(u.CreatedAt, CoreApp.Timezone)
// u.UpdatedAt = utils.Timezoner(u.UpdatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for Checkin will set the timezone
//func (c *Checkin) AfterFind() (err error) {
// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone)
// c.UpdatedAt = utils.Timezoner(c.UpdatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for checkinHit will set the timezone
//func (c *CheckinHit) AfterFind() (err error) {
// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone)
// return
//}
//
//// AfterFind for Message will set the timezone
//func (u *Message) AfterFind() (err error) {
// u.CreatedAt = utils.Timezoner(u.CreatedAt, CoreApp.Timezone)
// u.UpdatedAt = utils.Timezoner(u.UpdatedAt, CoreApp.Timezone)
// u.StartOn = utils.Timezoner(u.StartOn.UTC(), CoreApp.Timezone)
// u.EndOn = utils.Timezoner(u.EndOn.UTC(), CoreApp.Timezone)
// return
//}
// InsertCore create the single row for the Core settings in Statping
func (c *Core) InsertCore(db *types.DbConfig) (*Core, error) {
CoreApp = &Core{Core: &types.Core{
Name: db.Project,
Description: db.Description,
ConfigFile: "config.yml",
ApiKey: utils.NewSHA1Hash(9),
ApiSecret: utils.NewSHA1Hash(16),
Domain: db.Domain,
MigrationId: time.Now().Unix(),
Config: db,
}}
query := coreDB().Create(&CoreApp)
return CoreApp, query.Error
}
func findDbFile() string {
if CoreApp.Config.SqlFile != "" {
return CoreApp.Config.SqlFile
}
filename := types.SqliteFilename
err := filepath.Walk(utils.Directory, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if filepath.Ext(path) == ".db" {
filename = info.Name()
}
return nil
})
if err != nil {
log.Error(err)
}
return filename
}
// Connect will attempt to connect to the sqlite, postgres, or mysql database
func (c *Core) Connect(retry bool, location string) error {
postgresSSL := os.Getenv("POSTGRES_SSLMODE")
if DbSession != nil {
return nil
}
var conn, dbType string
var err error
dbType = CoreApp.Config.DbConn
if CoreApp.Config.DbPort == 0 {
CoreApp.Config.DbPort = defaultPort(dbType)
}
switch dbType {
case "sqlite":
sqlFilename := findDbFile()
conn = sqlFilename
log.Infof("SQL database file at: %v/%v", utils.Directory, conn)
dbType = "sqlite3"
case "mysql":
host := fmt.Sprintf("%v:%v", CoreApp.Config.DbHost, CoreApp.Config.DbPort)
conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", CoreApp.Config.DbUser, CoreApp.Config.DbPass, host, CoreApp.Config.DbData)
case "postgres":
sslMode := "disable"
if postgresSSL != "" {
sslMode = postgresSSL
}
conn = fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=%v", CoreApp.Config.DbHost, CoreApp.Config.DbPort, CoreApp.Config.DbUser, CoreApp.Config.DbData, CoreApp.Config.DbPass, sslMode)
case "mssql":
host := fmt.Sprintf("%v:%v", CoreApp.Config.DbHost, CoreApp.Config.DbPort)
conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", CoreApp.Config.DbUser, CoreApp.Config.DbPass, host, CoreApp.Config.DbData)
}
log.WithFields(utils.ToFields(c, conn)).Debugln("attempting to connect to database")
dbSession, err := gorm.Open(dbType, conn)
if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %v", err))
if retry {
log.Errorln(fmt.Sprintf("Database connection to '%v' is not available, trying again in 5 seconds...", CoreApp.Config.DbHost))
return c.waitForDb()
} else {
return err
}
}
log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database")
dbSession.DB().SetMaxOpenConns(5)
dbSession.DB().SetMaxIdleConns(5)
dbSession.DB().SetConnMaxLifetime(1 * time.Minute)
if dbSession.DB().Ping() == nil {
DbSession = dbSession
if utils.VerboseMode >= 4 {
DbSession.LogMode(true).Debug().SetLogger(log)
}
log.Infoln(fmt.Sprintf("Database %v connection was successful.", dbType))
}
return err
}
// waitForDb will sleep for 5 seconds and try to connect to the database again
func (c *Core) waitForDb() error {
time.Sleep(5 * time.Second)
return c.Connect(true, utils.Directory)
}
// DatabaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func DatabaseMaintence() {
for range time.Tick(60 * time.Minute) {
log.Infoln("Checking for database records older than 3 months...")
since := time.Now().AddDate(0, -3, 0).UTC()
DeleteAllSince("failures", since)
DeleteAllSince("hits", since)
}
}
// DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, date.Format("2006-01-02"))
db := DbSession.Exec(sql)
if db.Error != nil {
log.Warnln(db.Error)
}
}
// Update will save the config.yml file
func (c *Core) UpdateConfig() error {
var err error
config, err := os.Create(utils.Directory + "/config.yml")
if err != nil {
log.Errorln(err)
return err
}
data, err := yaml.Marshal(c.Config)
if err != nil {
log.Errorln(err)
return err
}
config.WriteString(string(data))
config.Close()
return err
}
// Save will initially create the config.yml file
func (c *Core) SaveConfig(configs *types.DbConfig) (*types.DbConfig, error) {
config, err := os.Create(utils.Directory + "/config.yml")
if err != nil {
log.Errorln(err)
return nil, err
}
defer config.Close()
log.WithFields(utils.ToFields(configs)).Debugln("saving config file at: " + utils.Directory + "/config.yml")
c.Config = configs
c.Config.ApiKey = utils.NewSHA1Hash(16)
c.Config.ApiSecret = utils.NewSHA1Hash(16)
data, err := yaml.Marshal(configs)
if err != nil {
log.Errorln(err)
return nil, err
}
config.WriteString(string(data))
log.WithFields(utils.ToFields(configs)).Infoln("saved config file at: " + utils.Directory + "/config.yml")
return c.Config, err
}
// CreateCore will initialize the global variable 'CoreApp". This global variable contains most of Statping app.
func (c *Core) CreateCore() *Core {
newCore := &types.Core{
Name: c.Name,
Description: c.Description,
ConfigFile: utils.Directory + "/config.yml",
ApiKey: c.ApiKey,
ApiSecret: c.ApiSecret,
Domain: c.Domain,
MigrationId: time.Now().Unix(),
}
db := coreDB().Create(&newCore)
if db.Error == nil {
CoreApp = &Core{Core: newCore}
}
CoreApp, err := SelectCore()
if err != nil {
log.Errorln(err)
}
return CoreApp
}
// DropDatabase will DROP each table Statping created
func (c *Core) DropDatabase() error {
log.Infoln("Dropping Database Tables...")
err := DbSession.DropTableIfExists("checkins")
err = DbSession.DropTableIfExists("checkin_hits")
err = DbSession.DropTableIfExists("notifications")
err = DbSession.DropTableIfExists("core")
err = DbSession.DropTableIfExists("failures")
err = DbSession.DropTableIfExists("hits")
err = DbSession.DropTableIfExists("services")
err = DbSession.DropTableIfExists("users")
err = DbSession.DropTableIfExists("messages")
err = DbSession.DropTableIfExists("incidents")
err = DbSession.DropTableIfExists("incident_updates")
return err.Error
}
// CreateDatabase will CREATE TABLES for each of the Statping elements
func (c *Core) CreateDatabase() error {
var err error
log.Infoln("Creating Database Tables...")
for _, table := range DbModels {
if err := DbSession.CreateTable(table); err.Error != nil {
return err.Error
}
}
if err := DbSession.Table("core").CreateTable(&types.Core{}); err.Error != nil {
return err.Error
}
log.Infoln("Statping Database Created")
return err
}
// MigrateDatabase will migrate the database structure to current version.
// This function will NOT remove previous records, tables or columns from the database.
// If this function has an issue, it will ROLLBACK to the previous state.
func (c *Core) MigrateDatabase() error {
log.Infoln("Migrating Database Tables...")
tx := DbSession.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
log.Errorln(tx.Error)
return tx.Error
}
for _, table := range DbModels {
tx = tx.AutoMigrate(table)
}
if err := tx.Table("core").AutoMigrate(&types.Core{}); err.Error != nil {
tx.Rollback()
log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error))
return tx.Error
}
log.Infoln("Statping Database Migrated")
return tx.Commit().Error
}

View File

@ -1,6 +0,0 @@
// Package core contains the main functionality of Statping. This includes everything for
// Services, Hits, Failures, Users, service checking mechanisms, databases, and notifiers
// in the notifier package
//
// More info on: https://github.com/hunterlong/statping
package core

View File

@ -1,82 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"bytes"
"encoding/json"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"html/template"
)
// ExportChartsJs renders the charts for the index page
func ExportChartsJs() string {
render, err := source.JsBox.String("charts.js")
if err != nil {
log.Errorln(err)
}
t := template.New("charts")
t.Funcs(template.FuncMap{
"safe": func(html string) template.HTML {
return template.HTML(html)
},
})
t.Parse(render)
var tpl bytes.Buffer
if err := t.Execute(&tpl, CoreApp.Services); err != nil {
log.Errorln(err)
}
result := tpl.String()
return result
}
type ExportData struct {
Core *types.Core `json:"core"`
Services []types.ServiceInterface `json:"services"`
Messages []*Message `json:"messages"`
Checkins []*Checkin `json:"checkins"`
Users []*User `json:"users"`
Groups []*Group `json:"groups"`
Notifiers []types.AllNotifiers `json:"notifiers"`
}
// ExportSettings will export a JSON file containing all of the settings below:
// - Core
// - Notifiers
// - Checkins
// - Users
// - Services
// - Groups
// - Messages
func ExportSettings() ([]byte, error) {
users, err := SelectAllUsers()
messages, err := SelectMessages()
if err != nil {
return nil, err
}
data := ExportData{
Core: CoreApp.Core,
Notifiers: CoreApp.Notifications,
Checkins: AllCheckins(),
Users: users,
Services: CoreApp.Services,
Groups: SelectGroups(true, true),
Messages: messages,
}
export, err := json.Marshal(data)
return export, err
}

View File

@ -1,215 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types"
"sort"
"strings"
"time"
)
type Failure struct {
*types.Failure
}
const (
limitedFailures = 32
limitedHits = 32
)
// CreateFailure will create a new Failure record for a service
func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
f.Service = s.Id
row := failuresDB().Create(f)
if row.Error != nil {
log.Errorln(row.Error)
return 0, row.Error
}
sort.Sort(types.FailSort(s.Failures))
//s.Failures = append(s.Failures, f)
if len(s.Failures) > limitedFailures {
s.Failures = s.Failures[1:]
}
return f.Id, row.Error
}
// AllFailures will return all failures attached to a service
func (s *Service) AllFailures() []*Failure {
var fails []*Failure
col := failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc")
err := col.Find(&fails)
if err.Error != nil {
log.Errorln(fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
return nil
}
return fails
}
// DeleteFailures will delete all failures for a service
func (s *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
if err.Error != nil {
log.Errorln(fmt.Sprintf("failed to delete all failures: %v", err))
}
s.Failures = nil
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedFailures(amount int64) []*Failure {
var failArr []*Failure
failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedCheckinFailures(amount int64) []*Failure {
var failArr []*Failure
failuresDB().Where("service = ?", s.Id).Where("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
}
// Ago returns a human readable timestamp for a Failure
func (f *Failure) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now().UTC(), f.CreatedAt)
return got
}
// Select returns a *types.Failure
func (f *Failure) Select() *types.Failure {
return f.Failure
}
// Delete will remove a Failure record from the database
func (f *Failure) Delete() error {
db := failuresDB().Delete(f)
return db.Error
}
// Count24HFailures returns the amount of failures for a service within the last 24 hours
func (c *Core) Count24HFailures() uint64 {
var count uint64
for _, s := range CoreApp.Services {
service := s.(*Service)
fails, _ := service.TotalFailures24()
count += fails
}
return count
}
// CountFailures returns the total count of failures for all services
func CountFailures() uint64 {
var count uint64
err := failuresDB().Count(&count)
if err.Error != nil {
log.Warnln(err.Error)
return 0
}
return count
}
// TotalFailuresOnDate returns the total amount of failures for a service on a specific time/date
func (s *Service) TotalFailuresOnDate(ago time.Time) (uint64, error) {
var count uint64
date := ago.UTC().Format("2006-01-02 00:00:00")
dateend := ago.UTC().Format("2006-01-02") + " 23:59:59"
rows := failuresDB().Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, date, dateend).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error
}
// TotalFailures24 returns the amount of failures for a service within the last 24 hours
func (s *Service) TotalFailures24() (uint64, error) {
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.TotalFailuresSince(ago)
}
// TotalFailures returns the total amount of failures for a service
func (s *Service) TotalFailures() (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ?", s.Id)
err := rows.Count(&count)
return count, err.Error
}
// FailuresDaysAgo returns the amount of failures since days ago
func (s *Service) FailuresDaysAgo(days int) uint64 {
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
count, _ := s.TotalFailuresSince(ago)
return count
}
// TotalFailuresSince returns the total amount of failures for a service since a specific time/date
func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error
}
// ParseError returns a human readable error for a Failure
func (f *Failure) ParseError() string {
if f.Method == "checkin" {
return fmt.Sprintf("Checkin is Offline")
}
err := strings.Contains(f.Issue, "connection reset by peer")
if err {
return fmt.Sprintf("Connection Reset")
}
err = strings.Contains(f.Issue, "operation timed out")
if err {
return fmt.Sprintf("HTTP Request Timed Out")
}
err = strings.Contains(f.Issue, "x509: certificate is valid")
if err {
return fmt.Sprintf("SSL Certificate invalid")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "no such host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "HTTP Status Code")
if err {
return fmt.Sprintf("Incorrect HTTP Status Code")
}
err = strings.Contains(f.Issue, "connection refused")
if err {
return fmt.Sprintf("Connection Failed")
}
err = strings.Contains(f.Issue, "can't assign requested address")
if err {
return fmt.Sprintf("Unable to Request Address")
}
err = strings.Contains(f.Issue, "no route to host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "i/o timeout")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while reading body")
if err {
return fmt.Sprintf("Timed Out on Response Body")
}
return f.Issue
}

View File

@ -1,101 +0,0 @@
package core
import (
"github.com/hunterlong/statping/types"
"sort"
"time"
)
type Group struct {
*types.Group
}
// Delete will remove a group
func (g *Group) Delete() error {
for _, s := range g.Services() {
s.GroupId = 0
s.Update(false)
}
err := groupsDb().Delete(g)
return err.Error
}
// Create will create a group and insert it into the database
func (g *Group) Create() (int64, error) {
g.CreatedAt = time.Now().UTC()
db := groupsDb().Create(g)
return g.Id, db.Error
}
// Update will update a group
func (g *Group) Update() (int64, error) {
g.UpdatedAt = time.Now().UTC()
db := groupsDb().Update(g)
return g.Id, db.Error
}
// Services returns all services belonging to a group
func (g *Group) Services() []*Service {
var services []*Service
for _, s := range Services() {
if s.Select().GroupId == int(g.Id) {
services = append(services, s.(*Service))
}
}
return services
}
// VisibleServices returns all services based on authentication
func (g *Group) VisibleServices(auth bool) []*Service {
var services []*Service
for _, g := range g.Services() {
if !g.Public.Bool {
if auth {
services = append(services, g)
}
} else {
services = append(services, g)
}
}
return services
}
// SelectGroups returns all groups
func SelectGroups(includeAll bool, auth bool) []*Group {
var groups []*Group
var validGroups []*Group
groupsDb().Find(&groups).Order("order_id desc")
for _, g := range groups {
if !g.Public.Bool {
if auth {
validGroups = append(validGroups, g)
}
} else {
validGroups = append(validGroups, g)
}
}
sort.Sort(GroupOrder(validGroups))
if includeAll {
emptyGroup := &Group{&types.Group{Id: 0, Public: types.NewNullBool(true)}}
validGroups = append(validGroups, emptyGroup)
}
return validGroups
}
// SelectGroup returns a *core.Group
func SelectGroup(id int64) *Group {
for _, g := range SelectGroups(true, true) {
if g.Id == id {
return g
}
}
return nil
}
// GroupOrder will reorder the groups based on 'order_id' (Order)
type GroupOrder []*Group
// Sort interface for resorting the Groups in order
func (c GroupOrder) Len() int { return len(c) }
func (c GroupOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c GroupOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }

View File

@ -1,91 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"github.com/hunterlong/statping/types"
"time"
)
type Hit struct {
*types.Hit
}
// CreateHit will create a new 'hit' record in the database for a successful/online service
func (s *Service) CreateHit(h *types.Hit) (int64, error) {
db := hitsDB().Create(&h)
if db.Error != nil {
log.Errorln(db.Error)
return 0, db.Error
}
return h.Id, db.Error
}
// CountHits returns a int64 for all hits for a service
func (s *Service) CountHits() (int64, error) {
var hits int64
col := hitsDB().Where("service = ?", s.Id)
err := col.Count(&hits)
return hits, err.Error
}
// Hits returns all successful hits for a service
func (s *Service) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc")
err := col.Find(&hits)
return hits, err.Error
}
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(amount)
err := col.Find(&hits)
return reverseHits(hits), err.Error
}
// reverseHits will reverse the service's hit slice
func reverseHits(input []*types.Hit) []*types.Hit {
if len(input) == 0 {
return input
}
return append(reverseHits(input[1:]), input[0])
}
// TotalHits returns the total amount of successful hits a service has
func (s *Service) TotalHits() (uint64, error) {
var count uint64
col := hitsDB().Where("service = ?", s.Id).Count(&count)
return count, col.Error
}
// TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
return count, rows.Error
}
// Sum returns the added value Latency for all of the services successful hits.
func (s *Service) Sum() float64 {
var sum float64
rows, _ := hitsDB().Where("service = ?", s.Id).Select("sum(latency) as total").Rows()
for rows.Next() {
rows.Scan(&sum)
}
return sum
}

View File

@ -1,73 +0,0 @@
package core
import (
"github.com/hunterlong/statping/types"
"time"
)
type Incident struct {
*types.Incident
}
type IncidentUpdate struct {
*types.IncidentUpdate
}
// AllIncidents will return all incidents and updates recorded
func AllIncidents() []*Incident {
var incidents []*Incident
incidentsDB().Find(&incidents).Order("id desc")
for _, i := range incidents {
var updates []*types.IncidentUpdate
incidentsUpdatesDB().Find(&updates).Order("id desc")
i.Updates = updates
}
return incidents
}
// Incidents will return the all incidents for a service
func (s *Service) Incidents() []*Incident {
var incidentArr []*Incident
incidentsDB().Where("service = ?", s.Id).Order("id desc").Find(&incidentArr)
return incidentArr
}
// AllUpdates will return the all updates for an incident
func (i *Incident) AllUpdates() []*IncidentUpdate {
var updatesArr []*IncidentUpdate
incidentsUpdatesDB().Where("incident = ?", i.Id).Order("id desc").Find(&updatesArr)
return updatesArr
}
// Delete will remove a incident
func (i *Incident) Delete() error {
err := incidentsDB().Delete(i)
return err.Error
}
// Create will create a incident and insert it into the database
func (i *Incident) Create() (int64, error) {
i.CreatedAt = time.Now().UTC()
db := incidentsDB().Create(i)
return i.Id, db.Error
}
// Update will update a incident
func (i *Incident) Update() (int64, error) {
i.UpdatedAt = time.Now().UTC()
db := incidentsDB().Update(i)
return i.Id, db.Error
}
// Delete will remove a incident update
func (i *IncidentUpdate) Delete() error {
err := incidentsUpdatesDB().Delete(i)
return err.Error
}
// Create will create a incident update and insert it into the database
func (i *IncidentUpdate) Create() (int64, error) {
i.CreatedAt = time.Now().UTC()
db := incidentsUpdatesDB().Create(i)
return i.Id, db.Error
}

View File

@ -1,130 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integrations
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"strconv"
"time"
)
const requiredSize = 17
type csvIntegration struct {
*types.Integration
}
var csvIntegrator = &csvIntegration{&types.Integration{
ShortName: "csv",
Name: "CSV File",
Icon: "<i class=\"fas fa-file-csv\"></i>",
Description: "Import multiple services from a CSV file. Please have your CSV file formatted with the correct amount of columns based on the <a href=\"https://raw.githubusercontent.com/hunterlong/statping/master/source/tmpl/bulk_import.csv\">example file on Github</a>.",
Fields: []*types.IntegrationField{
{
Name: "input",
Type: "textarea",
Description: "",
},
},
}}
var csvData [][]string
func (t *csvIntegration) Get() *types.Integration {
return t.Integration
}
func (t *csvIntegration) List() ([]*types.Service, error) {
data := Value(t, "input").(string)
buf := bytes.NewReader([]byte(data))
r := csv.NewReader(buf)
records, err := r.ReadAll()
if err != nil {
return nil, err
}
var services []*types.Service
for k, v := range records[1:] {
s, err := commaToService(v)
if err != nil {
log.Errorf("error on line %v: %v", k, err)
continue
}
services = append(services, s)
}
return services, nil
}
// commaToService will convert a CSV comma delimited string slice to a Service type
// this function is used for the bulk import services feature
func commaToService(s []string) (*types.Service, error) {
if len(s) != requiredSize {
err := fmt.Errorf("file has %v columns of data, not the expected amount of %v columns for a service", len(s), requiredSize)
return nil, err
}
interval, err := time.ParseDuration(s[4])
if err != nil {
return nil, errors.New("could not parse internal duration: " + s[4])
}
timeout, err := time.ParseDuration(s[9])
if err != nil {
return nil, errors.New("could not parse timeout duration: " + s[9])
}
allowNotifications, err := strconv.ParseBool(s[11])
if err != nil {
return nil, errors.New("could not parse allow notifications boolean: " + s[11])
}
public, err := strconv.ParseBool(s[12])
if err != nil {
return nil, errors.New("could not parse public boolean: " + s[12])
}
verifySsl, err := strconv.ParseBool(s[16])
if err != nil {
return nil, errors.New("could not parse verifiy SSL boolean: " + s[16])
}
newService := &types.Service{
Name: s[0],
Domain: s[1],
Expected: types.NewNullString(s[2]),
ExpectedStatus: int(utils.ToInt(s[3])),
Interval: int(utils.ToInt(interval.Seconds())),
Type: s[5],
Method: s[6],
PostData: types.NewNullString(s[7]),
Port: int(utils.ToInt(s[8])),
Timeout: int(utils.ToInt(timeout.Seconds())),
AllowNotifications: types.NewNullBool(allowNotifications),
Public: types.NewNullBool(public),
GroupId: int(utils.ToInt(s[13])),
Headers: types.NewNullString(s[14]),
Permalink: types.NewNullString(s[15]),
VerifySSL: types.NewNullBool(verifySsl),
}
return newService, nil
}

View File

@ -1,43 +0,0 @@
package integrations
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"testing"
)
func TestCsvFileIntegration(t *testing.T) {
data, err := ioutil.ReadFile("../../source/tmpl/bulk_import.csv")
require.Nil(t, err)
t.Run("Set Field Value", func(t *testing.T) {
formPost := map[string][]string{}
formPost["input"] = []string{string(data)}
_, err = SetFields(csvIntegrator, formPost)
require.Nil(t, err)
})
t.Run("Get Field Value", func(t *testing.T) {
value := Value(csvIntegrator, "input").(string)
assert.Equal(t, string(data), value)
})
t.Run("List Services from CSV File", func(t *testing.T) {
services, err := csvIntegrator.List()
require.Nil(t, err)
assert.Equal(t, 10, len(services))
})
t.Run("Confirm Services from CSV File", func(t *testing.T) {
services, err := csvIntegrator.List()
require.Nil(t, err)
assert.Equal(t, "Bulk Upload", services[0].Name)
assert.Equal(t, "http://google.com", services[0].Domain)
assert.Equal(t, 60, services[0].Interval)
for _, s := range services {
t.Log(s)
}
})
}

View File

@ -1,103 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integrations
import (
"context"
dTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/hunterlong/statping/types"
"os"
)
type dockerIntegration struct {
*types.Integration
}
var dockerIntegrator = &dockerIntegration{&types.Integration{
ShortName: "docker",
Name: "Docker",
Icon: "<i class=\"fab fa-docker\"></i>",
Description: `Import multiple services from Docker by attaching the unix socket to Statping.
You can also do this in Docker by setting <u>-v /var/run/docker.sock:/var/run/docker.sock</u> in the Statping Docker container.
All of the containers with open TCP/UDP ports will be listed for you to choose which services you want to add. If you running Statping inside of a container,
this container must be attached to all networks you want to communicate with.`,
Fields: []*types.IntegrationField{
{
Name: "path",
Description: "The absolute path to the Docker unix socket",
Type: "text",
Value: client.DefaultDockerHost,
},
{
Name: "version",
Description: "Version number of Docker server",
Type: "text",
Value: client.DefaultVersion,
},
},
}}
var cli *client.Client
func (t *dockerIntegration) Get() *types.Integration {
return t.Integration
}
func (t *dockerIntegration) List() ([]*types.Service, error) {
var err error
path := Value(t, "path").(string)
version := Value(t, "version").(string)
os.Setenv("DOCKER_HOST", path)
os.Setenv("DOCKER_VERSION", version)
cli, err = client.NewEnvClient()
if err != nil {
return nil, err
}
defer cli.Close()
var services []*types.Service
containers, err := cli.ContainerList(context.Background(), dTypes.ContainerListOptions{})
if err != nil {
return nil, err
}
for _, container := range containers {
if container.State != "running" {
continue
}
for _, v := range container.Ports {
if v.IP == "" {
continue
}
service := &types.Service{
Name: container.Names[0][1:],
Domain: v.IP,
Type: v.Type,
Port: int(v.PublicPort),
Interval: 60,
Timeout: 2,
}
services = append(services, service)
}
}
return services, nil
}

View File

@ -1,40 +0,0 @@
package integrations
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestDockerIntegration(t *testing.T) {
t.Run("Set Field Value", func(t *testing.T) {
formPost := map[string][]string{}
formPost["path"] = []string{"unix:///var/run/docker.sock"}
formPost["version"] = []string{"1.25"}
_, err := SetFields(csvIntegrator, formPost)
require.Nil(t, err)
})
t.Run("Get Field Value", func(t *testing.T) {
path := Value(dockerIntegrator, "path").(string)
version := Value(dockerIntegrator, "version").(string)
assert.Equal(t, "unix:///var/run/docker.sock", path)
assert.Equal(t, "1.25", version)
})
t.Run("List Services from Docker", func(t *testing.T) {
services, err := dockerIntegrator.List()
require.Nil(t, err)
assert.Equal(t, 0, len(services))
})
t.Run("Confirm Services from Docker", func(t *testing.T) {
services, err := dockerIntegrator.List()
require.Nil(t, err)
for _, s := range services {
t.Log(s)
}
})
}

View File

@ -1,64 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integrations
import (
"errors"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
)
var (
Integrations []types.Integrator
log = utils.Log.WithField("type", "integration")
)
func init() {
Integrations = append(Integrations,
csvIntegrator,
dockerIntegrator,
traefikIntegrator,
)
}
func Value(intg types.Integrator, fieldName string) interface{} {
for _, v := range intg.Get().Fields {
if fieldName == v.Name {
return v.Value
}
}
return nil
}
func SetFields(intg types.Integrator, data map[string][]string) (*types.Integration, error) {
i := intg.Get()
for _, v := range i.Fields {
if data[v.Name] != nil {
v.Value = data[v.Name][0]
}
}
return i, nil
}
func Find(name string) (types.Integrator, error) {
for _, i := range Integrations {
obj := i.Get()
if obj.ShortName == name {
return i, nil
}
}
return nil, errors.New(name + " not found")
}

View File

@ -1,15 +0,0 @@
package integrations
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestIntegrations(t *testing.T) {
t.Run("Collect Integrations", func(t *testing.T) {
amount := len(Integrations)
assert.Equal(t, 3, amount)
})
}

View File

@ -1,126 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package integrations
import (
"encoding/json"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/url"
"time"
)
type traefikIntegration struct {
*types.Integration
}
var traefikIntegrator = &traefikIntegration{&types.Integration{
ShortName: "traefik",
Name: "Traefik",
Icon: "<i class=\"fas fa-network-wired\"></i>",
Description: ``,
Fields: []*types.IntegrationField{
{
Name: "endpoint",
Description: "The URL for the traefik API Endpoint",
Type: "text",
Value: "http://localhost:8080",
},
{
Name: "username",
Description: "Username for HTTP Basic Authentication",
Type: "text",
},
{
Name: "password",
Description: "Password for HTTP Basic Authentication",
Type: "password",
},
},
}}
func (t *traefikIntegration) Get() *types.Integration {
return t.Integration
}
func (t *traefikIntegration) List() ([]*types.Service, error) {
var err error
var services []*types.Service
endpoint := Value(t, "endpoint").(string)
httpServices, err := fetchMethod(endpoint, "http")
if err != nil {
return nil, err
}
services = append(services, httpServices...)
tcpServices, err := fetchMethod(endpoint, "tcp")
if err != nil {
return nil, err
}
services = append(services, tcpServices...)
return services, err
}
func fetchMethod(endpoint, method string) ([]*types.Service, error) {
var traefikServices []traefikService
var services []*types.Service
d, _, err := utils.HttpRequest(endpoint+"/api/"+method+"/services", "GET", nil, []string{}, nil, 10*time.Second, false)
if err != nil {
return nil, err
}
if err := json.Unmarshal(d, &traefikServices); err != nil {
return nil, err
}
for _, s := range traefikServices {
log.Infoln(s)
for _, l := range s.LoadBalancer.Servers {
url, err := url.Parse(l.URL)
if err != nil {
return nil, err
}
service := &types.Service{
Name: s.Name,
Domain: url.Hostname(),
Port: int(utils.ToInt(url.Port())),
Type: method,
Interval: 60,
Timeout: 2,
}
services = append(services, service)
}
}
return services, err
}
type traefikService struct {
Status string `json:"status"`
UsedBy []string `json:"usedBy"`
Name string `json:"name"`
Provider string `json:"provider"`
LoadBalancer struct {
Servers []struct {
URL string `json:"url"`
} `json:"servers"`
PassHostHeader bool `json:"passHostHeader"`
} `json:"loadBalancer,omitempty"`
}

View File

@ -1,27 +0,0 @@
package integrations
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestTraefikIntegration(t *testing.T) {
t.Run("List Services from Traefik", func(t *testing.T) {
t.SkipNow()
services, err := traefikIntegrator.List()
require.Nil(t, err)
assert.NotEqual(t, 0, len(services))
})
t.Run("Confirm Services from Traefik", func(t *testing.T) {
t.SkipNow()
services, err := traefikIntegrator.List()
require.Nil(t, err)
for _, s := range services {
t.Log(s)
}
})
}

View File

@ -1,86 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statping/types"
"time"
)
type Message struct {
*types.Message
}
// SelectServiceMessages returns all messages for a service
func SelectServiceMessages(id int64) []*Message {
var message []*Message
messagesDb().Where("service = ?", id).Limit(10).Find(&message)
return message
}
// ReturnMessage will convert *types.Message to *core.Message
func ReturnMessage(m *types.Message) *Message {
return &Message{m}
}
// SelectMessages returns all messages
func SelectMessages() ([]*Message, error) {
var messages []*Message
db := messagesDb().Find(&messages).Order("id desc")
return messages, db.Error
}
// SelectMessage returns a Message based on the ID passed
func SelectMessage(id int64) (*Message, error) {
var message Message
db := messagesDb().Where("id = ?", id).Find(&message)
return &message, db.Error
}
func (m *Message) Service() *Service {
if m.ServiceId == 0 {
return nil
}
return SelectService(m.ServiceId)
}
// Create will create a Message and insert it into the database
func (m *Message) Create() (int64, error) {
m.CreatedAt = time.Now().UTC()
db := messagesDb().Create(m)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error))
return 0, db.Error
}
return m.Id, nil
}
// Delete will delete a Message from database
func (m *Message) Delete() error {
db := messagesDb().Delete(m)
return db.Error
}
// Update will update a Message in the database
func (m *Message) Update() (*Message, error) {
db := messagesDb().Update(m)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error))
return nil, db.Error
}
return m, nil
}

View File

@ -1,45 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import (
"fmt"
"strings"
)
var (
allowedVars = []string{"host", "username", "password", "port", "api_key", "api_secret", "var1", "var2"}
)
func checkNotifierForm(n Notifier) error {
notifier := n.Select()
for _, f := range notifier.Form {
contains := contains(f.DbField, allowedVars)
if !contains {
return fmt.Errorf("the DbField '%v' is not allowed, allowed vars: %v", f.DbField, allowedVars)
}
}
return nil
}
func contains(s string, arr []string) bool {
for _, v := range arr {
if strings.ToLower(s) == v {
return true
}
}
return false
}

View File

@ -1,120 +0,0 @@
// Package notifier contains the main functionality for the Statping Notification system
//
// Example Notifier
//
// Below is an example of a Notifier with multiple Form values to custom your inputs. Place your notifier go file
// into the /notifiers/ directory and follow the example below.
//
// type ExampleNotifier struct {
// *Notification
// }
//
// var example = &ExampleNotifier{&Notification{
// Method: "example",
// Title: "Example Notifier",
// Description: "This is an example of a notifier for Statping!",
// Author: "Hunter Long",
// AuthorUrl: "https://github.com/hunterlong",
// Delay: time.Duration(3 * time.Second),
// Limits: 7,
// Form: []NotificationForm{{
// Type: "text",
// Title: "Host",
// Placeholder: "Insert your Host here.",
// DbField: "host",
// SmallText: "this is where you would put the host",
// }, {
// Type: "text",
// Title: "Username",
// Placeholder: "Insert your Username here.",
// DbField: "username",
// }, {
// Type: "password",
// Title: "Password",
// Placeholder: "Insert your Password here.",
// DbField: "password",
// }, {
// Type: "number",
// Title: "Port",
// Placeholder: "Insert your Port here.",
// DbField: "port",
// }, {
// Type: "text",
// Title: "API Key",
// Placeholder: "Insert your API Key here",
// DbField: "api_key",
// }, {
// Type: "text",
// Title: "API Secret",
// Placeholder: "Insert your API Secret here",
// DbField: "api_secret",
// }, {
// Type: "text",
// Title: "Var 1",
// Placeholder: "Insert your Var1 here",
// DbField: "var1",
// }, {
// Type: "text",
// Title: "Var2",
// Placeholder: "Var2 goes here",
// DbField: "var2",
// }},
// }}
//
// Load the Notifier
//
// Include the init() function with AddNotifier and your notification struct. This is ran on start of Statping
// and will automatically create a new row in the database so the end user can save their own values.
//
// func init() {
// AddNotifier(example)
// }
//
// Required Methods for Notifier Interface
//
// Below are the required methods to have your notifier implement the Notifier interface. The Send method
// will be where you would include the logic for your notification.
//
// // REQUIRED
// func (n *ExampleNotifier) Send(msg interface{}) error {
// message := msg.(string)
// fmt.Printf("i received this string: %v\n", message)
// return nil
// }
//
// // REQUIRED
// func (n *ExampleNotifier) Select() *Notification {
// return n.Notification
// }
//
// // REQUIRED
// func (n *ExampleNotifier) OnSave() error {
// msg := fmt.Sprintf("received on save trigger")
// n.AddQueue(msg)
// return errors.New("onsave triggered")
// }
//
// Basic Events for Notifier
//
// You must include OnSuccess and OnFailure methods for your notifier. Anytime a service is online or offline
// these methods will be ran with the service corresponding to it.
//
// // REQUIRED - BASIC EVENT
// func (n *ExampleNotifier) OnSuccess(s *types.Service) {
// msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name)
// n.AddQueue(msg)
// }
//
// // REQUIRED - BASIC EVENT
// func (n *ExampleNotifier) OnFailure(s *types.Service, f *types.Failure) {
// msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name)
// n.AddQueue(msg)
// }
//
// Additional Events
//
// You can implement your notifier to different types of events that are triggered. Checkout the wiki to
// see more details and examples of how to build your own notifier.
//
// More info on: https://github.com/hunterlong/statping/wiki/Notifiers
package notifier

View File

@ -1,191 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
)
// OnSave will trigger a notifier when it has been saved - Notifier interface
func OnSave(method string) {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
notifier := comm.(Notifier)
if notifier.Select().Method == method {
notifier.OnSave()
}
}
}
}
// OnFailure will be triggered when a service is failing - BasicEvents interface
func OnFailure(s *types.Service, f *types.Failure) {
if !s.AllowNotifications.Bool {
return
}
// check if User wants to receive every Status Change
if s.UpdateNotify {
// send only if User hasn't been already notified about the Downtime
if !s.UserNotified {
s.UserNotified = true
goto sendMessages
} else {
return
}
}
sendMessages:
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && (s.Online || inLimits(comm)) {
notifier := comm.(Notifier).Select()
log.
WithField("trigger", "OnFailure").
WithFields(utils.ToFields(notifier, s)).Infoln(fmt.Sprintf("Sending [OnFailure] '%v' notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnFailure(s, f)
}
}
}
// OnSuccess will be triggered when a service is successful - BasicEvents interface
func OnSuccess(s *types.Service) {
if !s.AllowNotifications.Bool {
return
}
// check if User wants to receive every Status Change
if s.UpdateNotify && s.UserNotified {
s.UserNotified = false
}
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && (!s.Online || inLimits(comm)) {
notifier := comm.(Notifier).Select()
log.
WithField("trigger", "OnSuccess").
WithFields(utils.ToFields(notifier, s)).Infoln(fmt.Sprintf("Sending [OnSuccess] '%v' notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnSuccess(s)
}
}
}
// OnNewService is triggered when a new service is created - ServiceEvents interface
func OnNewService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.
WithField("trigger", "OnNewService").
Infoln(fmt.Sprintf("Sending new service notification for service %v", s.Name))
comm.(ServiceEvents).OnNewService(s)
}
}
}
// OnUpdatedService is triggered when a service is updated - ServiceEvents interface
func OnUpdatedService(s *types.Service) {
if !s.AllowNotifications.Bool {
return
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated service notification for service %v", s.Name))
comm.(ServiceEvents).OnUpdatedService(s)
}
}
}
// OnDeletedService is triggered when a service is deleted - ServiceEvents interface
func OnDeletedService(s *types.Service) {
if !s.AllowNotifications.Bool {
return
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending deleted service notification for service %v", s.Name))
comm.(ServiceEvents).OnDeletedService(s)
}
}
}
// OnNewUser is triggered when a new user is created - UserEvents interface
func OnNewUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending new user notification for user %v", u.Username))
comm.(UserEvents).OnNewUser(u)
}
}
}
// OnUpdatedUser is triggered when a new user is updated - UserEvents interface
func OnUpdatedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated user notification for user %v", u.Username))
comm.(UserEvents).OnUpdatedUser(u)
}
}
}
// OnDeletedUser is triggered when a new user is deleted - UserEvents interface
func OnDeletedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending deleted user notification for user %v", u.Username))
comm.(UserEvents).OnDeletedUser(u)
}
}
}
// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface
func OnUpdatedCore(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated core notification"))
comm.(CoreEvents).OnUpdatedCore(c)
}
}
}
// OnStart is triggered when the Statping service has started
func OnStart(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(CoreEvents).OnUpdatedCore(c)
}
}
}
// OnNewNotifier is triggered when a new notifier is loaded
func OnNewNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(NotifierEvents).OnNewNotifier(n)
}
}
}
// OnUpdatedNotifier is triggered when a notifier has been updated
func OnUpdatedNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated notifier for %v", n.Id))
comm.(NotifierEvents).OnUpdatedNotifier(n)
}
}
}

View File

@ -1,324 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import (
"errors"
"fmt"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"time"
)
// ExampleNotifier is an example on how to use the Statping notifier struct
type ExampleNotifier struct {
*Notification
}
// example is a example variable for a example notifier
var example = &ExampleNotifier{&Notification{
Method: METHOD,
Host: "http://exmaplehost.com",
Title: "Example",
Description: "Example Notifier",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(3 * time.Second),
Limits: 7,
Form: []NotificationForm{{
Type: "text",
Title: "Host",
Placeholder: "Insert your Host here.",
DbField: "host",
SmallText: "this is where you would put the host",
}, {
Type: "text",
Title: "Username",
Placeholder: "Insert your Username here.",
DbField: "username",
}, {
Type: "password",
Title: "Password",
Placeholder: "Insert your Password here.",
DbField: "password",
}, {
Type: "number",
Title: "Port",
Placeholder: "Insert your Port here.",
DbField: "port",
}, {
Type: "text",
Title: "API Key",
Placeholder: "Insert your API Key here",
DbField: "api_key",
}, {
Type: "text",
Title: "API Secret",
Placeholder: "Insert your API Secret here",
DbField: "api_secret",
}, {
Type: "text",
Title: "Var 1",
Placeholder: "Insert your Var1 here",
DbField: "var1",
}, {
Type: "text",
Title: "Var2",
Placeholder: "Var2 goes here",
DbField: "var2",
}},
}}
// init will be ran when Statping is loaded, AddNotifier will add the notifier instance to the system
func init() {
dir = utils.Directory
source.Assets()
utils.InitLogs()
injectDatabase()
AddNotifiers(example)
}
// Send is the main function to hold your notifier functionality
func (n *ExampleNotifier) Send(msg interface{}) error {
message := msg.(string)
fmt.Printf("i received this string: %v\n", message)
return nil
}
// Select is a required basic event for the Notifier interface
func (n *ExampleNotifier) Select() *Notification {
return n.Notification
}
// OnSave is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnSave() error {
msg := fmt.Sprintf("received on save trigger")
n.AddQueue("onsave", msg)
return errors.New("onsave triggered")
}
// OnSuccess is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnSuccess(s *types.Service) {
msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnFailure is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnTest is a option testing event for the Notifier interface
func (n *ExampleNotifier) OnTest() error {
fmt.Printf("received a test trigger with form data: %v\n", n.Host)
return nil
}
// OnNewService is a option event for new services
func (n *ExampleNotifier) OnNewService(s *types.Service) {
msg := fmt.Sprintf("received a new service trigger for service: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedService is a option event for updated services
func (n *ExampleNotifier) OnUpdatedService(s *types.Service) {
msg := fmt.Sprintf("received a update service trigger for service: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnDeletedService is a option event for deleted services
func (n *ExampleNotifier) OnDeletedService(s *types.Service) {
msg := fmt.Sprintf("received a delete service trigger for service: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnNewUser is a option event for new users
func (n *ExampleNotifier) OnNewUser(s *types.User) {
msg := fmt.Sprintf("received a new user trigger for user: %v\n", s.Username)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedUser is a option event for updated users
func (n *ExampleNotifier) OnUpdatedUser(s *types.User) {
msg := fmt.Sprintf("received a updated user trigger for user: %v\n", s.Username)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnDeletedUser is a option event for deleted users
func (n *ExampleNotifier) OnDeletedUser(s *types.User) {
msg := fmt.Sprintf("received a deleted user trigger for user: %v\n", s.Username)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedCore is a option event when the settings are updated
func (n *ExampleNotifier) OnUpdatedCore(s *types.Core) {
msg := fmt.Sprintf("received a updated core trigger for core: %v\n", s.Name)
n.AddQueue("core", msg)
}
// OnStart is triggered when statup has been started
func (n *ExampleNotifier) OnStart(s *types.Core) {
msg := fmt.Sprintf("received a trigger on Statping boot: %v\n", s.Name)
n.AddQueue(fmt.Sprintf("core"), msg)
}
// OnNewNotifier is triggered when a new notifier has initialized
func (n *ExampleNotifier) OnNewNotifier(s *Notification) {
msg := fmt.Sprintf("received a new notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(fmt.Sprintf("notifier_%v", s.Id), msg)
}
// OnUpdatedNotifier is triggered when a notifier has been updated
func (n *ExampleNotifier) OnUpdatedNotifier(s *Notification) {
msg := fmt.Sprintf("received a update notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(fmt.Sprintf("notifier_%v", s.Id), msg)
}
// Create a new notifier that includes a form for the end user to insert their own values
func ExampleNotification() {
// Create a new variable for your Notifier
example = &ExampleNotifier{&Notification{
Method: "Example",
Title: "Example Notifier",
Description: "Example Notifier can hold many different types of fields for a customized look.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(1500 * time.Millisecond),
Limits: 7,
Form: []NotificationForm{{
Type: "text",
Title: "Host",
Placeholder: "Insert your Host here.",
DbField: "host",
SmallText: "you can also use SmallText to insert some helpful hints under this input",
}, {
Type: "text",
Title: "API Key",
Placeholder: "Include some type of API key here",
DbField: "api_key",
}},
}}
// AddNotifier accepts a Notifier to load into the Statping Notification system
err := AddNotifiers(example)
fmt.Println(err)
// Output: <nil>
}
// Add a Notifier to the AddQueue function to insert it into the system
func ExampleAddNotifier() {
err := AddNotifiers(example)
fmt.Println(err)
// Output: <nil>
}
// OnSuccess will be triggered everytime a service is online
func ExampleNotification_OnSuccess() {
msg := fmt.Sprintf("this is a successful message as a string passing into AddQueue function")
example.AddQueue("example", msg)
fmt.Println(len(example.Queue))
// Output:
// 1
}
// Add a new message into the queue OnSuccess
func ExampleOnSuccess() {
msg := fmt.Sprintf("received a count trigger for service: %v\n", service.Name)
example.AddQueue("example", msg)
}
// Add a new message into the queue OnFailure
func ExampleOnFailure() {
msg := fmt.Sprintf("received a failing service: %v\n", service.Name)
example.AddQueue("example", msg)
}
// OnTest allows your notifier to be testable
func ExampleOnTest() {
err := example.OnTest()
fmt.Print(err)
// Output <nil>
}
// Implement the Test interface to give your notifier testing abilities
func ExampleNotification_CanTest() {
testable := example.CanTest()
fmt.Print(testable)
// Output: true
}
// Add any type of interface to the AddQueue function to be ran in the queue
func ExampleNotification_AddQueue() {
msg := fmt.Sprintf("this is a failing message as a string passing into AddQueue function")
example.AddQueue("example", msg)
queue := example.Queue
fmt.Printf("Example has %v items in the queue", len(queue))
// Output:
// Example has 2 items in the queue
}
// The Send method will run the main functionality of your notifier
func ExampleNotification_Send() {
msg := "this can be any type of interface"
example.Send(msg)
queue := example.Queue
fmt.Printf("Example has %v items in the queue", len(queue))
// Output:
// i received this string: this can be any type of interface
// Example has 2 items in the queue
}
// LastSent will return the time.Duration of the last sent message
func ExampleNotification_LastSent() {
last := example.LastSent()
fmt.Printf("Last message was sent %v seconds ago", last.Seconds())
// Output: Last message was sent 0 seconds ago
}
// Logs will return a slice of previously sent items from your notifier
func ExampleNotification_Logs() {
logs := example.Logs()
fmt.Printf("Example has %v items in the log", len(logs))
// Output: Example has 0 items in the log
}
// SentLastMinute will return he amount of notifications sent in last 1 minute
func ExampleNotification_SentLastMinute() {
lastMinute := example.SentLastMinute()
fmt.Printf("%v notifications sent in the last minute", lastMinute)
// Output: 0 notifications sent in the last minute
}
// SentLastHour will return he amount of notifications sent in last 1 hour
func ExampleNotification_SentLastHour() {
lastHour := example.SentLastHour()
fmt.Printf("%v notifications sent in the last hour", lastHour)
// Output: 0 notifications sent in the last hour
}
// SentLastHour will return he amount of notifications sent in last 1 hour
func ExampleNotification_WithinLimits() {
ok, err := example.WithinLimits()
if err != nil {
panic(err)
}
if ok {
fmt.Printf("Example notifier is still within its sending limits")
}
// Output: Example notifier is still within its sending limits
}

View File

@ -1,68 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import "github.com/hunterlong/statping/types"
// Notifier interface is required to create a new Notifier
type Notifier interface {
OnSave() error // OnSave is triggered when the notifier is saved
Send(interface{}) error // OnSave is triggered when the notifier is saved
Select() *Notification // Select returns the *Notification for a notifier
}
// BasicEvents includes the most minimal events, failing and successful service triggers
type BasicEvents interface {
OnSuccess(*types.Service) // OnSuccess is triggered when a service is successful
OnFailure(*types.Service, *types.Failure) // OnFailure is triggered when a service is failing
}
// Tester interface will include a function to Test users settings before saving
type Tester interface {
OnTest() error
}
// ServiceEvents are events for Services
type ServiceEvents interface {
OnNewService(*types.Service)
OnUpdatedService(*types.Service)
OnDeletedService(*types.Service)
}
// UserEvents are events for Users
type UserEvents interface {
OnNewUser(*types.User)
OnUpdatedUser(*types.User)
OnDeletedUser(*types.User)
}
// CoreEvents are events for the main Core app
type CoreEvents interface {
OnUpdatedCore(*types.Core)
OnStart(*types.Core)
}
// NotifierEvents are events for other Notifiers
type NotifierEvents interface {
OnNewNotifier(*Notification)
OnUpdatedNotifier(*Notification)
}
// HTTPRouter interface will allow your notifier to accept http GET/POST requests
type HTTPRouter interface {
OnGET() error
OnPOST() error
}

View File

@ -1,483 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
"reflect"
"strings"
"time"
)
var (
// AllCommunications holds all the loaded notifiers
AllCommunications []types.AllNotifiers
// db holds the Statping database connection
db *gorm.DB
timezone float32
log = utils.Log.WithField("type", "notifier")
)
// Notification contains all the fields for a Statping Notifier.
type Notification struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Method string `gorm:"column:method" json:"method"`
Host string `gorm:"not null;column:host" json:"host,omitempty"`
Port int `gorm:"not null;column:port" json:"port,omitempty"`
Username string `gorm:"not null;column:username" json:"username,omitempty"`
Password string `gorm:"not null;column:password" json:"password,omitempty"`
Var1 string `gorm:"not null;column:var1" json:"var1,omitempty"`
Var2 string `gorm:"not null;column:var2" json:"var2,omitempty"`
ApiKey string `gorm:"not null;column:api_key" json:"api_key,omitempty"`
ApiSecret string `gorm:"not null;column:api_secret" json:"api_secret,omitempty"`
Enabled types.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Limits int `gorm:"not null;column:limits" json:"limits"`
Removable bool `gorm:"column:removable" json:"removeable"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Form []NotificationForm `gorm:"-" json:"form"`
logs []*NotificationLog `gorm:"-" json:"logs"`
Title string `gorm:"-" json:"title"`
Description string `gorm:"-" json:"description"`
Author string `gorm:"-" json:"author"`
AuthorUrl string `gorm:"-" json:"author_url"`
Icon string `gorm:"-" json:"icon"`
Delay time.Duration `gorm:"-" json:"delay,string"`
Queue []*QueueData `gorm:"-" json:"-"`
Running chan bool `gorm:"-" json:"-"`
testable bool `gorm:"-" json:"testable"`
}
// QueueData is the struct for the messaging queue with service
type QueueData struct {
Id string
Data interface{}
}
// NotificationForm contains the HTML fields for each variable/input you want the notifier to accept.
type NotificationForm struct {
Type string `json:"type"` // the html input type (text, password, email)
Title string `json:"title"` // include a title for ease of use
Placeholder string `json:"placeholder"` // add a placeholder for the input
DbField string `json:"field"` // true variable key for input
SmallText string `json:"small_text"` // insert small text under a html input
Required bool `json:"required"` // require this input on the html form
IsHidden bool `json:"hidden"` // hide this form element from end user
IsList bool `json:"list"` // make this form element a comma separated list
IsSwitch bool `json:"switch"` // make the notifier a boolean true/false switch
}
// NotificationLog contains the normalized message from previously sent notifications
type NotificationLog struct {
Message string `json:"message"`
Time utils.Timestamp `json:"time"`
Timestamp time.Time `json:"timestamp"`
}
// AfterFind for Notification will set the timezone
func (n *Notification) AfterFind() (err error) {
n.CreatedAt = utils.Timezoner(n.CreatedAt, timezone)
n.UpdatedAt = utils.Timezoner(n.UpdatedAt, timezone)
return
}
// AddQueue will add any type of interface (json, string, struct, etc) into the Notifiers queue
func (n *Notification) AddQueue(uid string, msg interface{}) {
data := &QueueData{uid, msg}
n.Queue = append(n.Queue, data)
log.WithFields(utils.ToFields(data, n)).Infoln(fmt.Sprintf("Notifier '%v' added new item (%v) to the queue. (%v queued)", n.Method, uid, len(n.Queue)))
}
// CanTest returns true if the notifier implements the OnTest interface
func (n *Notification) CanTest() bool {
return n.testable
}
// db will return the notifier database column/record
func modelDb(n *Notification) *gorm.DB {
return db.Model(&Notification{}).Where("method = ?", n.Method).Find(n)
}
// SetDB is called by core to inject the database for a notifier to use
func SetDB(d *gorm.DB, zone float32) {
db = d
timezone = zone
}
// asNotification accepts a Notifier and returns a Notification struct
func asNotification(n Notifier) *Notification {
return n.Select()
}
// AddNotifier accept a Notifier interface to be added into the array
func AddNotifiers(notifiers ...Notifier) error {
for _, n := range notifiers {
if isType(n, new(Notifier)) {
err := checkNotifierForm(n)
if err != nil {
return err
}
AllCommunications = append(AllCommunications, n)
Init(n)
} else {
return errors.New("notifier does not have the required methods")
}
}
startAllNotifiers()
return nil
}
// normalizeType will accept multiple interfaces and converts it into a string for logging
func normalizeType(ty interface{}) string {
switch v := ty.(type) {
case int, int32, int64:
return fmt.Sprintf("%v", v)
case float32, float64:
return fmt.Sprintf("%v", v)
case string:
return v
case []byte:
return string(v)
case []string:
return fmt.Sprintf("%v", v)
case interface{}, map[string]interface{}:
j, _ := json.Marshal(v)
return string(j)
default:
return fmt.Sprintf("%v", v)
}
}
// Log will record a new notification into memory and will show the logs on the settings page
func (n *Notification) makeLog(msg interface{}) {
log := &NotificationLog{
Message: normalizeType(msg),
Time: utils.Timestamp(utils.Now()),
Timestamp: utils.Now(),
}
n.logs = append(n.logs, log)
}
// Logs returns an array of the notifiers logs
func (n *Notification) Logs() []*NotificationLog {
return reverseLogs(n.logs)
}
// reverseLogs will reverse the notifier's logs to be time desc
func reverseLogs(input []*NotificationLog) []*NotificationLog {
if len(input) == 0 {
return input
}
return append(reverseLogs(input[1:]), input[0])
}
// isInDatabase returns true if the notifier has already been installed
func isInDatabase(n Notifier) bool {
inDb := modelDb(n.Select()).RecordNotFound()
return !inDb
}
// SelectNotification returns the Notification struct from the database
func SelectNotification(n Notifier) (*Notification, error) {
notifier := n.Select()
err := db.Model(&Notification{}).Where("method = ?", notifier.Method).Scan(&notifier)
return notifier, err.Error
}
// Update will update the notification into the database
func Update(n Notifier, notif *Notification) (*Notification, error) {
notif.ResetQueue()
err := db.Model(&Notification{}).Update(notif)
if notif.Enabled.Bool {
notif.close()
notif.start()
go Queue(n)
} else {
notif.close()
}
return notif, err.Error
}
// insertDatabase will create a new record into the database for the notifier
func insertDatabase(n Notifier) (int64, error) {
noti := n.Select()
noti.Limits = 3
query := db.Create(noti)
if query.Error != nil {
return 0, query.Error
}
return noti.Id, query.Error
}
// SelectNotifier returns the Notification struct from the database
func SelectNotifier(method string) (*Notification, Notifier, error) {
for _, comm := range AllCommunications {
n, ok := comm.(Notifier)
if !ok {
return nil, nil, fmt.Errorf("incorrect notification type: %v", reflect.TypeOf(n).String())
}
notifier := n.Select()
if notifier.Method == method {
return notifier, comm.(Notifier), nil
}
}
return nil, nil, errors.New("cannot find notifier")
}
// Init accepts the Notifier interface to initialize the notifier
func Init(n Notifier) (*Notification, error) {
err := install(n)
var notify *Notification
if err == nil {
notify, _ = SelectNotification(n)
notify.CreatedAt = utils.Timezoner(notify.CreatedAt, timezone)
notify.UpdatedAt = utils.Timezoner(notify.UpdatedAt, timezone)
if notify.Delay.Seconds() == 0 {
notify.Delay = time.Duration(1 * time.Second)
}
notify.testable = isType(n, new(Tester))
notify.Form = n.Select().Form
}
return notify, err
}
// startAllNotifiers will start the go routine for each loaded notifier
func startAllNotifiers() {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
notify := comm.(Notifier)
if notify.Select().Enabled.Bool {
notify.Select().close()
notify.Select().start()
go Queue(notify)
}
}
}
}
// Queue is the FIFO go routine to send notifications when objects are triggered
func Queue(n Notifier) {
notification := n.Select()
rateLimit := notification.Delay
CheckNotifier:
for {
select {
case <-notification.Running:
break CheckNotifier
case <-time.After(rateLimit):
notification = n.Select()
if len(notification.Queue) > 0 {
ok, _ := notification.WithinLimits()
if ok {
msg := notification.Queue[0]
err := n.Send(msg.Data)
if err != nil {
log.WithFields(utils.ToFields(notification, msg)).Warnln(fmt.Sprintf("Notifier '%v' had an error: %v", notification.Method, err))
} else {
log.WithFields(utils.ToFields(notification, msg)).Infoln(fmt.Sprintf("Notifier '%v' sent outgoing message (%v) %v left in queue.", notification.Method, msg.Id, len(notification.Queue)))
}
notification.makeLog(msg.Data)
if len(notification.Queue) > 1 {
notification.Queue = notification.Queue[1:]
} else {
notification.Queue = nil
}
rateLimit = notification.Delay
}
}
}
continue
}
}
// install will check the database for the notification, if its not inserted it will insert a new record for it
func install(n Notifier) error {
inDb := isInDatabase(n)
log.WithField("installed", inDb).
WithFields(utils.ToFields(n)).
Debugln(fmt.Sprintf("Checking if notifier '%v' is installed: %v", n.Select().Method, inDb))
if !inDb {
_, err := insertDatabase(n)
if err != nil {
log.Errorln(err)
return err
}
}
return nil
}
// LastSent returns a time.Duration of the last sent notification for the notifier
func (n *Notification) LastSent() time.Duration {
if len(n.logs) == 0 {
return time.Duration(0)
}
last := n.Logs()[0]
since := time.Since(last.Timestamp)
return since
}
// SentLastHour returns the total amount of notifications sent in last 1 hour
func (n *Notification) SentLastHour() int {
since := utils.Now().Add(-1 * time.Hour)
return n.SentLast(since)
}
// SentLastMinute returns the total amount of notifications sent in last 1 minute
func (n *Notification) SentLastMinute() int {
since := utils.Now().Add(-1 * time.Minute)
return n.SentLast(since)
}
// SentLast accept a time.Time and returns the amount of sent notifications within your time to current
func (n *Notification) SentLast(since time.Time) int {
sent := 0
for _, v := range n.Logs() {
lastTime := time.Time(v.Time)
if lastTime.After(since) {
sent++
}
}
return sent
}
// GetValue returns the database value of a accept DbField value.
func (n *Notification) GetValue(dbField string) string {
dbField = strings.ToLower(dbField)
switch dbField {
case "host":
return n.Host
case "port":
return fmt.Sprintf("%v", n.Port)
case "username":
return n.Username
case "password":
if n.Password != "" {
return "##########"
}
case "var1":
return n.Var1
case "var2":
return n.Var2
case "api_key":
return n.ApiKey
case "api_secret":
return n.ApiSecret
case "limits":
return utils.ToString(int(n.Limits))
}
return ""
}
// isType will return true if a variable can implement an interface
func isType(n interface{}, obj interface{}) bool {
one := reflect.TypeOf(n)
two := reflect.ValueOf(obj).Elem()
return one.Implements(two.Type())
}
// isEnabled returns true if the notifier is enabled
func isEnabled(n interface{}) bool {
notifier := n.(Notifier).Select()
return notifier.Enabled.Bool
}
// inLimits will return true if the notifier is within sending limits
func inLimits(n interface{}) bool {
notifier := n.(Notifier).Select()
ok, _ := notifier.WithinLimits()
return ok
}
// WithinLimits returns true if the notifier is within its sending limits
func (n *Notification) WithinLimits() (bool, error) {
if n.SentLastMinute() == 0 {
return true, nil
}
if n.SentLastMinute() >= n.Limits {
return false, fmt.Errorf("notifier sent %v out of %v in last minute", n.SentLastMinute(), n.Limits)
}
if n.LastSent().Seconds() == 0 {
return true, nil
}
if n.Delay.Seconds() >= n.LastSent().Seconds() {
return false, fmt.Errorf("notifiers delay (%v) is greater than last message sent (%v)", n.Delay.Seconds(), n.LastSent().Seconds())
}
return true, nil
}
// ResetQueue will clear the notifiers Queue
func (n *Notification) ResetQueue() {
n.Queue = nil
}
// ResetQueue will clear the notifiers Queue for a service
func (n *Notification) ResetUniqueQueue(uid string) []*QueueData {
var queue []*QueueData
for _, v := range n.Queue {
if v.Id != uid {
queue = append(queue, v)
}
}
n.Queue = queue
return queue
}
// start will start the go routine for the notifier queue
func (n *Notification) start() {
n.Running = make(chan bool)
}
// close will stop the go routine for queue
func (n *Notification) close() {
if n.IsRunning() {
close(n.Running)
}
}
// IsRunning will return true if the notifier is currently running a queue
func (n *Notification) IsRunning() bool {
if n.Running == nil {
return false
}
select {
case <-n.Running:
return false
default:
return true
}
}
// ExampleService can be used for the OnTest() method for notifiers
var ExampleService = &types.Service{
Id: 1,
Name: "Interpol - All The Rage Back Home",
Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
LastStatusCode: 404,
Expected: types.NewNullString("test example"),
LastResponse: "<html>this is an example response</html>",
CreatedAt: utils.Now().Add(-24 * time.Hour),
}

View File

@ -1,221 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifier
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var (
dir string
METHOD = "example"
)
var service = &types.Service{
Name: "Interpol - All The Rage Back Home",
Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
AllowNotifications: types.NewNullBool(true),
}
var failure = &types.Failure{
Issue: "testing",
}
var user = &types.User{
Username: "admin",
Email: "info@email.com",
}
var core = &types.Core{
Name: "testing notifiers",
}
func injectDatabase() {
utils.DeleteFile(dir + "/notifier.db")
db, _ = gorm.Open("sqlite3", dir+"/notifier.db")
db.CreateTable(&Notification{})
}
func TestIsBasicType(t *testing.T) {
assert.True(t, isType(example, new(Notifier)))
assert.True(t, isType(example, new(BasicEvents)))
assert.True(t, isType(example, new(ServiceEvents)))
assert.True(t, isType(example, new(UserEvents)))
assert.True(t, isType(example, new(CoreEvents)))
assert.True(t, isType(example, new(NotifierEvents)))
assert.True(t, isType(example, new(Tester)))
}
func TestIsInDatabase(t *testing.T) {
in := isInDatabase(example)
assert.True(t, in)
}
func TestSelectNotification(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "example", notifier.Method)
assert.False(t, notifier.Enabled.Bool)
assert.False(t, notifier.IsRunning())
}
func TestAddQueue(t *testing.T) {
msg := "this is a test in the queue!"
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 1, len(example.Queue))
}
func TestNotification_Update(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
notifier.Host = "http://demo.statping.com/api"
notifier.Port = 9090
notifier.Username = "admin"
notifier.Password = "password123"
notifier.Var1 = "var1_is_here"
notifier.Var2 = "var2_is_here"
notifier.ApiKey = "USBdu82HDiiuw9327yGYDGw"
notifier.ApiSecret = "PQopncow929hUIDHGwiud"
notifier.Limits = 10
_, err = Update(example, notifier)
assert.Nil(t, err)
selected, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "http://demo.statping.com/api", selected.GetValue("host"))
assert.Equal(t, "http://demo.statping.com/api", example.Notification.Host)
assert.Equal(t, "http://demo.statping.com/api", example.Host)
assert.Equal(t, "USBdu82HDiiuw9327yGYDGw", selected.GetValue("api_key"))
assert.Equal(t, "USBdu82HDiiuw9327yGYDGw", example.ApiKey)
assert.False(t, selected.Enabled.Bool)
assert.False(t, selected.IsRunning())
}
func TestEnableNotification(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
notifier.Enabled = types.NewNullBool(true)
updated, err := Update(example, notifier)
assert.Nil(t, err)
assert.True(t, updated.Enabled.Bool)
assert.True(t, updated.IsRunning())
}
func TestIsEnabled(t *testing.T) {
assert.True(t, isEnabled(example))
}
func TestIsRunning(t *testing.T) {
assert.True(t, example.IsRunning())
}
func TestLastSent(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "0s", notifier.LastSent().String())
}
func TestWithinLimits(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, 10, notifier.Limits)
assert.True(t, inLimits(example))
}
func TestNotification_GetValue(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
val := notifier.GetValue("Host")
assert.Equal(t, "http://demo.statping.com/api", val)
}
func TestOnSave(t *testing.T) {
err := example.OnSave()
assert.Equal(t, "onsave triggered", err.Error())
}
func TestOnSuccess(t *testing.T) {
OnSuccess(service)
assert.Equal(t, 2, len(example.Queue))
}
func TestOnFailure(t *testing.T) {
OnFailure(service, failure)
assert.Equal(t, 3, len(example.Queue))
}
func TestOnNewService(t *testing.T) {
OnNewService(service)
assert.Equal(t, 4, len(example.Queue))
}
func TestOnUpdatedService(t *testing.T) {
OnUpdatedService(service)
assert.Equal(t, 5, len(example.Queue))
}
func TestOnDeletedService(t *testing.T) {
OnDeletedService(service)
assert.Equal(t, 6, len(example.Queue))
}
func TestOnNewUser(t *testing.T) {
OnNewUser(user)
assert.Equal(t, 7, len(example.Queue))
}
func TestOnUpdatedUser(t *testing.T) {
OnUpdatedUser(user)
assert.Equal(t, 8, len(example.Queue))
}
func TestOnDeletedUser(t *testing.T) {
OnDeletedUser(user)
assert.Equal(t, 9, len(example.Queue))
}
func TestOnUpdatedCore(t *testing.T) {
OnUpdatedCore(core)
assert.Equal(t, 10, len(example.Queue))
}
func TestOnUpdatedNotifier(t *testing.T) {
OnUpdatedNotifier(example.Select())
assert.Equal(t, 11, len(example.Queue))
}
func TestRunAllQueueAndStop(t *testing.T) {
assert.True(t, example.IsRunning())
assert.Equal(t, 11, len(example.Queue))
go Queue(example)
time.Sleep(13 * time.Second)
assert.NotZero(t, len(example.Queue))
example.close()
assert.False(t, example.IsRunning())
assert.NotZero(t, len(example.Queue))
}

View File

@ -1,596 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sync"
"time"
)
var (
sampleStart = time.Now().Add((-24 * 7) * time.Hour).UTC()
SampleHits = 9900.
)
// InsertSampleData will create the example/dummy services for a brand new Statping installation
func InsertSampleData() error {
log.Infoln("Inserting Sample Data...")
insertSampleGroups()
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
s1 := ReturnService(&types.Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 1,
GroupId: 1,
Permalink: types.NewNullString("google"),
VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn,
})
s2 := ReturnService(&types.Service{
Name: "Statping Github",
Domain: "https://github.com/hunterlong/statping",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 2,
Permalink: types.NewNullString("statping_github"),
VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn,
})
s3 := ReturnService(&types.Service{
Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 30,
Order: 3,
Public: types.NewNullBool(true),
VerifySSL: types.NewNullBool(true),
GroupId: 2,
CreatedAt: createdOn,
})
s4 := ReturnService(&types.Service{
Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201,
Expected: types.NewNullString(`(title)": "((\\"|[statping])*)"`),
Interval: 30,
Type: "http",
Method: "POST",
PostData: types.NewNullString(`{ "title": "statping", "body": "bar", "userId": 19999 }`),
Timeout: 30,
Order: 4,
Public: types.NewNullBool(true),
VerifySSL: types.NewNullBool(true),
GroupId: 2,
CreatedAt: createdOn,
})
s5 := ReturnService(&types.Service{
Name: "Google DNS",
Domain: "8.8.8.8",
Interval: 20,
Type: "tcp",
Port: 53,
Timeout: 120,
Order: 5,
Public: types.NewNullBool(true),
GroupId: 1,
CreatedAt: createdOn,
})
s1.Create(false)
s2.Create(false)
s3.Create(false)
s4.Create(false)
s5.Create(false)
insertMessages()
insertSampleIncidents()
log.Infoln("Sample data has finished importing")
return nil
}
func insertSampleIncidents() error {
incident1 := &Incident{&types.Incident{
Title: "Github Downtime",
Description: "This is an example of a incident for a service.",
ServiceId: 2,
}}
_, err := incident1.Create()
incidentUpdate1 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github's page for Statping seems to be sending a 501 error.",
Type: "Investigating",
}}
_, err = incidentUpdate1.Create()
incidentUpdate2 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Problem is continuing and we are looking at the issues.",
Type: "Update",
}}
_, err = incidentUpdate2.Create()
incidentUpdate3 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
}}
_, err = incidentUpdate3.Create()
return err
}
func insertSampleGroups() error {
group1 := &Group{&types.Group{
Name: "Main Services",
Public: types.NewNullBool(true),
Order: 2,
}}
_, err := group1.Create()
group2 := &Group{&types.Group{
Name: "Linked Services",
Public: types.NewNullBool(false),
Order: 1,
}}
_, err = group2.Create()
group3 := &Group{&types.Group{
Name: "Empty Group",
Public: types.NewNullBool(false),
Order: 3,
}}
_, err = group3.Create()
return err
}
// insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin
func insertSampleCheckins() error {
s1 := SelectService(1)
checkin1 := ReturnCheckin(&types.Checkin{
ServiceId: s1.Id,
Interval: 300,
GracePeriod: 300,
})
checkin1.Update()
s2 := SelectService(1)
checkin2 := ReturnCheckin(&types.Checkin{
ServiceId: s2.Id,
Interval: 900,
GracePeriod: 300,
})
checkin2.Update()
checkTime := time.Now().UTC().Add(-24 * time.Hour)
for i := 0; i <= 60; i++ {
checkHit := ReturnCheckinHit(&types.CheckinHit{
Checkin: checkin1.Id,
From: "192.168.0.1",
CreatedAt: checkTime.UTC(),
})
checkHit.Create()
checkTime = checkTime.Add(10 * time.Minute)
}
return nil
}
// InsertSampleHits will create a couple new hits for the sample services
func InsertSampleHits() error {
tx := hitsDB().Begin()
sg := new(sync.WaitGroup)
for i := int64(1); i <= 5; i++ {
sg.Add(1)
service := SelectService(i)
seed := time.Now().UnixNano()
log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name))
createdAt := sampleStart
p := utils.NewPerlin(2., 2., 10, seed)
go func() {
defer sg.Done()
for hi := 0.; hi <= float64(SampleHits); hi++ {
latency := p.Noise1D(hi / 500)
createdAt = createdAt.Add(60 * time.Second)
hit := &types.Hit{
Service: service.Id,
CreatedAt: createdAt,
Latency: latency,
}
tx = tx.Create(&hit)
}
}()
}
sg.Wait()
err := tx.Commit().Error
if err != nil {
log.Errorln(err)
}
return err
}
// insertSampleCore will create a new Core for the seed
func insertSampleCore() error {
core := &types.Core{
Name: "Statping Sample Data",
Description: "This data is only used to testing",
ApiKey: "sample",
ApiSecret: "samplesecret",
Domain: "http://localhost:8080",
Version: "test",
CreatedAt: time.Now().UTC(),
UseCdn: types.NewNullBool(false),
}
query := coreDB().Create(core)
return query.Error
}
// insertSampleUsers will create 2 admin users for a seed database
func insertSampleUsers() error {
u2 := ReturnUser(&types.User{
Username: "testadmin",
Password: "password123",
Email: "info@betatude.com",
Admin: types.NewNullBool(true),
})
u3 := ReturnUser(&types.User{
Username: "testadmin2",
Password: "password123",
Email: "info@adminhere.com",
Admin: types.NewNullBool(true),
})
_, err := u2.Create()
_, err = u3.Create()
return err
}
func insertMessages() error {
m1 := ReturnMessage(&types.Message{
Title: "Routine Downtime",
Description: "This is an example a upcoming message for a service!",
ServiceId: 1,
StartOn: time.Now().UTC().Add(15 * time.Minute),
EndOn: time.Now().UTC().Add(2 * time.Hour),
})
if _, err := m1.Create(); err != nil {
return err
}
m2 := ReturnMessage(&types.Message{
Title: "Server Reboot",
Description: "This is another example a upcoming message for a service!",
ServiceId: 3,
StartOn: time.Now().Add(15 * time.Minute),
EndOn: time.Now().Add(2 * time.Hour),
})
if _, err := m2.Create(); err != nil {
return err
}
return nil
}
// InsertLargeSampleData will create the example/dummy services for testing the Statping server
func InsertLargeSampleData() error {
if err := insertSampleCore(); err != nil {
return err
}
if err := InsertSampleData(); err != nil {
return err
}
if err := insertSampleUsers(); err != nil {
return err
}
if err := insertSampleCheckins(); err != nil {
return err
}
if err := insertMessages(); err != nil {
return err
}
createdOn := time.Now().UTC().Add((-24 * 90) * time.Hour)
s6 := ReturnService(&types.Service{
Name: "JSON Lint",
Domain: "https://jsonlint.com",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 6,
CreatedAt: createdOn,
})
s7 := ReturnService(&types.Service{
Name: "Demo Page",
Domain: "https://demo.statping.com",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 15,
Order: 7,
CreatedAt: createdOn,
})
s8 := ReturnService(&types.Service{
Name: "Golang",
Domain: "https://golang.org",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 8,
})
s9 := ReturnService(&types.Service{
Name: "Santa Monica",
Domain: "https://www.santamonica.com",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 9,
CreatedAt: createdOn,
})
s10 := ReturnService(&types.Service{
Name: "Oeschs Die Dritten",
Domain: "https://www.oeschs-die-dritten.ch/en/",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 10,
CreatedAt: createdOn,
})
s11 := ReturnService(&types.Service{
Name: "XS Project - Bochka, Bass, Kolbaser",
Domain: "https://www.youtube.com/watch?v=VLW1ieY4Izw",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 11,
CreatedAt: createdOn,
})
s12 := ReturnService(&types.Service{
Name: "Github",
Domain: "https://github.com/hunterlong",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 12,
CreatedAt: createdOn,
})
s13 := ReturnService(&types.Service{
Name: "Failing URL",
Domain: "http://thisdomainisfakeanditsgoingtofail.com",
ExpectedStatus: 200,
Interval: 45,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 13,
CreatedAt: createdOn,
})
s14 := ReturnService(&types.Service{
Name: "Oesch's die Dritten - Die Jodelsprache",
Domain: "https://www.youtube.com/watch?v=k3GTxRt4iao",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 12,
Order: 14,
CreatedAt: createdOn,
})
s15 := ReturnService(&types.Service{
Name: "Gorm",
Domain: "http://gorm.io/",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 12,
Order: 15,
CreatedAt: createdOn,
})
s6.Create(false)
s7.Create(false)
s8.Create(false)
s9.Create(false)
s10.Create(false)
s11.Create(false)
s12.Create(false)
s13.Create(false)
s14.Create(false)
s15.Create(false)
var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour)
insertHitRecords(dayAgo, 5450)
insertFailureRecords(dayAgo, 730)
return nil
}
// insertFailureRecords will create failures for 15 services from seed
func insertFailureRecords(since time.Time, amount int64) {
for i := int64(14); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Name))
createdAt := since
for fi := int64(1); fi <= amount; fi++ {
createdAt = createdAt.Add(2 * time.Minute)
failure := &types.Failure{
Service: service.Id,
Issue: "testing right here",
CreatedAt: createdAt,
}
service.CreateFailure(failure)
}
}
}
// insertHitRecords will create successful Hit records for 15 services
func insertHitRecords(since time.Time, amount int64) {
for i := int64(1); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name))
createdAt := since
p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano())
for hi := int64(1); hi <= amount; hi++ {
latency := p.Noise1D(float64(hi / 10))
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{
Service: service.Id,
CreatedAt: createdAt.UTC(),
Latency: latency,
}
service.CreateHit(hit)
}
}
}
// TmpRecords is used for testing Statping. It will create a SQLite database file
// with sample data and store it in the /tmp folder to be used by the tests.
func TmpRecords(dbFile string) error {
var sqlFile = utils.Directory + "/" + dbFile
utils.CreateDirectory(utils.Directory + "/tmp")
var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename
SampleHits = 480
var err error
CoreApp = NewCore()
CoreApp.Name = "Tester"
configs := &types.DbConfig{
DbConn: "sqlite",
Project: "Tester",
Location: utils.Directory,
SqlFile: sqlFile,
}
log.Infoln("saving config.yml in: " + utils.Directory)
if configs, err = CoreApp.SaveConfig(configs); err != nil {
return err
}
log.Infoln("loading config.yml from: " + utils.Directory)
if configs, err = LoadConfigFile(utils.Directory); err != nil {
return err
}
log.Infoln("connecting to database")
exists := utils.FileExists(tmpSqlFile)
if exists {
log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile)
if err := utils.DeleteFile(sqlFile); err != nil {
log.Infoln(sqlFile + " was not found")
}
if err := utils.CopyFile(tmpSqlFile, sqlFile); err != nil {
return err
}
log.Infoln("loading config.yml from: " + utils.Directory)
if err := CoreApp.Connect(false, utils.Directory); err != nil {
return err
}
log.Infoln("selecting the Core variable")
if _, err := SelectCore(); err != nil {
return err
}
log.Infoln("inserting notifiers into database")
if err := InsertNotifierDB(); err != nil {
return err
}
log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil {
return err
}
if err := AttachNotifiers(); err != nil {
return err
}
CoreApp.Notifications = notifier.AllCommunications
return nil
}
log.Infoln(tmpSqlFile + " not found, creating a new database...")
if err := CoreApp.Connect(false, utils.Directory); err != nil {
return err
}
log.Infoln("creating database")
if err := CoreApp.CreateDatabase(); err != nil {
return err
}
log.Infoln("migrating database")
if err := CoreApp.MigrateDatabase(); err != nil {
return err
}
log.Infoln("insert large sample data into database")
if err := InsertLargeSampleData(); err != nil {
return err
}
log.Infoln("selecting the Core variable")
if CoreApp, err = SelectCore(); err != nil {
return err
}
log.Infoln("inserting notifiers into database")
if err := InsertNotifierDB(); err != nil {
return err
}
log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil {
return err
}
log.Infoln("copying sql database file to: " + tmpSqlFile)
if err := utils.CopyFile(sqlFile, tmpSqlFile); err != nil {
return err
}
return err
}

View File

@ -1,454 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"encoding/json"
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sort"
"strconv"
"time"
)
type Service struct {
*types.Service
}
// Select will return the *types.Service struct for Service
func (s *Service) Select() *types.Service {
return s.Service
}
// ReturnService will convert *types.Service to *core.Service
func ReturnService(s *types.Service) *Service {
return &Service{s}
}
func Services() []types.ServiceInterface {
return CoreApp.Services
}
// SelectService returns a *core.Service from in memory
func SelectService(id int64) *Service {
for _, s := range Services() {
if s.Select().Id == id {
return s.(*Service)
}
}
return nil
}
func SelectServices(auth bool) []*Service {
var validServices []*Service
for _, sr := range CoreApp.Services {
s := sr.(*Service)
if !s.Public.Bool {
if auth {
validServices = append(validServices, s)
}
} else {
validServices = append(validServices, s)
}
}
return validServices
}
// SelectServiceLink returns a *core.Service from the service permalink
func SelectServiceLink(permalink string) *Service {
for _, s := range Services() {
if s.Select().Permalink.String == permalink {
return s.(*Service)
}
}
return nil
}
// CheckinProcess runs the checkin routine for each checkin attached to service
func (s *Service) CheckinProcess() {
checkins := s.AllCheckins()
for _, c := range checkins {
c.Start()
go c.Routine()
}
}
// AllCheckins will return a slice of AllCheckins for a Service
func (s *Service) AllCheckins() []*Checkin {
var checkin []*Checkin
checkinDB().Where("service = ?", s.Id).Find(&checkin)
return checkin
}
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup.
func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
var services []*Service
db := servicesDB().Find(&services).Order("order_id desc")
if db.Error != nil {
log.Errorln(fmt.Sprintf("service error: %v", db.Error))
return nil, db.Error
}
CoreApp.Services = nil
for _, service := range services {
if start {
service.Start()
service.CheckinProcess()
}
fails := service.LimitedFailures(limitedFailures)
for _, f := range fails {
service.Failures = append(service.Failures, f)
}
checkins := service.AllCheckins()
for _, c := range checkins {
c.Failures = c.LimitedFailures(limitedFailures)
c.Hits = c.LimitedHits(limitedHits)
service.Checkins = append(service.Checkins, c)
}
CoreApp.Services = append(CoreApp.Services, service)
}
reorderServices()
return services, db.Error
}
// reorderServices will sort the services based on 'order_id'
func reorderServices() {
sort.Sort(ServiceOrder(CoreApp.Services))
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() string {
total, _ := s.TotalHits()
if total == 0 {
return "0"
}
sum := s.Sum()
avg := sum / float64(total) * 100
return fmt.Sprintf("%0.0f", avg*10)
}
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *Service) OnlineDaysPercent(days int) float32 {
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
return s.OnlineSince(ago)
}
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
s.Online24Hours = 100.00
return s.Online24Hours
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
s.Online24Hours = 0
return s.Online24Hours
}
avg := float64(failed) / float64(total) * 100
avg = 100 - avg
if avg < 0 {
avg = 0
}
amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10)
s.Online24Hours = float32(amount)
return s.Online24Hours
}
// DateScan struct is for creating the charts.js graph JSON array
type DateScan struct {
CreatedAt string `json:"x,omitempty"`
Value int64 `json:"y"`
}
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []DateScan `json:"data"`
}
// lastFailure returns the last Failure a service had
func (s *Service) lastFailure() *Failure {
limited := s.LimitedFailures(1)
if len(limited) == 0 {
return nil
}
last := limited[len(limited)-1]
return last
}
// SmallText returns a short description about a services status
// service.SmallText()
// // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string {
last := s.LimitedFailures(1)
//hits, _ := s.LimitedHits(1)
zone := CoreApp.Timezone
if s.Online {
if len(last) == 0 {
return fmt.Sprintf("Online since %v", utils.Timezoner(s.CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
} else {
return fmt.Sprintf("Online, last Failure was %v", utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
}
}
if len(last) > 0 {
lastFailure := s.lastFailure()
got, _ := timeago.TimeAgoWithTime(time.Now().UTC().Add(s.Downtime()), time.Now().UTC())
return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError())
} else {
return fmt.Sprintf("%v is currently offline", s.Name)
}
}
// DowntimeText will return the amount of downtime for a service based on the duration
// service.DowntimeText()
// // Service has been offline for 15 minutes
func (s *Service) DowntimeText() string {
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
}
// Dbtimestamp will return a SQL query for grouping by date
func Dbtimestamp(group string, column string) string {
seconds := 3600
switch group {
case "minute":
seconds = 60
case "hour":
seconds = 3600
case "day":
seconds = 86400
case "week":
seconds = 604800
case "month":
seconds = 2592000
case "year":
seconds = 31557600
default:
seconds = 60
}
switch CoreApp.Config.DbConn {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(%v) AS value", column)
case "postgres":
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(%v) AS value", group, column)
default:
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(%v) as value", seconds, seconds, column)
}
}
// Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration {
hits, _ := s.Hits()
fail := s.lastFailure()
if fail == nil {
return time.Duration(0)
}
if len(hits) == 0 {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var data []DateScan
outgoing := new(DateScanObj)
model := service.(*Service).HitsBetween(start, end, group, column)
model = model.Order("timeframe asc", false).Group("timeframe")
rows, err := model.Rows()
if err != nil {
log.Errorln(fmt.Errorf("issue fetching service chart data: %v", err))
}
for rows.Next() {
var gd DateScan
var createdAt string
var value float64
var createdTime time.Time
var err error
rows.Scan(&createdAt, &value)
if CoreApp.Config.DbConn == "postgres" {
createdTime, err = time.Parse(types.TIME_NANO, createdAt)
if err != nil {
log.Errorln(fmt.Errorf("issue parsing time from database: %v to %v", createdAt, types.TIME_NANO))
}
} else {
createdTime, err = time.Parse(types.TIME, createdAt)
}
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
gd.Value = int64(value * 1000)
data = append(data, gd)
}
outgoing.Array = data
return outgoing
}
// ToString will convert the DateScanObj into a JSON string for the charts to render
func (d *DateScanObj) ToString() string {
data, err := json.Marshal(d.Array)
if err != nil {
log.Warnln(err)
return "{}"
}
return string(data)
}
// AvgUptime24 returns a service's average online status for last 24 hours
func (s *Service) AvgUptime24() string {
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.AvgUptime(ago)
}
// AvgUptime returns average online status for last 24 hours
func (s *Service) AvgUptime(ago time.Time) string {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
return "100"
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
return "0.00"
}
percent := float64(failed) / float64(total) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
}
// TotalUptime returns the total uptime percent of a service
func (s *Service) TotalUptime() string {
hits, _ := s.TotalHits()
failures, _ := s.TotalFailures()
percent := float64(failures) / float64(hits) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
}
// index returns a services index int for updating the []*core.Services slice
func (s *Service) index() int {
for k, service := range CoreApp.Services {
if s.Id == service.(*Service).Id {
return k
}
}
return 0
}
// updateService will update a service in the []*core.Services slice
func updateService(s *Service) {
CoreApp.Services[s.index()] = s
}
// Delete will remove a service from the database, it will also end the service checking go routine
func (s *Service) Delete() error {
i := s.index()
err := servicesDB().Delete(s)
if err.Error != nil {
log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error))
return err.Error
}
s.Close()
slice := CoreApp.Services
CoreApp.Services = append(slice[:i], slice[i+1:]...)
reorderServices()
notifier.OnDeletedService(s.Service)
return err.Error
}
// Update will update a service in the database, the service's checking routine can be restarted by passing true
func (s *Service) Update(restart bool) error {
err := servicesDB().Update(&s)
if err.Error != nil {
log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
return err.Error
}
// clear the notification queue for a service
if !s.AllowNotifications.Bool {
for _, n := range CoreApp.Notifications {
notif := n.(notifier.Notifier).Select()
notif.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
}
}
if restart {
s.Close()
s.Start()
s.SleepDuration = time.Duration(s.Interval) * time.Second
go s.CheckQueue(true)
}
reorderServices()
updateService(s)
notifier.OnUpdatedService(s.Service)
return err.Error
}
// Create will create a service and insert it into the database
func (s *Service) Create(check bool) (int64, error) {
s.CreatedAt = time.Now().UTC()
db := servicesDB().Create(s)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error))
return 0, db.Error
}
s.Start()
go s.CheckQueue(check)
CoreApp.Services = append(CoreApp.Services, s)
reorderServices()
notifier.OnNewService(s.Service)
return s.Id, nil
}
// Messages returns all Messages for a Service
func (s *Service) Messages() []*Message {
messages := SelectServiceMessages(s.Id)
return messages
}
// ActiveMessages returns all service messages that are available based on the current time
func (s *Service) ActiveMessages() []*Message {
var messages []*Message
msgs := SelectServiceMessages(s.Id)
for _, m := range msgs {
if m.StartOn.UTC().After(time.Now().UTC()) {
messages = append(messages, m)
}
}
return messages
}
// CountOnline returns the amount of services online
func (c *Core) CountOnline() int {
amount := 0
for _, s := range CoreApp.Services {
if s.Select().Online {
amount++
}
}
return amount
}

View File

@ -1,100 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var (
testCheckin *Checkin
)
func TestCreateCheckin(t *testing.T) {
service := SelectService(1)
testCheckin = ReturnCheckin(&types.Checkin{
ServiceId: service.Id,
Interval: 10,
GracePeriod: 5,
ApiKey: utils.RandomString(7),
})
id, err := testCheckin.Create()
assert.Nil(t, err)
assert.NotZero(t, id)
assert.NotEmpty(t, testCheckin.ApiKey)
assert.Equal(t, int64(10), testCheckin.Interval)
assert.Equal(t, int64(5), testCheckin.GracePeriod)
assert.True(t, testCheckin.Expected().Minutes() < 0)
}
func TestSelectCheckin(t *testing.T) {
service := SelectService(1)
checkins := service.AllCheckins()
assert.NotNil(t, checkins)
assert.Equal(t, 1, len(checkins))
testCheckin = checkins[0]
assert.Equal(t, int64(10), testCheckin.Interval)
assert.Equal(t, int64(5), testCheckin.GracePeriod)
assert.Equal(t, 7, len(testCheckin.ApiKey))
}
func TestUpdateCheckin(t *testing.T) {
testCheckin.Interval = 60
testCheckin.GracePeriod = 15
id, err := testCheckin.Update()
assert.Nil(t, err)
assert.NotZero(t, id)
assert.NotEmpty(t, testCheckin.ApiKey)
service := SelectService(1)
checkin := service.AllCheckins()[0]
assert.Equal(t, int64(60), checkin.Interval)
assert.Equal(t, int64(15), checkin.GracePeriod)
t.Log(testCheckin.Expected())
assert.True(t, testCheckin.Expected().Minutes() < 0)
}
func TestCreateCheckinHits(t *testing.T) {
service := SelectService(1)
checkins := service.AllCheckins()
assert.Equal(t, 1, len(checkins))
created := time.Now().UTC().Add(-60 * time.Second)
hit := ReturnCheckinHit(&types.CheckinHit{
Checkin: testCheckin.Id,
From: "192.168.1.1",
CreatedAt: created,
})
hit.Create()
hits := testCheckin.AllHits()
assert.Equal(t, 1, len(hits))
}
func TestSelectCheckinMethods(t *testing.T) {
time.Sleep(5 * time.Second)
service := SelectService(1)
checkins := service.AllCheckins()
assert.NotNil(t, checkins)
lastHit := testCheckin.Last()
assert.Equal(t, float64(60), testCheckin.Period().Seconds())
assert.Equal(t, float64(15), testCheckin.Grace().Seconds())
t.Log(testCheckin.Expected())
assert.True(t, testCheckin.Expected().Seconds() < -5)
assert.False(t, lastHit.CreatedAt.IsZero())
assert.Equal(t, "A minute ago", lastHit.Ago())
}

View File

@ -1,411 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var (
newServiceId int64
)
func TestSelectHTTPService(t *testing.T) {
services, err := CoreApp.SelectAllServices(false)
assert.Nil(t, err)
assert.Equal(t, 15, len(services))
assert.Equal(t, "Google", services[0].Name)
assert.Equal(t, "http", services[0].Type)
}
func TestSelectAllServices(t *testing.T) {
services := CoreApp.Services
for _, s := range services {
service := s.(*Service)
service.Check(false)
assert.False(t, service.IsRunning())
t.Logf("ID: %v %v\n", service.Id, service.Name)
}
assert.Equal(t, 15, len(services))
}
func TestServiceDowntime(t *testing.T) {
t.SkipNow()
service := SelectService(15)
downtime := service.Downtime()
assert.True(t, downtime.Seconds() > 0)
}
func TestSelectTCPService(t *testing.T) {
services := CoreApp.Services
assert.Equal(t, 15, len(services))
service := SelectService(5)
assert.NotNil(t, service)
assert.Equal(t, "Google DNS", service.Name)
assert.Equal(t, "tcp", service.Type)
}
func TestUpdateService(t *testing.T) {
service := SelectService(1)
assert.Equal(t, "Google", service.Name)
service.Name = "Updated Google"
service.Interval = 5
err := service.Update(true)
assert.Nil(t, err)
// check if updating pointer array shutdown any other service
service = SelectService(1)
assert.Equal(t, "Updated Google", service.Name)
assert.Equal(t, 5, service.Interval)
}
func TestUpdateAllServices(t *testing.T) {
services, err := CoreApp.SelectAllServices(false)
assert.Nil(t, err)
for k, srv := range services {
srv.Name = "Changed " + srv.Name
srv.Interval = k + 3
err := srv.Update(true)
assert.Nil(t, err)
}
}
func TestServiceHTTPCheck(t *testing.T) {
service := SelectService(1)
service.Check(true)
assert.Equal(t, "Changed Updated Google", service.Name)
assert.True(t, service.Online)
}
func TestCheckHTTPService(t *testing.T) {
service := SelectService(1)
assert.Equal(t, "Changed Updated Google", service.Name)
assert.True(t, service.Online)
assert.Equal(t, 200, service.LastStatusCode)
assert.NotZero(t, service.Latency)
assert.NotZero(t, service.PingTime)
}
func TestServiceTCPCheck(t *testing.T) {
service := SelectService(5)
service.Check(true)
assert.Equal(t, "Changed Google DNS", service.Name)
assert.True(t, service.Online)
}
func TestCheckTCPService(t *testing.T) {
service := SelectService(5)
assert.Equal(t, "Changed Google DNS", service.Name)
assert.True(t, service.Online)
assert.NotZero(t, service.Latency)
assert.NotZero(t, service.PingTime)
}
func TestServiceOnline24Hours(t *testing.T) {
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)
assert.Equal(t, float32(100), service.OnlineSince(since))
service2 := SelectService(5)
assert.Equal(t, float32(100), service2.OnlineSince(since))
service3 := SelectService(14)
assert.True(t, service3.OnlineSince(since) > float32(49))
}
func TestServiceSmallText(t *testing.T) {
service := SelectService(5)
text := service.SmallText()
assert.Contains(t, text, "Online since")
}
func TestServiceAvgUptime(t *testing.T) {
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)
assert.NotEqual(t, "0.00", service.AvgUptime(since))
service2 := SelectService(5)
assert.Equal(t, "100", service2.AvgUptime(since))
service3 := SelectService(13)
assert.NotEqual(t, "0", service3.AvgUptime(since))
service4 := SelectService(15)
assert.NotEqual(t, "0", service4.AvgUptime(since))
}
func TestServiceHits(t *testing.T) {
service := SelectService(5)
hits, err := service.Hits()
assert.Nil(t, err)
assert.True(t, len(hits) > 1400)
}
func TestServiceLimitedHits(t *testing.T) {
service := SelectService(5)
hits, err := service.LimitedHits(1024)
assert.Nil(t, err)
assert.Equal(t, int(1024), len(hits))
}
func TestServiceTotalHits(t *testing.T) {
service := SelectService(5)
hits, err := service.TotalHits()
assert.Nil(t, err)
assert.NotZero(t, hits)
}
func TestServiceSum(t *testing.T) {
service := SelectService(5)
sum := service.Sum()
assert.NotZero(t, sum)
}
func TestCountOnline(t *testing.T) {
amount := CoreApp.CountOnline()
assert.True(t, amount >= 2)
}
func TestCreateService(t *testing.T) {
s := ReturnService(&types.Service{
Name: "That'll do 🐢",
Domain: "https://www.youtube.com/watch?v=rjQtzV9IZ0Q",
ExpectedStatus: 200,
Interval: 3,
Type: "http",
Method: "GET",
Timeout: 20,
GroupId: 1,
})
var err error
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
assert.Equal(t, "That'll do 🐢", newService.Name)
}
func TestViewNewService(t *testing.T) {
newService := SelectService(newServiceId)
assert.Equal(t, "That'll do 🐢", newService.Name)
}
func TestCreateFailingHTTPService(t *testing.T) {
s := ReturnService(&types.Service{
Name: "Bad URL",
Domain: "http://localhost/iamnothere",
ExpectedStatus: 200,
Interval: 2,
Type: "http",
Method: "GET",
Timeout: 5,
GroupId: 1,
})
var err error
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
assert.Equal(t, "Bad URL", newService.Name)
t.Log("new service ID: ", newServiceId)
}
func TestServiceFailedCheck(t *testing.T) {
service := SelectService(17)
assert.Equal(t, "Bad URL", service.Name)
service.Check(false)
assert.Equal(t, "Bad URL", service.Name)
assert.False(t, service.Online)
}
func TestCreateFailingTCPService(t *testing.T) {
s := ReturnService(&types.Service{
Name: "Bad TCP",
Domain: "localhost",
Port: 5050,
Interval: 30,
Type: "tcp",
Timeout: 5,
GroupId: 1,
})
var err error
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
assert.Equal(t, "Bad TCP", newService.Name)
t.Log("new failing tcp service ID: ", newServiceId)
}
func TestServiceFailedTCPCheck(t *testing.T) {
service := SelectService(newServiceId)
service.Check(false)
assert.Equal(t, "Bad TCP", service.Name)
assert.False(t, service.Online)
}
func TestCreateServiceFailure(t *testing.T) {
fail := &types.Failure{
Issue: "This is not an issue, but it would container HTTP response errors.",
Method: "http",
}
service := SelectService(8)
id, err := service.CreateFailure(fail)
assert.Nil(t, err)
assert.NotZero(t, id)
}
func TestDeleteService(t *testing.T) {
service := SelectService(newServiceId)
count, err := CoreApp.SelectAllServices(false)
assert.Nil(t, err)
assert.Equal(t, 18, len(count))
err = service.Delete()
assert.Nil(t, err)
services := CoreApp.Services
assert.Equal(t, 17, len(services))
}
func TestServiceCloseRoutine(t *testing.T) {
s := ReturnService(new(types.Service))
s.Name = "example"
s.Domain = "https://google.com"
s.Type = "http"
s.Method = "GET"
s.ExpectedStatus = 200
s.Interval = 1
s.Start()
assert.True(t, s.IsRunning())
t.Log(s.Checkpoint)
t.Log(s.SleepDuration)
go s.CheckQueue(false)
t.Log(s.Checkpoint)
t.Log(s.SleepDuration)
time.Sleep(5 * time.Second)
t.Log(s.Checkpoint)
t.Log(s.SleepDuration)
assert.True(t, s.IsRunning())
s.Close()
assert.False(t, s.IsRunning())
s.Close()
assert.False(t, s.IsRunning())
}
func TestServiceCheckQueue(t *testing.T) {
s := ReturnService(new(types.Service))
s.Name = "example"
s.Domain = "https://google.com"
s.Type = "http"
s.Method = "GET"
s.ExpectedStatus = 200
s.Interval = 1
s.Start()
assert.True(t, s.IsRunning())
go s.CheckQueue(false)
go func() {
time.Sleep(5 * time.Second)
t.Log(s.Checkpoint)
time.Sleep(6 * time.Second)
}()
time.Sleep(5 * time.Second)
assert.True(t, s.IsRunning())
s.Close()
assert.False(t, s.IsRunning())
s.Close()
assert.False(t, s.IsRunning())
}
func TestDNScheckService(t *testing.T) {
s := ReturnService(new(types.Service))
s.Name = "example"
s.Domain = "http://localhost:9000"
s.Type = "http"
s.Method = "GET"
s.ExpectedStatus = 200
s.Interval = 1
amount, err := s.dnsCheck()
assert.Nil(t, err)
assert.NotZero(t, amount)
}
func TestSelectServiceLink(t *testing.T) {
service := SelectService(1)
assert.Equal(t, "google", service.Permalink.String)
}
func TestDbtimestamp(t *testing.T) {
CoreApp.Config.DbConn = "mysql"
query := Dbtimestamp("minute", "latency")
assert.Equal(t, "CONCAT(date_format(created_at, '%Y-%m-%d %H:00:00')) AS timeframe, AVG(latency) AS value", query)
CoreApp.Config.DbConn = "postgres"
query = Dbtimestamp("minute", "latency")
assert.Equal(t, "date_trunc('minute', created_at) AS timeframe, AVG(latency) AS value", query)
CoreApp.Config.DbConn = "sqlite"
query = Dbtimestamp("minute", "latency")
assert.Equal(t, "datetime((strftime('%s', created_at) / 60) * 60, 'unixepoch') AS timeframe, AVG(latency) as value", query)
}
func TestGroup_Create(t *testing.T) {
group := &Group{&types.Group{
Name: "Testing",
}}
newGroupId, err := group.Create()
assert.Nil(t, err)
assert.NotZero(t, newGroupId)
}
func TestGroup_Services(t *testing.T) {
group := SelectGroup(1)
assert.NotEmpty(t, group.Services())
}
func TestSelectGroups(t *testing.T) {
groups := SelectGroups(true, false)
assert.Equal(t, int(3), len(groups))
groups = SelectGroups(true, true)
assert.Equal(t, int(5), len(groups))
}
func TestService_TotalFailures(t *testing.T) {
service := SelectService(8)
failures, err := service.TotalFailures()
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestService_TotalFailures24(t *testing.T) {
service := SelectService(8)
failures, err := service.TotalFailures24()
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestService_TotalFailuresOnDate(t *testing.T) {
t.SkipNow()
ago := utils.Now().UTC()
service := SelectService(8)
failures, err := service.TotalFailuresOnDate(ago)
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestCountFailures(t *testing.T) {
failures := CountFailures()
assert.NotEqual(t, uint64(0), failures)
}

View File

@ -1,31 +0,0 @@
package core
import (
"github.com/hunterlong/statping/utils"
"strings"
"time"
)
// SparklineDayFailures returns a string array of daily service failures
func (s *Service) SparklineDayFailures(days int) string {
var arr []string
ago := time.Now().UTC().Add((time.Duration(days) * -24) * time.Hour)
for day := 1; day <= days; day++ {
ago = ago.Add(24 * time.Hour)
failures, _ := s.TotalFailuresOnDate(ago)
arr = append(arr, utils.ToString(failures))
}
return "[" + strings.Join(arr, ",") + "]"
}
// SparklineHourResponse returns a string array for the average response or ping time for a service
func (s *Service) SparklineHourResponse(hours int, method string) string {
var arr []string
end := time.Now().UTC()
start := end.Add(time.Duration(-hours) * time.Hour)
obj := GraphDataRaw(s, start, end, "hour", method)
for _, v := range obj.Array {
arr = append(arr, utils.ToString(v.Value))
}
return "[" + strings.Join(arr, ",") + "]"
}

View File

@ -1,115 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"golang.org/x/crypto/bcrypt"
"time"
)
type User struct {
*types.User
}
// ReturnUser returns *core.User based off a *types.User
func ReturnUser(u *types.User) *User {
return &User{u}
}
// CountUsers returns the amount of users
func CountUsers() int64 {
var amount int64
usersDB().Count(&amount)
return amount
}
// SelectUser returns the User based on the User's ID.
func SelectUser(id int64) (*User, error) {
var user User
err := usersDB().Where("id = ?", id).First(&user)
return &user, err.Error
}
// SelectUsername returns the User based on the User's username
func SelectUsername(username string) (*User, error) {
var user User
res := usersDB().Where("username = ?", username)
err := res.First(&user)
return &user, err.Error
}
// Delete will remove the User record from the database
func (u *User) Delete() error {
return usersDB().Delete(u).Error
}
// Update will update the User's record in database
func (u *User) Update() error {
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
return usersDB().Update(u).Error
}
// Create will insert a new User into the database
func (u *User) Create() (int64, error) {
u.CreatedAt = time.Now().UTC()
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
db := usersDB().Create(u)
if db.Error != nil {
return 0, db.Error
}
if u.Id == 0 {
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error))
return 0, db.Error
}
return u.Id, db.Error
}
// SelectAllUsers returns all users
func SelectAllUsers() ([]*User, error) {
var users []*User
db := usersDB().Find(&users)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to load all users. %v", db.Error))
return nil, db.Error
}
return users, db.Error
}
// AuthUser will return the User and a boolean if authentication was correct.
// AuthUser accepts username, and password as a string
func AuthUser(username, password string) (*User, bool) {
user, err := SelectUsername(username)
if err != nil {
log.Warnln(fmt.Errorf("user %v not found", username))
return nil, false
}
if CheckHash(password, user.Password) {
return user, true
}
return nil, false
}
// CheckHash returns true if the password matches with a hashed bcrypt password
func CheckHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

View File

@ -1,118 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"github.com/hunterlong/statping/types"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateUser(t *testing.T) {
user := ReturnUser(&types.User{
Username: "hunter",
Password: "password123",
Email: "test@email.com",
Admin: types.NewNullBool(true),
})
userId, err := user.Create()
assert.Nil(t, err)
assert.NotZero(t, userId)
}
func TestSelectAllUsers(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
assert.Equal(t, 3, len(users))
}
func TestSelectUser(t *testing.T) {
user, err := SelectUser(1)
assert.Nil(t, err)
assert.Equal(t, "info@betatude.com", user.Email)
assert.True(t, user.Admin.Bool)
}
func TestSelectUsername(t *testing.T) {
user, err := SelectUsername("hunter")
assert.Nil(t, err)
assert.Equal(t, "test@email.com", user.Email)
assert.Equal(t, int64(3), user.Id)
assert.True(t, user.Admin.Bool)
}
func TestUpdateUser(t *testing.T) {
user, err := SelectUser(1)
assert.Nil(t, err)
user.Username = "updated"
err = user.Update()
assert.Nil(t, err)
updatedUser, err := SelectUser(1)
assert.Nil(t, err)
assert.Equal(t, "updated", updatedUser.Username)
}
func TestCreateUser2(t *testing.T) {
user := ReturnUser(&types.User{
Username: "hunterlong",
Password: "password123",
Email: "User@email.com",
Admin: types.NewNullBool(true),
})
userId, err := user.Create()
assert.Nil(t, err)
assert.NotZero(t, userId)
}
func TestSelectAllUsersAgain(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
assert.Equal(t, 4, len(users))
}
func TestAuthUser(t *testing.T) {
user, auth := AuthUser("hunterlong", "password123")
assert.True(t, auth)
assert.NotNil(t, user)
assert.Equal(t, "User@email.com", user.Email)
assert.Equal(t, int64(4), user.Id)
assert.True(t, user.Admin.Bool)
}
func TestFailedAuthUser(t *testing.T) {
user, auth := AuthUser("hunterlong", "wrongpassword")
assert.False(t, auth)
assert.Nil(t, user)
}
func TestCheckPassword(t *testing.T) {
user, err := SelectUser(2)
assert.Nil(t, err)
pass := CheckHash("password123", user.Password)
assert.True(t, pass)
}
func TestDeleteUser(t *testing.T) {
user, err := SelectUser(2)
assert.Nil(t, err)
err = user.Delete()
assert.Nil(t, err)
}
func TestDbConfig_Close(t *testing.T) {
err := DbSession.Close()
assert.Nil(t, err)
}

503
database/database.go Normal file
View File

@ -0,0 +1,503 @@
package database
import (
"database/sql"
"fmt"
"github.com/jinzhu/gorm"
"github.com/statping/statping/utils"
"strings"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
const (
TIME_NANO = "2006-01-02T15:04:05Z"
TIME = "2006-01-02 15:04:05"
CHART_TIME = "2006-01-02T15:04:05.999999-07:00"
TIME_DAY = "2006-01-02"
)
var database Database
// Database is an interface which DB implements
type Database interface {
Close() error
DB() *sql.DB
New() Database
NewScope(value interface{}) *gorm.Scope
CommonDB() gorm.SQLCommon
Callback() *gorm.Callback
SetLogger(l gorm.Logger)
LogMode(enable bool) Database
SingularTable(enable bool)
Where(query interface{}, args ...interface{}) Database
Or(query interface{}, args ...interface{}) Database
Not(query interface{}, args ...interface{}) Database
Limit(value int) Database
Offset(value int) Database
Order(value string, reorder ...bool) Database
Select(query interface{}, args ...interface{}) Database
Omit(columns ...string) Database
Group(query string) Database
Having(query string, values ...interface{}) Database
Joins(query string, args ...interface{}) Database
Scopes(funcs ...func(*gorm.DB) *gorm.DB) Database
Unscoped() Database
Attrs(attrs ...interface{}) Database
Assign(attrs ...interface{}) Database
First(out interface{}, where ...interface{}) Database
Last(out interface{}, where ...interface{}) Database
Find(out interface{}, where ...interface{}) Database
Scan(dest interface{}) Database
Row() *sql.Row
Rows() (*sql.Rows, error)
ScanRows(rows *sql.Rows, result interface{}) error
Pluck(column string, value interface{}) Database
Count(value interface{}) Database
Related(value interface{}, foreignKeys ...string) Database
FirstOrInit(out interface{}, where ...interface{}) Database
FirstOrCreate(out interface{}, where ...interface{}) Database
Update(attrs ...interface{}) Database
Updates(values interface{}, ignoreProtectedAttrs ...bool) Database
UpdateColumn(attrs ...interface{}) Database
UpdateColumns(values interface{}) Database
Save(value interface{}) Database
Create(value interface{}) Database
Delete(value interface{}, where ...interface{}) Database
Raw(sql string, values ...interface{}) Database
Exec(sql string, values ...interface{}) Database
Model(value interface{}) Database
Table(name string) Database
Debug() Database
Begin() Database
Commit() Database
Rollback() Database
NewRecord(value interface{}) bool
RecordNotFound() bool
CreateTable(values ...interface{}) Database
DropTable(values ...interface{}) Database
DropTableIfExists(values ...interface{}) Database
HasTable(value interface{}) bool
AutoMigrate(values ...interface{}) Database
ModifyColumn(column string, typ string) Database
DropColumn(column string) Database
AddIndex(indexName string, column ...string) Database
AddUniqueIndex(indexName string, column ...string) Database
RemoveIndex(indexName string) Database
AddForeignKey(field string, dest string, onDelete string, onUpdate string) Database
Association(column string) *gorm.Association
Preload(column string, conditions ...interface{}) Database
Set(name string, value interface{}) Database
InstantSet(name string, value interface{}) Database
Get(name string) (value interface{}, ok bool)
SetJoinTableHandler(source interface{}, column string, handler gorm.JoinTableHandlerInterface)
AddError(err error) error
GetErrors() (errors []error)
// extra
Error() error
RowsAffected() int64
Since(time.Time) Database
Between(time.Time, time.Time) Database
SelectByTime(time.Duration) string
MultipleSelects(args ...string) Database
FormatTime(t time.Time) string
ParseTime(t string) (time.Time, error)
DbType() string
}
func (it *Db) DbType() string {
return it.Database.Dialect().GetName()
}
func Close(db Database) error {
if db == nil {
return nil
}
return db.Close()
}
func LogMode(db Database, b bool) Database {
return db.LogMode(b)
}
func Begin(db Database, model interface{}) Database {
if all, ok := model.(string); ok {
if all == "migration" {
return db.Begin()
}
}
return db.Model(model).Begin()
}
func Available(db Database) bool {
if db == nil {
return false
}
if err := db.DB().Ping(); err != nil {
return false
}
return true
}
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("service = ?", 1000)
}
func (it *Db) MultipleSelects(args ...string) Database {
joined := strings.Join(args, ", ")
return it.Select(joined)
}
func OrderStatus(status []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Scopes(AmountGreaterThan1000).Where("status IN (?)", status)
}
}
type Db struct {
Database *gorm.DB
Type string
}
// Openw is a drop-in replacement for Open()
func Openw(dialect string, args ...interface{}) (db Database, err error) {
gorm.NowFunc = func() time.Time {
return time.Now().UTC()
}
gormdb, err := gorm.Open(dialect, args...)
if err != nil {
return nil, err
}
database = Wrap(gormdb)
return database, err
}
func OpenTester() (Database, error) {
newDb, err := Openw("sqlite3", fmt.Sprintf("file:%s?mode=memory&cache=shared", utils.RandomString(12)))
newDb.DB().SetMaxOpenConns(1)
return newDb, err
}
// Wrap wraps gorm.DB in an interface
func Wrap(db *gorm.DB) Database {
return &Db{
Database: db,
Type: db.Dialect().GetName(),
}
}
func (it *Db) Close() error {
return it.Database.Close()
}
func (it *Db) DB() *sql.DB {
return it.Database.DB()
}
func (it *Db) New() Database {
return Wrap(it.Database.New())
}
func (it *Db) NewScope(value interface{}) *gorm.Scope {
return it.Database.NewScope(value)
}
func (it *Db) CommonDB() gorm.SQLCommon {
return it.Database.CommonDB()
}
func (it *Db) Callback() *gorm.Callback {
return it.Database.Callback()
}
func (it *Db) SetLogger(log gorm.Logger) {
it.Database.SetLogger(log)
}
func (it *Db) LogMode(enable bool) Database {
return Wrap(it.Database.LogMode(enable))
}
func (it *Db) SingularTable(enable bool) {
it.Database.SingularTable(enable)
}
func (it *Db) Where(query interface{}, args ...interface{}) Database {
return Wrap(it.Database.Where(query, args...))
}
func (it *Db) Or(query interface{}, args ...interface{}) Database {
return Wrap(it.Database.Or(query, args...))
}
func (it *Db) Not(query interface{}, args ...interface{}) Database {
return Wrap(it.Database.Not(query, args...))
}
func (it *Db) Limit(value int) Database {
return Wrap(it.Database.Limit(value))
}
func (it *Db) Offset(value int) Database {
return Wrap(it.Database.Offset(value))
}
func (it *Db) Order(value string, reorder ...bool) Database {
return Wrap(it.Database.Order(value, reorder...))
}
func (it *Db) Select(query interface{}, args ...interface{}) Database {
return Wrap(it.Database.Select(query, args...))
}
func (it *Db) Omit(columns ...string) Database {
return Wrap(it.Database.Omit(columns...))
}
func (it *Db) Group(query string) Database {
return Wrap(it.Database.Group(query))
}
func (it *Db) Having(query string, values ...interface{}) Database {
return Wrap(it.Database.Having(query, values...))
}
func (it *Db) Joins(query string, args ...interface{}) Database {
return Wrap(it.Database.Joins(query, args...))
}
func (it *Db) Scopes(funcs ...func(*gorm.DB) *gorm.DB) Database {
return Wrap(it.Database.Scopes(funcs...))
}
func (it *Db) Unscoped() Database {
return Wrap(it.Database.Unscoped())
}
func (it *Db) Attrs(attrs ...interface{}) Database {
return Wrap(it.Database.Attrs(attrs...))
}
func (it *Db) Assign(attrs ...interface{}) Database {
return Wrap(it.Database.Assign(attrs...))
}
func (it *Db) First(out interface{}, where ...interface{}) Database {
return Wrap(it.Database.First(out, where...))
}
func (it *Db) Last(out interface{}, where ...interface{}) Database {
return Wrap(it.Database.Last(out, where...))
}
func (it *Db) Find(out interface{}, where ...interface{}) Database {
return Wrap(it.Database.Find(out, where...))
}
func (it *Db) Scan(dest interface{}) Database {
return Wrap(it.Database.Scan(dest))
}
func (it *Db) Row() *sql.Row {
return it.Database.Row()
}
func (it *Db) Rows() (*sql.Rows, error) {
return it.Database.Rows()
}
func (it *Db) ScanRows(rows *sql.Rows, result interface{}) error {
return it.Database.ScanRows(rows, result)
}
func (it *Db) Pluck(column string, value interface{}) Database {
return Wrap(it.Database.Pluck(column, value))
}
func (it *Db) Count(value interface{}) Database {
return Wrap(it.Database.Count(value))
}
func (it *Db) Related(value interface{}, foreignKeys ...string) Database {
return Wrap(it.Database.Related(value, foreignKeys...))
}
func (it *Db) FirstOrInit(out interface{}, where ...interface{}) Database {
return Wrap(it.Database.FirstOrInit(out, where...))
}
func (it *Db) FirstOrCreate(out interface{}, where ...interface{}) Database {
return Wrap(it.Database.FirstOrCreate(out, where...))
}
func (it *Db) Update(attrs ...interface{}) Database {
return Wrap(it.Database.Update(attrs...))
}
func (it *Db) Updates(values interface{}, ignoreProtectedAttrs ...bool) Database {
return Wrap(it.Database.Updates(values, ignoreProtectedAttrs...))
}
func (it *Db) UpdateColumn(attrs ...interface{}) Database {
return Wrap(it.Database.UpdateColumn(attrs...))
}
func (it *Db) UpdateColumns(values interface{}) Database {
return Wrap(it.Database.UpdateColumns(values))
}
func (it *Db) Save(value interface{}) Database {
return Wrap(it.Database.Save(value))
}
func (it *Db) Create(value interface{}) Database {
return Wrap(it.Database.Create(value))
}
func (it *Db) Delete(value interface{}, where ...interface{}) Database {
return Wrap(it.Database.Delete(value, where...))
}
func (it *Db) Raw(sql string, values ...interface{}) Database {
return Wrap(it.Database.Raw(sql, values...))
}
func (it *Db) Exec(sql string, values ...interface{}) Database {
return Wrap(it.Database.Exec(sql, values...))
}
func (it *Db) Model(value interface{}) Database {
return Wrap(it.Database.Model(value))
}
func (it *Db) Table(name string) Database {
return Wrap(it.Database.Table(name))
}
func (it *Db) Debug() Database {
return Wrap(it.Database.Debug())
}
func (it *Db) Begin() Database {
return Wrap(it.Database.Begin())
}
func (it *Db) Commit() Database {
return Wrap(it.Database.Commit())
}
func (it *Db) Rollback() Database {
return Wrap(it.Database.Rollback())
}
func (it *Db) NewRecord(value interface{}) bool {
return it.Database.NewRecord(value)
}
func (it *Db) RecordNotFound() bool {
return it.Database.RecordNotFound()
}
func (it *Db) CreateTable(values ...interface{}) Database {
return Wrap(it.Database.CreateTable(values...))
}
func (it *Db) DropTable(values ...interface{}) Database {
return Wrap(it.Database.DropTable(values...))
}
func (it *Db) DropTableIfExists(values ...interface{}) Database {
return Wrap(it.Database.DropTableIfExists(values...))
}
func (it *Db) HasTable(value interface{}) bool {
return it.Database.HasTable(value)
}
func (it *Db) AutoMigrate(values ...interface{}) Database {
return Wrap(it.Database.AutoMigrate(values...))
}
func (it *Db) ModifyColumn(column string, typ string) Database {
return Wrap(it.Database.ModifyColumn(column, typ))
}
func (it *Db) DropColumn(column string) Database {
return Wrap(it.Database.DropColumn(column))
}
func (it *Db) AddIndex(indexName string, columns ...string) Database {
return Wrap(it.Database.AddIndex(indexName, columns...))
}
func (it *Db) AddUniqueIndex(indexName string, columns ...string) Database {
return Wrap(it.Database.AddUniqueIndex(indexName, columns...))
}
func (it *Db) RemoveIndex(indexName string) Database {
return Wrap(it.Database.RemoveIndex(indexName))
}
func (it *Db) Association(column string) *gorm.Association {
return it.Database.Association(column)
}
func (it *Db) Preload(column string, conditions ...interface{}) Database {
return Wrap(it.Database.Preload(column, conditions...))
}
func (it *Db) Set(name string, value interface{}) Database {
return Wrap(it.Database.Set(name, value))
}
func (it *Db) InstantSet(name string, value interface{}) Database {
return Wrap(it.Database.InstantSet(name, value))
}
func (it *Db) Get(name string) (interface{}, bool) {
return it.Database.Get(name)
}
func (it *Db) SetJoinTableHandler(source interface{}, column string, handler gorm.JoinTableHandlerInterface) {
it.Database.SetJoinTableHandler(source, column, handler)
}
func (it *Db) AddForeignKey(field string, dest string, onDelete string, onUpdate string) Database {
return Wrap(it.Database.AddForeignKey(field, dest, onDelete, onUpdate))
}
func (it *Db) AddError(err error) error {
return it.Database.AddError(err)
}
func (it *Db) GetErrors() (errors []error) {
return it.Database.GetErrors()
}
func (it *Db) RowsAffected() int64 {
return it.Database.RowsAffected
}
func (it *Db) Error() error {
return it.Database.Error
}
func (it *Db) Since(ago time.Time) Database {
return it.Where("created_at > ?", it.FormatTime(ago))
}
func (it *Db) Between(t1 time.Time, t2 time.Time) Database {
return it.Where("created_at BETWEEN ? AND ?", it.FormatTime(t1), it.FormatTime(t2))
}
type TimeValue struct {
Timeframe string `json:"timeframe"`
Amount int64 `json:"amount"`
}

222
database/grouping.go Normal file
View File

@ -0,0 +1,222 @@
package database
import (
"errors"
"fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"net/http"
"net/url"
"strconv"
"time"
)
type GroupBy struct {
db Database
query *GroupQuery
}
type GroupByer interface {
ToTimeValue() (*TimeVar, error)
}
type By string
func (b By) String() string {
return string(b)
}
type GroupQuery struct {
Start time.Time
End time.Time
Group time.Duration
Order string
Limit int
Offset int
FillEmpty bool
db Database
}
func (b GroupQuery) Find(data interface{}) error {
return b.db.Find(data).Error()
}
func (b GroupQuery) Database() Database {
return b.db
}
var (
ByCount = By("COUNT(id) as amount")
ByAverage = func(column string, multiplier int) By {
switch database.DbType() {
case "mysql":
return By(fmt.Sprintf("CAST(AVG(%s) as UNSIGNED) as amount", column))
case "postgres":
return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
default:
return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
}
}
)
type TimeVar struct {
g *GroupQuery
data []*TimeValue
}
func (t *TimeVar) ToValues() ([]*TimeValue, error) {
return t.data, nil
}
// GraphData will return all hits or failures
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
by.String(),
).Group("timeframe")
g.db = dbQuery
caller, err := g.ToTimeValue()
if err != nil {
return nil, err
}
if g.FillEmpty {
return caller.FillMissing(g.Start, g.End)
}
return caller.ToValues()
}
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows()
if err != nil {
return nil, err
}
var data []*TimeValue
for rows.Next() {
var timeframe string
amount := int64(0)
if err := rows.Scan(&timeframe, &amount); err != nil {
log.Error(err, timeframe)
}
trueTime, _ := g.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, g.Group)
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
})
}
return &TimeVar{g, data}, nil
}
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
timeMap := make(map[string]int64)
var validSet []*TimeValue
dur := t.g.Group
for _, v := range t.data {
timeMap[v.Timeframe] = v.Amount
}
currentStr := types.FixedTime(current, t.g.Group)
for {
var amount int64
if timeMap[currentStr] != 0 {
amount = timeMap[currentStr]
}
validSet = append(validSet, &TimeValue{
Timeframe: currentStr,
Amount: amount,
})
if current.After(end) {
break
}
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.Group)
}
return validSet, nil
}
type isObject interface {
Db() Database
}
func ParseQueries(r *http.Request, o isObject) (*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
}
q := o.Db()
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,
db: q,
}
if query.Start.After(query.End) {
return nil, errors.New("start time is after ending time")
}
if startField == 0 {
query.Start = utils.Now().Add(-7 * types.Day)
}
if endField == 0 {
query.End = utils.Now()
}
if query.End.After(utils.Now()) {
query.End = utils.Now()
}
if query.Limit != 0 {
q = q.Limit(query.Limit)
}
if query.Offset > 0 {
q = q.Offset(query.Offset)
}
q = q.Where("created_at BETWEEN ? AND ?", q.FormatTime(query.Start), q.FormatTime(query.End))
if query.Order != "" {
q = q.Order(query.Order)
}
query.db = q
return query, nil
}
func parseForm(r *http.Request) url.Values {
r.ParseForm()
return r.PostForm
}
func parseGet(r *http.Request) url.Values {
r.ParseForm()
return r.Form
}

11
database/interface.go Normal file
View File

@ -0,0 +1,11 @@
package database
type DbObject interface {
Create() error
Update() error
Delete() error
}
type Sampler interface {
Sample() DbObject
}

66
database/routines.go Normal file
View File

@ -0,0 +1,66 @@
package database
import (
"fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"os"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
var (
log = utils.Log
removeRowsAfter = types.Month * 6
maintenceDuration = types.Hour
)
func StartMaintenceRoutine() {
dur := os.Getenv("REMOVE_AFTER")
var removeDur time.Duration
if dur != "" {
parsedDur, err := time.ParseDuration(dur)
if err != nil {
log.Errorf("could not parse duration: %s, using default: %s", dur, removeRowsAfter.String())
removeDur = removeRowsAfter
} else {
removeDur = parsedDur
}
} else {
removeDur = removeRowsAfter
}
log.Infof("Service Failure and Hit records will be automatically removed after %s", removeDur.String())
go databaseMaintence(removeDur)
}
// databaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func databaseMaintence(dur time.Duration) {
//deleteAfter := time.Now().UTC().Add(dur)
time.Sleep(20 * types.Second)
for range time.Tick(maintenceDuration) {
log.Infof("Deleting failures older than %s", dur.String())
//DeleteAllSince("failures", deleteAfter)
log.Infof("Deleting hits older than %s", dur.String())
//DeleteAllSince("hits", deleteAfter)
maintenceDuration = types.Hour
}
}
// DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %s WHERE created_at < '%s';", table, database.FormatTime(date))
q := database.Exec(sql).Debug()
if q.Error() != nil {
log.Warnln(q.Error())
}
}

73
database/time.go Normal file
View File

@ -0,0 +1,73 @@
package database
import (
"fmt"
"time"
)
type TimeGroup interface {
}
func (it *Db) ParseTime(t string) (time.Time, error) {
switch it.Type {
case "mysql":
return time.Parse("2006-01-02 15:04:05", t)
case "postgres":
return time.Parse("2006-01-02T15:04:05Z", t)
default:
return time.Parse("2006-01-02 15:04:05", t)
}
}
func (it *Db) FormatTime(t time.Time) string {
switch it.Type {
case "mysql":
return t.Format("2006-01-02 15:04:05")
case "postgres":
return t.Format("2006-01-02 15:04:05.999999999")
default:
return t.Format("2006-01-02 15:04:05")
}
}
func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int(increment.Seconds())
switch it.Type {
case "mysql":
return fmt.Sprintf("FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / %d) * %d) AS timeframe", seconds, seconds)
case "postgres":
return fmt.Sprintf("date_trunc('%s', created_at) AS timeframe", increment)
default:
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
}
}
func (it *Db) correctTimestamp(increment string) string {
var timestamper string
switch increment {
case "second":
timestamper = "%Y-%m-%d %H:%M:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%M:00"
case "hour":
timestamper = "%Y-%m-%d %H:00:00"
case "day":
timestamper = "%Y-%m-%d 00:00:00"
case "month":
timestamper = "%Y-%m-01 00:00:00"
case "year":
timestamper = "%Y-01-01 00:00:00"
default:
timestamper = "%Y-%m-%d 00:00:00"
}
switch it.Type {
case "mysql":
case "second":
timestamper = "%Y-%m-%d %H:%i:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%i:00"
}
return timestamper
}

2209
dev/COVERAGE.html vendored

File diff suppressed because one or more lines are too long

17
dev/Dockerfile vendored
View File

@ -1,17 +0,0 @@
FROM golang:1.13.5-alpine as base
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
RUN apk add --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
WORKDIR /go/src/github.com/hunterlong/statping
ADD . /go/src/github.com/hunterlong/statping
RUN go mod vendor
RUN make dev-deps
RUN make install
ENV IS_DOCKER=true
ENV STATPING_DIR=/app
WORKDIR /app
CMD ["statping"]

View File

@ -1,15 +0,0 @@
FROM cypress/browsers:chrome67
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
# Statping 'test' image for running a full test using the production environment
WORKDIR $HOME/statping
ADD dev/test .
RUN npm install node-sass
ENV SASS=node-sass
RUN npm install
ADD ./statping-linux-amd64 /usr/local/bin/statping
RUN statping version
RUN npm run test-docker

26
dev/Dockerfile-demo vendored
View File

@ -1,26 +0,0 @@
FROM alpine
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ENV STATPING_VERSION=0.80.35
RUN apk add --no-cache ca-certificates linux-headers curl
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
RUN curl -L -s https://github.com/hunterlong/statping/releases/download/v$STATPING_VERSION/statping-linux-alpine.tar.gz | tar -xz && \
chmod +x statping && mv statping /usr/local/bin/statping
ENV DB_CONN=sqlite
ENV NAME="Statping Demo"
ENV DESCRIPTION="An Awesome Demo of a Statping Server running on Docker"
ENV DOMAIN=demo.statping.com
ENV SASS=/usr/local/bin/sass
ENV IS_DOCKER=true
ENV STATPING_DIR=/app
WORKDIR /app
COPY ./dev/demo-script.sh /app/
ENTRYPOINT ./demo-script.sh

36
dev/Dockerfile.dev vendored Normal file
View File

@ -0,0 +1,36 @@
FROM golang:1.13.5-alpine
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
RUN apk add --update --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq libsass nodejs nodejs-npm
RUN npm install -g yarn
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
chmod +x /usr/local/bin/sass
WORKDIR /go/src/github.com/hunterlong/statping
ADD go.mod go.sum version.txt ./
RUN go mod download
RUN go get github.com/stretchr/testify/... && \
go get github.com/GeertJohan/go.rice/rice && \
go get github.com/cortesi/modd/cmd/modd
ADD frontend/package.json frontend/yarn.lock ./frontend/
RUN cd frontend && yarn install --pure-lockfile --network-timeout 1000000 && yarn cache clean
ENV IS_DOCKER=true
ENV STATPING_DIR=/go/src/github.com/hunterlong/statping
EXPOSE 8585
EXPOSE 8888
RUN cd frontend && yarn build && cp -R dist ../source/
RUN pwd && ls
COPY . .
CMD dev/dev-env.sh

2
dev/LINT.md vendored
View File

@ -1,2 +0,0 @@
0 problems (0 errors) (0 warnings)

2601
dev/README.md vendored

File diff suppressed because one or more lines are too long

7
dev/demo-script.sh vendored
View File

@ -1,7 +0,0 @@
#!/usr/bin/env sh
COMMAND="rm -rf /app/statping.db && reboot"
echo "* * * * * echo $COMMAND >> /test_file 2>&1" > /etc/crontabs/root
statping

11
dev/dev-env.sh vendored Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env sh
echo "Starting!"
echo "Serving Vue frontend first..."
cd frontend && yarn serve &
echo "Now serving Vue, lets build the golang backend now..."
modd -f dev/modd.conf

View File

@ -1,86 +0,0 @@
version: '2.3'
services:
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
networks:
- internet
- database
depends_on:
- postgres
volumes:
- ./statping:/app
environment:
VIRTUAL_HOST: localhost
VIRTUAL_PORT: 8080
LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
DB_CONN: postgres
DB_HOST: postgres
DB_USER: statping
DB_PASS: password123
DB_DATABASE: statping
NAME: EC2 Example
DESCRIPTION: This is a Statping Docker Compose instance
nginx:
container_name: nginx
image: jwilder/nginx-proxy
ports:
- 0.0.0.0:80:80
- 0.0.0.0:443:443
networks:
- internet
restart: always
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx/certs:/etc/nginx/certs:ro
- ./nginx/vhost:/etc/nginx/vhost.d
- ./nginx/html:/usr/share/nginx/html:ro
- ./nginx/dhparam:/etc/nginx/dhparam
environment:
DEFAULT_HOST: localhost
postgres:
container_name: postgres
image: postgres
restart: always
networks:
- database
volumes:
- ./postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_USER: statping
POSTGRES_DB: statping
letsencrypt:
container_name: letsencrypt
image: jrcs/letsencrypt-nginx-proxy-companion
networks:
- internet
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./nginx/certs:/etc/nginx/certs
- ./nginx/vhost:/etc/nginx/vhost.d
- ./nginx/html:/usr/share/nginx/html
- ./nginx/dhparam:/etc/nginx/dhparam
watchtower:
image: v2tec/watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 120 --cleanup
networks:
internet:
driver: bridge
database:
driver: bridge

View File

@ -1,21 +0,0 @@
version: '3'
services:
nginx:
container_name: nginx
image: nginx:latest
restart: always
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80
expose:
- 80
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
expose:
- 8080

View File

@ -1,62 +0,0 @@
version: '2.3'
services:
nginx:
container_name: nginx
image: jwilder/nginx-proxy
ports:
- 0.0.0.0:80:80
- 0.0.0.0:443:443
networks:
- internet
restart: always
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./statping/nginx/certs:/etc/nginx/certs:ro
- ./statping/nginx/vhost:/etc/nginx/vhost.d
- ./statping/nginx/html:/usr/share/nginx/html:ro
- ./statping/nginx/dhparam:/etc/nginx/dhparam
environment:
DEFAULT_HOST: localhost
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
networks:
- internet
- database
depends_on:
- postgres_statping
volumes:
- ./statping/app:/app
environment:
VIRTUAL_HOST: localhost
VIRTUAL_PORT: 8080
DB_CONN: postgres
DB_HOST: postgres_statping
DB_USER: statping
DB_PASS: password123
DB_DATABASE: statping
NAME: EC2 Example
DESCRIPTION: This is a Statping Docker Compose instance
postgres_statping:
container_name: postgres_statping
image: postgres
restart: always
networks:
- database
volumes:
- ./statping/postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_USER: statping
POSTGRES_DB: statping
networks:
internet:
driver: bridge
database:
driver: bridge

View File

@ -1,17 +0,0 @@
version: '2.3'
services:
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
ports:
- 80:8080
volumes:
- ./statping:/app
expose:
- 8080
environment:
NAME: Statping
DESCRIPTION: You'll need to input your own Database information in Setup process

View File

@ -1,86 +0,0 @@
version: '2.3'
services:
nginx:
container_name: nginx
image: jwilder/nginx-proxy
ports:
- 0.0.0.0:80:80
- 0.0.0.0:443:443
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
networks:
- internet
restart: always
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./statping/nginx/certs:/etc/nginx/certs:ro
- ./statping/nginx/vhost:/etc/nginx/vhost.d
- ./statping/nginx/html:/usr/share/nginx/html:ro
- ./statping/nginx/dhparam:/etc/nginx/dhparam
environment:
DEFAULT_HOST: ${LETSENCRYPT_HOST}
letsencrypt:
container_name: letsencrypt
image: jrcs/letsencrypt-nginx-proxy-companion
networks:
- internet
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./statping/nginx/certs:/etc/nginx/certs
- ./statping/nginx/vhost:/etc/nginx/vhost.d
- ./statping/nginx/html:/usr/share/nginx/html
- ./statping/nginx/dhparam:/etc/nginx/dhparam
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
networks:
- internet
- database
depends_on:
- postgres
volumes:
- ./statping/app:/app
environment:
VIRTUAL_HOST: ${LETSENCRYPT_HOST}
VIRTUAL_PORT: 8080
LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
DB_CONN: postgres
DB_HOST: postgres
DB_USER: statping
DB_PASS: password123
DB_DATABASE: statping
NAME: Statping SSL Instance
DESCRIPTION: This Statping Status Page should be running ${LETSENCRYPT_HOST} with SSL.
postgres:
container_name: postgres
image: postgres
restart: always
networks:
- database
volumes:
- ./statping/postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_USER: statping
POSTGRES_DB: statping
watchtower:
image: v2tec/watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 120 --cleanup
networks:
internet:
driver: bridge
database:
driver: bridge

98
dev/docker-compose.db.yml vendored Normal file
View File

@ -0,0 +1,98 @@
version: '2.3'
services:
postgres:
container_name: postgres
image: postgres
volumes:
- ../docker/databases/postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_DB: statping
POSTGRES_USER: root
ports:
- 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root"]
interval: 15s
timeout: 10s
retries: 20
mysql:
container_name: mysql
image: mysql:5.7
volumes:
- ../docker/databases/mysql:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: password123
MYSQL_DATABASE: statping
MYSQL_USER: root
MYSQL_PASSWORD: password123
ports:
- 3306:3306
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
timeout: 20s
interval: 15s
retries: 30
phpmyadmin:
container_name: phpmyadmin
image: phpmyadmin/phpmyadmin
restart: on-failure
depends_on:
mysql:
condition: service_healthy
ports:
- 5050:80
links:
- mysql:db
environment:
MYSQL_ROOT_PASSWORD: password123
PMA_HOST: mysql
PMA_USER: root
PMA_PASSWORD: password123
PMA_PORT: 3306
sqlite-web:
container_name: sqlite-web
image: coleifer/sqlite-web
restart: on-failure
command: sqlite_web -H 0.0.0.0 -r -x /data/statping.db
ports:
- 6050:8080
volumes:
- ../docker/statping/sqlite/statping.db:/data/statping.db:ro
environment:
SQLITE_DATABASE: /data/statping.db
pgadmin4:
container_name: pgadmin4
image: fenglc/pgadmin4
restart: on-failure
environment:
DEFAULT_USER: admin@admin.com
DEFAULT_PASSWORD: admin
depends_on:
postgres:
condition: service_healthy
ports:
- 7000:5050
links:
- postgres:postgres
prometheus:
container_name: prometheus
image: prom/prometheus:v2.0.0
restart: on-failure
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ../docker/databases/prometheus:/prometheus
ports:
- 7050:9090
healthcheck:
test: "/bin/wget -q -Y off http://localhost:9090/status -O /dev/null > /dev/null 2>&1"
interval: 10s
timeout: 3s

303
dev/docker-compose.full.yml vendored Normal file
View File

@ -0,0 +1,303 @@
version: '2.3'
services:
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- 80:80
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
# statping_dev:
# container_name: statping_dev
# build:
# context: .
# dockerfile: ./dev/Dockerfile.dev
# args:
# VERSION: DEV
# COMMIT: DEV
# restart: on-failure
# volumes:
# - ./:/go/src/github.com/hunterlong/statping
# environment:
# VIRTUAL_HOST: local.statping.com
# VIRTUAL_PORT: 8888
# GO_ENV: test
# DB_CONN: sqlite
# API_KEY: exampleapikey
# API_SECRET: exampleapisecret
# NAME: Statping on SQLite
# DOMAIN: http://localhost:4000
# DESCRIPTION: This is a dev environment on SQLite!
# ADMIN_USER: admin
# ADMIN_PASS: admin
# PORT: 8585
# SERVICES: '[{"name": "Local Statping", "type": "http", "domain": "http://localhost:8585", "interval": 30}]'
# ports:
# - 8888:8888
# - 8585:8585
# networks:
# - statping
# healthcheck:
# test: ["CMD-SHELL", "curl -f http://localhost:8585/health || false"]
# timeout: 2s
# interval: 20s
# retries: 30
statping:
container_name: statping
build:
context: ./
restart: on-failure
volumes:
- ./docker/statping/sqlite:/app
environment:
SERVICES: '[{"name": "Local Statping", "type": "http", "domain": "http://localhost:8585", "interval": 30}]'
VIRTUAL_HOST: sqlite.dev.statping.com
VIRTUAL_PORT: 8080
DB_CONN: sqlite
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping on SQLite
DOMAIN: http://localhost:4000
DESCRIPTION: This is a dev environment on SQLite!
ADMIN_USER: admin
ADMIN_PASS: admin
ports:
- 4000:8080
networks:
- statping
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || false"]
timeout: 2s
interval: 10s
retries: 20
statping_mysql:
container_name: statping_mysql
build:
context: ./
restart: on-failure
ports:
- 4005:8080
volumes:
- ./docker/statping/mysql:/app
links:
- mysql
environment:
VIRTUAL_HOST: mysql.dev.statping.com
VIRTUAL_PORT: 8080
DB_CONN: mysql
DB_HOST: mysql
DB_PORT: 3306
DB_DATABASE: statping
DB_USER: root
DB_PASS: password123
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping on MySQL
DOMAIN: http://localhost:4005
DESCRIPTION: This is a dev environment on MySQL!
ADMIN_USER: admin
ADMIN_PASS: admin
networks:
- statping
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || false"]
timeout: 2s
interval: 10s
retries: 20
statping_postgres:
container_name: statping_postgres
build:
context: ./
restart: on-failure
ports:
- 4010:8080
volumes:
- ./docker/statping/postgres:/app
links:
- postgres
environment:
VIRTUAL_HOST: postgres.dev.statping.com
VIRTUAL_PORT: 8080
DB_CONN: postgres
DB_HOST: postgres
DB_PORT: 5432
DB_DATABASE: statping
DB_USER: root
DB_PASS: password123
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping on Postgres
DOMAIN: http://localhost:4010
DESCRIPTION: This is a dev environment on Postgres!
ADMIN_USER: admin
ADMIN_PASS: admin
networks:
- statping
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || false"]
timeout: 5s
interval: 5s
retries: 30
postgres:
container_name: postgres
image: postgres
volumes:
- ./docker/databases/postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_DB: statping
POSTGRES_USER: root
networks:
- statping
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root"]
interval: 15s
timeout: 10s
retries: 20
mysql:
container_name: mysql
image: mysql:5.7
volumes:
- ./docker/databases/mysql:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: password123
MYSQL_DATABASE: statping
MYSQL_USER: root
MYSQL_PASSWORD: password
networks:
- statping
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
timeout: 20s
interval: 15s
retries: 30
phpmyadmin:
container_name: phpmyadmin
image: phpmyadmin/phpmyadmin
restart: on-failure
depends_on:
mysql:
condition: service_healthy
ports:
- 5050:80
links:
- mysql:db
environment:
VIRTUAL_HOST: phpmyadmin.statping.com
VIRTUAL_PORT: 80
MYSQL_ROOT_PASSWORD: password123
PMA_HOST: mysql
PMA_USER: root
PMA_PASSWORD: password123
PMA_PORT: 3306
networks:
- statping
sqlite-web:
container_name: sqlite-web
image: coleifer/sqlite-web
restart: on-failure
command: sqlite_web -H 0.0.0.0 -r -x /data/statping.db
depends_on:
statping:
condition: service_healthy
ports:
- 6050:8080
links:
- statping
volumes:
- ./docker/statping/sqlite/statping.db:/data/statping.db:ro
environment:
VIRTUAL_HOST: sqladmin.statping.com
VIRTUAL_PORT: 8080
SQLITE_DATABASE: /data/statping.db
networks:
- statping
pgadmin4:
container_name: pgadmin4
image: fenglc/pgadmin4
restart: on-failure
environment:
VIRTUAL_HOST: pgadmin.statping.com
VIRTUAL_PORT: 5050
DEFAULT_USER: admin@admin.com
DEFAULT_PASSWORD: admin
depends_on:
postgres:
condition: service_healthy
ports:
- 7000:5050
links:
- postgres:postgres
networks:
- statping
prometheus:
container_name: prometheus
image: prom/prometheus:v2.0.0
restart: on-failure
volumes:
- ./dev/prometheus.yml:/etc/prometheus/prometheus.yml
- ./docker/databases/prometheus:/prometheus
links:
- statping
- statping_mysql
- statping_postgres
ports:
- 7050:9090
networks:
- statping
environment:
VIRTUAL_HOST: prometheus.statping.com
VIRTUAL_PORT: 9090
healthcheck:
test: "/bin/wget -q -Y off http://localhost:9090/status -O /dev/null > /dev/null 2>&1"
interval: 10s
timeout: 3s
grafana:
container_name: grafana
image: grafana/grafana
restart: on-failure
ports:
- 3000:3000
volumes:
- ./docker/grafana:/var/lib/grafana
- ./dev/grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- ./dev/grafana/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml
- ./dev/grafana/statping_dashboard.json:/etc/grafana/provisioning/dashboards/statping_dashboard.json
environment:
- VIRTUAL_HOST=grafana.statping.com
- VIRTUAL_PORT=3000
- GF_USERS_ALLOW_SIGN_UP=false
- GF_AUTH_ANONYMOUS_ENABLED=true
depends_on:
prometheus:
condition: service_healthy
links:
- prometheus
networks:
- statping
healthcheck:
test: "/usr/bin/wget -q -Y off http://localhost:3000/api/health -O /dev/null > /dev/null 2>&1"
interval: 10s
retries: 20
networks:
statping:

42
dev/docker-compose.lite.yml vendored Normal file
View File

@ -0,0 +1,42 @@
version: '2.3'
services:
statping_dev:
container_name: statping_dev
image: hunterlong/statping:dev
restart: on-failure
volumes:
- ./cmd:/go/src/github.com/hunterlong/statping/cmd/
- ./core:/go/src/github.com/hunterlong/statping/core/
- ./database:/go/src/github.com/hunterlong/statping/database/
- ./dev:/go/src/github.com/hunterlong/statping/dev/
- ./frontend:/go/src/github.com/hunterlong/statping/frontend/
- ./handlers:/go/src/github.com/hunterlong/statping/handlers/
- ./notifiers:/go/src/github.com/hunterlong/statping/notifiers/
- ./source:/go/src/github.com/hunterlong/statping/source/
- ./types:/go/src/github.com/hunterlong/statping/types/
- ./utils:/go/src/github.com/hunterlong/statping/utils/
environment:
DB_CONN: sqlite
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping
DOMAIN: http://localhost:8585
DESCRIPTION: This is a dev environment with auto reloading!
ADMIN_USER: admin
ADMIN_PASS: admin
PORT: 8585
ports:
- 8888:8888
- 8585:8585
networks:
- statping
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8585/health || false"]
timeout: 2s
interval: 5s
retries: 10
networks:
statping:

27
dev/ec2-ssl.sh vendored
View File

@ -1,27 +0,0 @@
#!/usr/bin/env bash
#
# Steps for Automatic SSL with Statping
#
# 1. Create EC2 Instance while including this file has "--data-file"
# 2. Once EC2 is created, view System Logs in EC2 instance.
# 3. In System logs, scroll all the way down and you'll see useful DNS records to use
# 4. Edit your domains DNS to point to the EC2 server.
#
## Domain for new SSL certificate and email for Lets Encrypt SSL recovery
LETSENCRYPT_HOST="status.MYDOMAIN.com"
LETSENCRYPT_EMAIL="noreply@MYEMAIL.com"
###################################################
############# Leave this part alone ###############
###################################################
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
printf "Statping will try to create an SSL for domain: $LETSENCRYPT_HOST\n"
printf "\nexport LETSENCRYPT_HOST=$LETSENCRYPT_HOST\n" >> /home/ubuntu/.profile
printf "export LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL\n" >> /home/ubuntu/.profile
sudo printf "\nexport LETSENCRYPT_HOST=$LETSENCRYPT_HOST\n" >> /root/.profile
sudo printf "export LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL\n" >> /root/.profile
source /home/ubuntu/.profile
sudo /bin/bash -c "source /root/.profile"
LETSENCRYPT_HOST=$LETSENCRYPT_HOST LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL /home/ubuntu/init.sh

11
dev/grafana/dashboard.yml Normal file
View File

@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'Prometheus'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards

View File

@ -0,0 +1,50 @@
# config file version
apiVersion: 1
# list of datasources that should be deleted from the database
deleteDatasources:
- name: Prometheus
orgId: 1
# list of datasources to insert/update depending
# whats available in the database
datasources:
# <string, required> name of the datasource. Required
- name: Prometheus
# <string, required> datasource type. Required
type: prometheus
# <string, required> access mode. direct or proxy. Required
access: proxy
# <int> org id. will default to orgId 1 if not specified
orgId: 1
# <string> url
url: http://prometheus:9090
# <string> database password, if used
password:
# <string> database user, if used
user:
# <string> database name, if used
database:
# <bool> enable/disable basic auth
basicAuth: false
# <string> basic auth username, if used
basicAuthUser:
# <string> basic auth password, if used
basicAuthPassword:
# <bool> enable/disable with credentials headers
withCredentials:
# <bool> mark as default datasource. Max one per org
isDefault: true
# <map> fields that will be converted to json and stored in json_data
jsonData:
graphiteVersion: "1.1"
tlsAuth: false
tlsAuthWithCACert: false
# <string> json object of data that will be encrypted.
secureJsonData:
tlsCACert: "..."
tlsClientCert: "..."
tlsClientKey: "..."
version: 1
# <bool> allow users to edit datasources from the UI.
editable: true

7
dev/grafana/grafana.ini Normal file
View File

@ -0,0 +1,7 @@
[users]
# disable user signup / registration
allow_sign_up = false
[auth.anonymous]
# enable anonymous access
enabled = true

View File

@ -0,0 +1,616 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "Required Prometheus endpoint",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "5.2.1"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": "5.0.0"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "5.0.0"
},
{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": "5.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": 6950,
"graphTooltip": 0,
"id": null,
"iteration": 1531455265624,
"links": [],
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"#890f02",
"rgba(237, 129, 40, 0.89)",
"rgb(72, 198, 35)"
],
"datasource": "${DS_PROMETHEUS}",
"format": "locale",
"gauge": {
"maxValue": 1,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 2,
"w": 5,
"x": 0,
"y": 0
},
"id": 6,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "statup_service_online{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": "0,1",
"title": "",
"transparent": true,
"type": "singlestat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "ONLINE",
"value": "1"
},
{
"op": "=",
"text": "OFFLINE",
"value": "0"
}
],
"valueName": "current"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"gridPos": {
"h": 8,
"w": 19,
"x": 5,
"y": 0
},
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "statup_service_latency{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "${service} Latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ms",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#890f02"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": 0,
"format": "ms",
"gauge": {
"maxValue": 500,
"minValue": 1,
"show": true,
"thresholdLabels": true,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 5,
"x": 0,
"y": 2
},
"id": 7,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "statup_service_latency{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": "250,400",
"title": "",
"transparent": true,
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"gridPos": {
"h": 8,
"w": 19,
"x": 5,
"y": 8
},
"id": 8,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "statup_service_response_length{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "${service} Response Size",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "${DS_PROMETHEUS}",
"format": "locale",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 5,
"x": 0,
"y": 9
},
"id": 5,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "statup_service_status_code{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": "",
"title": "Response Status Code",
"type": "singlestat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "${DS_PROMETHEUS}",
"format": "bytes",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 5,
"x": 0,
"y": 12
},
"id": 4,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "statup_service_response_length{name=\"${service}\"}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": "",
"title": "Response Size",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
}
],
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "${DS_PROMETHEUS}",
"hide": 0,
"includeAll": false,
"label": "service",
"multi": false,
"name": "service",
"options": [],
"query": "label_values(statup_service_latency, name)",
"refresh": 1,
"regex": "",
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Statping",
"uid": "6BzRjddmz",
"version": 25,
"description": "Monitor your websites and applications using Statping service. View more information at https://github.com/hunterlong/statping"
}

4
dev/modd.conf vendored Normal file
View File

@ -0,0 +1,4 @@
**/*.go {
prep: go build -o statping ./cmd
daemon: ./statping --port=8585
}

12
dev/nginx.conf vendored
View File

@ -1,12 +0,0 @@
worker_processes 1;
events { worker_connections 1024; }
http {
server {
location / {
proxy_pass http://statping:8080;
}
listen 80;
}
}

View File

@ -1,15 +0,0 @@
# Deploy Portainer demo (demo.portainer.io) in a play-with-docker playground
#
# - Go to http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml
# - Login and/or Start.
# - Wait until 'Your session is ready!' and 'Close' modal.
# - Refresh (if a shell is not shown).
# - Wait until a link with text '80' is shown. It is the link to the demo.
# - Visit https://github.com/portainer/portainer to check default credentials.
#
version: '3'
services:
trigger:
image: franela/dind
command: sh -c "git clone https://github.com/portainer/portainer-demo/ && ./portainer-demo/portainer-demo.sh && tail -f /dev/null"
volumes: [ '/var/run/docker.sock:/var/run/docker.sock' ]

View File

@ -1,40 +0,0 @@
package main
import (
"github.com/hunterlong/statping/types"
"net/http"
)
type PluginObj types.PluginInfo
var Plugin = PluginObj{
Info: &types.Info{
Name: "Example Plugin",
Description: "This is an example plugin for Statping Status Page application. It can be implemented pretty quick!",
},
Routes: []*types.PluginRoute{{
Url: "/setuper",
Method: "GET",
Func: SampleHandler,
}},
}
func main() {
}
func SampleHandler(w http.ResponseWriter, r *http.Request) {
}
func (e *PluginObj) OnLoad() error {
return nil
}
func (e *PluginObj) GetInfo() *types.Info {
return e.Info
}
func (e *PluginObj) Router() []*types.PluginRoute {
return e.Routes
}

3474
dev/postman.json vendored Normal file

File diff suppressed because it is too large Load Diff

34
dev/prometheus.yml vendored Normal file
View File

@ -0,0 +1,34 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'statping_local'
scrape_interval: 15s
bearer_token: 'samplesecret'
static_configs:
- targets: ['docker0:8585']
- job_name: 'statping'
scrape_interval: 15s
bearer_token: 'exampleapisecret'
static_configs:
- targets: ['statping:8080']
- job_name: 'statping_mysql'
scrape_interval: 15s
bearer_token: 'exampleapisecret'
static_configs:
- targets: ['statping_mysql:8080']
- job_name: 'statping_postgres'
scrape_interval: 15s
bearer_token: 'exampleapisecret'
static_configs:
- targets: ['statping_postgres:8080']
- job_name: 'statping_dev'
scrape_interval: 15s
bearer_token: 'exampleapisecret'
static_configs:
- targets: ['statping_dev:8585']

37
dev/services_in_text.txt vendored Normal file
View File

@ -0,0 +1,37 @@
https://demo.statping.com/
https://github.com/hunterlong/statping
https://statping.com
https://hub.docker.com/r/hunterlong/statping/
https://godoc.org/github.com/hunterlong/statping
https://hub.docker.com/r/hunterlong/statping/
https://goreportcard.com/report/github.com/hunterlong/statping
http://slack.statping.com
https://www.google.com/search?q=do+a+barrel+roll
https://travis-ci.com/hunterlong/statping
https://vue.statping.com
http://gorm.io
https://www.youtube.com/watch?v=mLOdar3jshI
https://www.youtube.com/watch?v=WwoGhpYdebQ&t=1m11s
https://www.youtube.com/watch?v=RPS-Cq4uMFs
https://www.youtube.com/watch?v=5Ao5mg11xIk&t=1m9s
https://www.youtube.com/watch?v=3tJ_SZDbPwQ
https://www.youtube.com/watch?v=6IgaDHRiujA
https://www.youtube.com/watch?v=x8eDyCRa0mY
https://www.youtube.com/watch?v=N3kkNfH4yco
tcp://8.8.8.8:53
tcp://8.8.4.4:53
https://mysql.statping.com
https://postgres.statping.com
tcp://hosted3.nfoservers.com:21
tcp://hosted3.nfoservers.com:22
tcp://hosted3.nfoservers.com:443
tcp://hosted3.nfoservers.com:3306
tcp://hosted15.nfoservers.com:3306
tcp://hosted15.nfoservers.com:21
tcp://hosted15.nfoservers.com:22
tcp://hosted15.nfoservers.com:80
tcp://hosted15.nfoservers.com:443
tcp://demo.statping.com:80
tcp://demo.statping.com:443
tcp://facebook.com:80
tcp://facebook.com:443

4
doc.go
View File

@ -1,10 +1,10 @@
// Package statping is a server monitoring application that includes a status page server. Visit the Statping repo at
// https://github.com/hunterlong/statping to get a full understanding of what this application can do.
// https://github.com/statping/statping to get a full understanding of what this application can do.
//
// Install Statping
//
// Statping is available for Mac, Linux and Windows 64x. You can download the tar.gz file or use a couple other methods. Download
// the latest release at https://github.com/hunterlong/statping/releases/latest or view below. If you're on windows, download
// the latest release at https://github.com/statping/statping/releases/latest or view below. If you're on windows, download
// the zip file from the latest releases link.
//
// // MacOS using homebrew

View File

@ -1,10 +1,14 @@
statping:
container_name: statping
image: hunterlong/statping
restart: always
ports:
- 8080:8080
volumes:
- ./statping:/app
environment:
DB_CONN: sqlite
version: '2.3'
services:
statping:
container_name: statping
image: hunterlong/statping:latest
restart: always
volumes:
- statping_data:/app
environment:
DB_CONN: sqlite
ports:
- 8080:8080

16
frontend/.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8", "ie >= 11"]
}
}
]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}

59
frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,59 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module",
"ecmaVersion": 2018
},
"extends": [
"eslint:recommended",
"plugin:vue/base",
"plugin:vue/essential",
"plugin:vue/strongly-recommended",
"plugin:vue/recommended"
],
"rules": {
"array-bracket-spacing": [ "error", "always" ],
"arrow-spacing": "error",
"constructor-super": "error",
"curly": "error",
"indent": ["error", 4, { "SwitchCase": 1 }],
"keyword-spacing": "error",
"no-console": 0,
"no-const-assign": "error",
"no-debugger": 0,
"no-duplicate-imports": "error",
"no-multiple-empty-lines": [ 2, { "max": 2, "maxEOF": 1 } ],
"no-this-before-super": "error",
"no-unreachable": "error",
"no-unused-vars": 0,
"no-useless-escape": 0,
"no-var": "error",
"object-curly-spacing": [ "error", "always" ],
"one-var": ["error", { "var": "never", "let": "never", "const": "never" }],
"prefer-arrow-callback": "error",
"prefer-const": "error",
"quotes": [ "error", "single", { "allowTemplateLiterals": true } ],
"semi": ["error", "always"],
"sort-imports": ["error", { "ignoreDeclarationSort": true }],
"space-before-function-paren": "error",
"valid-typeof": "error",
"vue/component-name-in-template-casing": ["error", "PascalCase",
{
"ignores": [
"router-link",
"router-view",
"transition"
]
}
]
},
"plugins": [
"vue"
]
}

21
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"extends": "stylelint-config-sass-guidelines"
}

5
frontend/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

3
frontend/config/dev.env Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: 'development'
};

View File

@ -0,0 +1,15 @@
'use strict';
const path = require('path');
const _root = path.resolve(__dirname, '..');
exports.root = function (args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [ _root ].concat(args));
};
exports.assetsPath = function (_path) {
return path.posix.join('assets', _path);
};

3
frontend/config/prod.env Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: 'production'
};

View File

@ -0,0 +1,7 @@
[defaults]
org=Statping
project=statping_frontend
url=https://sentry.statping.com
[auth]
token=6b5b07262307407daef8fc9606954dfd56453f9fed2e4bb5b494ab1753bbcac0

Some files were not shown because too many files have changed in this diff Show More