Merge branch 'master' into fix/216

pull/224/head
Hunter Long 2020-01-13 03:40:38 -08:00 committed by GitHub
commit 7f9e639898
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 3980 additions and 5096 deletions

View File

@ -16,4 +16,5 @@ dev
!dev/demo-script.sh
!build/alpine-linux-amd64
config.yml
statup.db
*.db
tmp

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
*.gohtml linguist-language=golang
*.js linguist-detectable=false
*.yml linguist-detectable=false
*.json linguist-detectable=false
dev/* linguist-vendored

2
.github/FUNDING.yml vendored Normal file
View File

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

3
.gitignore vendored
View File

@ -5,7 +5,7 @@ stage
parts
core/rice-box.go
config.yml
statup.db
*.db
plugins/*.so
data
build
@ -19,6 +19,7 @@ assets
*.log
.env
logs
tmp
/dev/test/node_modules
dev/test/cypress/videos
dev/test/cypress/screenshots

View File

@ -1,74 +1,59 @@
os:
- linux
- linux
language: go
go: 1.12.1
go: 1.13.5
go_import_path: github.com/hunterlong/statping
cache:
directories:
- $GOPATH/pkg/dep
- ~/.npm
- ~/.cache
- $GOPATH/src/github.com/hunterlong/statping/vendor
- "~/.npm"
- "~/.cache"
- "$GOPATH/src/github.com/hunterlong/statping/tmp"
- "$GOPATH/src/github.com/hunterlong/statping/vendor"
sudo: required
services:
- docker
- postgresql
- mysql
- mongodb
- docker
- postgresql
- mysql
- mongodb
env:
global:
- PATH=/snap/bin:$PATH
- DB_HOST=localhost
- DB_USER=travis
- DB_PASS=
- DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/hunterlong/statping
- PATH=$HOME/.local/bin:$PATH
- DB_HOST=localhost
- DB_USER=travis
- DB_PASS=
- DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/hunterlong/statping
matrix:
allow_failures:
- go: master
- go: master
fast_finish: true
notifications:
email: true
branches:
only:
- master
- dev
before_install:
- curl -L -s https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -o $GOPATH/bin/dep
- chmod +x $GOPATH/bin/dep
- master
install:
- npm install -g sass
- npm install -g newman
- make dev-deps
- make dep
- make install
- npm install -g sass
- npm install -g newman
- pip install --user awscli
- go mod vendor
- make dev-deps
- make install
before_script:
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
- psql -c 'create database test;' -U postgres
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
- psql -c 'create database test;' -U postgres
script:
- travis_retry make test-all
- make test-api
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then make coverage; fi
- travis_retry make test-all
- make test-api
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then
make coverage; fi
after_success:
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then make travis-build; fi
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then
make travis-build; fi
webhooks:
urls:
- $GITTER
- "$GITTER"
on_success: change
on_failure: always
on_start: never

View File

@ -1,15 +1,12 @@
FROM golang:1.12-alpine as base
FROM golang:1.13.5-alpine as base
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
ENV DEP_VERSION v0.5.0
RUN apk add --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq libsass
RUN curl -L -s https://github.com/golang/dep/releases/download/$DEP_VERSION/dep-linux-amd64 -o /go/bin/dep && \
chmod +x /go/bin/dep
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 Gopkg.* /go/src/github.com/hunterlong/statping/
RUN make dep && \
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
@ -31,6 +28,6 @@ WORKDIR /app
VOLUME /app
EXPOSE $PORT
HEALTHCHECK --interval=5s --timeout=5s --retries=5 CMD curl -s "http://localhost:$PORT/health" | jq -r -e ".online==true"
HEALTHCHECK --interval=60s --timeout=10s --retries=3 CMD curl -s "http://localhost:$PORT/health" | jq -r -e ".online==true"
CMD statping -port $PORT

328
Gopkg.lock generated
View File

@ -1,328 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:a2de8c02990ea7cb28b962d2effa6840cc2c849b298736eef57a9713432cc0d3"
name = "github.com/99designs/gqlgen"
packages = [
"complexity",
"graphql",
"graphql/introspection",
"handler",
]
pruneopts = "UT"
revision = "afe33f73875beca92e917742c1f49c1f6145018b"
[[projects]]
digest = "1:07f7314344b2771963ada0b2a4a426c59d782dac227dcfff2499188a186446c0"
name = "github.com/GeertJohan/go.rice"
packages = [
".",
"embedded",
]
pruneopts = "UT"
revision = "cd53cd147dd5288bc2fb990fb983e58e301abb5e"
version = "v1.0.0"
[[projects]]
digest = "1:786e862ec180708b60ee670723e3edd969fd4309e7b1c315cd7de058ac62a011"
name = "github.com/agnivade/levenshtein"
packages = ["."]
pruneopts = "UT"
revision = "51b298ff305e72cfd29166dccc3f9878e82f9fdc"
version = "v1.0.2"
[[projects]]
digest = "1:f1ec92a2b8473612547f6e13edbc8c8e6cda6c8be9c54b31958aad4a7ccaaa2b"
name = "github.com/ararog/timeago"
packages = ["."]
pruneopts = "UT"
revision = "518814407569bf983ea81e1bf8b550dd4e7b34f3"
version = "0.0.1"
[[projects]]
digest = "1:86b9f9c54fcddec313009ef19073bf0124bbdae3701080c32f67c0c8817b3b7c"
name = "github.com/daaku/go.zipexe"
packages = ["."]
pruneopts = "UT"
revision = "db7cf2ba330f8c2d28b827826e33d6628ea7e9e0"
version = "v1.0.0"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:3806f369b846160fcbde19bdcf93790868defe7c58d1bb6bc8d974c5b8f8dc1e"
name = "github.com/go-mail/mail"
packages = ["."]
pruneopts = "UT"
revision = "f59b9b83a4e522098e3d3eb94e6f81850ad6e973"
version = "v2.3.1"
[[projects]]
digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65"
name = "github.com/go-sql-driver/mysql"
packages = ["."]
pruneopts = "UT"
revision = "72cd26f257d44c1114970e19afddcd812016007e"
version = "v1.4.1"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "github.com/go-yaml/yaml"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:d5f97fc268267ec1b61c3453058c738246fc3e746f14b1ae25161513b7367b0c"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "c5c6c98bc25355028a63748a498942a6398ccd22"
version = "v1.7.1"
[[projects]]
digest = "1:e72d1ebb8d395cf9f346fd9cbc652e5ae222dd85e0ac842dc57f175abed6d195"
name = "github.com/gorilla/securecookie"
packages = ["."]
pruneopts = "UT"
revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
version = "v1.1.1"
[[projects]]
digest = "1:e5bf52fd66a2e984b57b4c0f2c4ee024ed749a19886246240629998dc0cf31ce"
name = "github.com/gorilla/sessions"
packages = ["."]
pruneopts = "UT"
revision = "f57b7e2d29c6211d16ffa52a0998272f75799030"
version = "v1.1.3"
[[projects]]
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
digest = "1:d15ee511aa0f56baacc1eb4c6b922fa1c03b38413b6be18166b996d82a0156ea"
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru",
]
pruneopts = "UT"
revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c"
version = "v0.5.1"
[[projects]]
digest = "1:0731b955911f880c75409845b54203f5127c72e003691e2d34462a0516a9b1f6"
name = "github.com/jinzhu/gorm"
packages = [
".",
"dialects/mysql",
"dialects/postgres",
"dialects/sqlite",
]
pruneopts = "UT"
revision = "e3987fd4b803c16497aa4dfd2e75db7a6a061a4e"
version = "v1.9.4"
[[projects]]
branch = "master"
digest = "1:fd97437fbb6b7dce04132cf06775bd258cce305c44add58eb55ca86c6c325160"
name = "github.com/jinzhu/inflection"
packages = ["."]
pruneopts = "UT"
revision = "04140366298a54a039076d798123ffa108fff46c"
[[projects]]
digest = "1:ecd9aa82687cf31d1585d4ac61d0ba180e42e8a6182b85bd785fcca8dfeefc1b"
name = "github.com/joho/godotenv"
packages = ["."]
pruneopts = "UT"
revision = "23d116af351c84513e1946b527c88823e476be13"
version = "v1.3.0"
[[projects]]
digest = "1:226be3582c304c347481157049c862924fdd6277256e854781c5ba4728901215"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid",
"scram",
]
pruneopts = "UT"
revision = "51e2106eed1cea199c802d2a49e91e2491b02056"
version = "v1.1.0"
[[projects]]
digest = "1:4a49346ca45376a2bba679ca0e83bec949d780d4e927931317904bad482943ec"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = "UT"
revision = "c7c4067b79cc51e6dfdcef5c702e74b1e0fa7c75"
version = "v1.10.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:8dd6663207b795abbe94a20d2785c9eb16be59183f5468e8816f98aeda466c7f"
name = "github.com/rendon/testcli"
packages = ["."]
pruneopts = "UT"
revision = "6283090d169f51a2410b4e260341a01c9a4c0ca7"
[[projects]]
digest = "1:9421f6e9e28ef86933e824b5caff441366f2b69bb281085b9dca40e1f27a1602"
name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."]
pruneopts = "UT"
revision = "7bfe4c7ecddb3666a94b053b422cdd8f5aaa3615"
version = "v1.0.0"
[[projects]]
digest = "1:972c2427413d41a1e06ca4897e8528e5a1622894050e2f527b38ddf0f343f759"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0"
[[projects]]
branch = "master"
digest = "1:9f3a60def1a1eb5ac184e71dde43c6f99606f54d106db7c95f8b8338629a777b"
name = "github.com/tatsushid/go-fastping"
packages = ["."]
pruneopts = "UT"
revision = "d7bb493dee3e090e2ffb6914adddf17c1e7c026c"
[[projects]]
digest = "1:b4e8aaca88f799355f4ac560bce4293fb85ff21003dd0d5741ca503f7a788e91"
name = "github.com/vektah/gqlparser"
packages = [
".",
"ast",
"gqlerror",
"lexer",
"parser",
"validator",
"validator/rules",
]
pruneopts = "UT"
revision = "05741cdb0871330d8bc980d4afd21ab34eceee83"
version = "v1.1.2"
[[projects]]
branch = "master"
digest = "1:9d5b5d543996dd584da1db1e0de1926f3e4c3a8dba0fa2f8db70f3ebee2342e0"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
]
pruneopts = "UT"
revision = "a29dc8fdc73485234dbef99ebedb95d2eced08de"
[[projects]]
branch = "master"
digest = "1:5e22b2014e7cd102e2b41ab7dd38b9c5175e3447b653033987164bebc858e958"
name = "golang.org/x/net"
packages = [
"bpf",
"icmp",
"internal/iana",
"internal/socket",
"ipv4",
"ipv6",
]
pruneopts = "UT"
revision = "4829fb13d2c62012c17688fa7f629f371014946d"
[[projects]]
digest = "1:c25289f43ac4a68d88b02245742347c94f1e108c534dda442188015ff80669b3"
name = "google.golang.org/appengine"
packages = ["cloudsql"]
pruneopts = "UT"
revision = "54a98f90d1c46b7731eb8fb305d2a321c30ef610"
version = "v1.5.0"
[[projects]]
branch = "v3"
digest = "1:7388652e2215a3f45d341d58766ed58317971030eb1cbd75f005f96ace8e9196"
name = "gopkg.in/alexcesaro/quotedprintable.v3"
packages = ["."]
pruneopts = "UT"
revision = "2caba252f4dc53eaf6b553000885530023f54623"
[[projects]]
digest = "1:c805e517269b0ba4c21ded5836019ed7d16953d4026cb7d00041d039c7906be9"
name = "gopkg.in/natefinch/lumberjack.v2"
packages = ["."]
pruneopts = "UT"
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
version = "v2.1"
[[projects]]
digest = "1:2ee0f15eb0fb04f918db7c2dcf39745f40d69f798ef171610a730e8a56aaa4fd"
name = "gopkg.in/russross/blackfriday.v2"
packages = ["."]
pruneopts = "UT"
revision = "d3b5b032dc8e8927d31a5071b56e14c89f045135"
version = "v2.0.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/99designs/gqlgen/graphql",
"github.com/99designs/gqlgen/graphql/introspection",
"github.com/99designs/gqlgen/handler",
"github.com/GeertJohan/go.rice",
"github.com/GeertJohan/go.rice/embedded",
"github.com/ararog/timeago",
"github.com/go-mail/mail",
"github.com/go-yaml/yaml",
"github.com/gorilla/mux",
"github.com/gorilla/sessions",
"github.com/jinzhu/gorm",
"github.com/jinzhu/gorm/dialects/mysql",
"github.com/jinzhu/gorm/dialects/postgres",
"github.com/jinzhu/gorm/dialects/sqlite",
"github.com/joho/godotenv",
"github.com/rendon/testcli",
"github.com/stretchr/testify/assert",
"github.com/tatsushid/go-fastping",
"github.com/vektah/gqlparser",
"github.com/vektah/gqlparser/ast",
"golang.org/x/crypto/bcrypt",
"gopkg.in/natefinch/lumberjack.v2",
"gopkg.in/russross/blackfriday.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,94 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/99designs/gqlgen"
branch = "master"
[[constraint]]
name = "github.com/GeertJohan/go.rice"
version = "1.0.0"
[[constraint]]
name = "github.com/ararog/timeago"
version = "0.0.1"
[[constraint]]
name = "github.com/go-mail/mail"
version = "2.3.1"
[[constraint]]
name = "github.com/go-yaml/yaml"
version = "2.2.2"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.7.1"
[[constraint]]
name = "github.com/gorilla/sessions"
version = "1.1.3"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.4"
[[constraint]]
name = "github.com/joho/godotenv"
version = "1.3.0"
[[constraint]]
branch = "master"
name = "github.com/rendon/testcli"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.3.0"
[[constraint]]
branch = "master"
name = "github.com/tatsushid/go-fastping"
[[constraint]]
name = "github.com/vektah/gqlparser"
version = "1.1.2"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "gopkg.in/natefinch/lumberjack.v2"
version = "2.1.0"
[[constraint]]
name = "gopkg.in/russross/blackfriday.v2"
version = "2.0.1"
[prune]
go-tests = true
unused-packages = true

128
Makefile
View File

@ -1,24 +1,21 @@
VERSION=$(shell cat version.txt)
SIGN_KEY=B76D61FAA6DB759466E83D9964B9C6AAE2D55278
BINARY_NAME=statping
GOPATH:=$(GOPATH)
GOCMD=go
GOBUILD=$(GOCMD) build -a
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOVERSION=1.12.x
GOINSTALL=$(GOCMD) install
XGO=GOPATH=$(GOPATH) xgo -go $(GOVERSION) --dest=build
GOBUILD=go build -a
GOVERSION=1.13.5
XGO=xgo -go $(GOVERSION) --dest=build
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)"
RICE=$(GOPATH)/bin/rice
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
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}" }, "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": "$(GH_TOKEN)", "file_glob": true, "file": "build/*", "skip_cleanup": true } ], "notifications": { "email": false }, "before_script": ["gem install sass"], "script": [ "wget -O statping.gpg $(SIGN_URL)", "gpg --import statping.gpg", "travis_wait 30 docker pull karalabe/xgo-latest", "make release" ], "after_success": [], "after_deploy": [ "make publish-homebrew" ] } } }'
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
PATH:=$(PATH)
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
# build all arch's and release Statping
release: dev-deps build-all
release: dev-deps
wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg
make build-all
# build and push the images to docker hub
docker: docker-build-all docker-publish-all
@ -45,6 +42,7 @@ 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
@ -70,10 +68,28 @@ install: build
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 && $(GOPATH)/bin/rice embed-go
cd source && rice embed-go
rm -rf .sass-cache
# benchmark testing
@ -204,52 +220,41 @@ databases:
docker run --name statping_mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password123 -e MYSQL_DATABASE=root -d mysql
sleep 30
#
# Download and Install dependencies
#
# run dep to install all required golang dependecies
dep:
dep ensure -vendor-only
# install all required golang dependecies
dev-deps:
$(GOGET) github.com/stretchr/testify/assert
$(GOGET) golang.org/x/tools/cmd/cover
$(GOGET) github.com/mattn/goveralls
$(GOINSTALL) github.com/mattn/goveralls
$(GOGET) github.com/rendon/testcli
$(GOGET) github.com/robertkrimen/godocdown/godocdown
$(GOGET) github.com/karalabe/xgo
$(GOGET) github.com/GeertJohan/go.rice
$(GOGET) github.com/GeertJohan/go.rice/rice
$(GOINSTALL) github.com/GeertJohan/go.rice/rice
$(GOCMD) get github.com/axw/gocov/gocov
$(GOCMD) get gopkg.in/matm/v1/gocov-html
$(GOCMD) install gopkg.in/matm/v1/gocov-html
$(GOCMD) get github.com/mgechev/revive
$(GOCMD) get github.com/fatih/structs
$(GOGET) github.com/ararog/timeago
$(GOGET) gopkg.in/natefinch/lumberjack.v2
$(GOGET) golang.org/x/crypto/bcrypt
$(GOGET) github.com/99designs/gqlgen
go get github.com/stretchr/testify/assert
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
# remove files for a clean compile/build
clean:
rm -rf ./{logs,assets,plugins,statup.db,config.yml,.sass-cache,config.yml,statping,build,.sass-cache,statup.db,index.html,vendor}
rm -rf cmd/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log,*.html,*.json}
rm -rf core/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf handlers/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf notifiers/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf source/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf types/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf utils/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log}
rm -rf dev/{logs,assets,plugins,statup.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
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 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}
rm -rf types/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf utils/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf dev/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
rm -rf {parts,prime,snap,stage}
rm -rf dev/test/cypress/videos
rm -f coverage.* sass
rm -f source/rice-box.go
rm -f *.db-journal
rm -rf **/*.db-journal
rm -rf *.snap
find . -name "*.out" -type f -delete
find . -name "*.cpu" -type f -delete
@ -310,8 +315,19 @@ cypress-install:
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"
aws s3 cp ./source/font $(ASSETS_BKT) --recursive --exclude "*" --include "*.eot" --include "*.svg" --include "*.woff" --include "*.woff2" --include "*.ttf" --include "*.css"
aws s3 cp ./source/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT)
travis_s3_creds:
mkdir -p ~/.aws
echo "[default]\naws_access_key_id = ${AWS_ACCESS_KEY_ID}\naws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" > ~/.aws/credentials
# build Statping using a travis ci trigger
travis-build:
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)
@ -344,12 +360,16 @@ valid-sign:
# install xgo and pull the xgo docker image
xgo-install: clean
go get github.com/karalabe/xgo
docker pull karalabe/xgo-latest
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
.SILENT: travis_s3_creds

View File

@ -34,43 +34,43 @@ import (
// catchCLI will run functions based on the commands sent to Statping
func catchCLI(args []string) error {
dir := utils.Directory
if err := utils.InitLogs(); err != nil {
return err
}
source.Assets()
runLogs := utils.InitLogs
runAssets := source.Assets
loadDotEnvs()
switch args[0] {
case "version":
if COMMIT != "" {
fmt.Printf("Statping v%v (%v)\n", VERSION, COMMIT)
fmt.Printf("%v (%v)\n", VERSION, COMMIT)
} else {
fmt.Printf("Statping v%v\n", VERSION)
fmt.Printf("%v\n", VERSION)
}
return errors.New("end")
case "assets":
var err error
if err = runLogs(); err != nil {
return err
}
if err = runAssets(); err != nil {
return err
}
if err = source.CreateAllAssets(dir); err != nil {
return err
}
return errors.New("end")
case "sass":
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
if err := source.CompileSASS(dir); err != nil {
return err
}
return errors.New("end")
case "update":
var err error
var gitCurrent githubResponse
if gitCurrent, err = checkGithubUpdates(); err != nil {
return err
}
fmt.Printf("Statping Version: v%v\nLatest Version: %v\n", VERSION, gitCurrent.TagName)
if VERSION != gitCurrent.TagName[1:] {
fmt.Printf("You don't have the latest version v%v!\nDownload the latest release at: https://github.com/hunterlong/statping\n", gitCurrent.TagName[1:])
} else {
fmt.Printf("You have the latest version of Statping!\n")
}
updateDisplay()
return errors.New("end")
case "test":
cmd := args[1]
@ -81,32 +81,40 @@ func catchCLI(args []string) error {
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)
utils.InitLogs()
if core.Configs, err = core.LoadConfigFile(dir); err != nil {
utils.Log(4, "config.yml file not found")
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 {
utils.Log(4, err)
log.Errorln(err)
return err
}
utils.Log(1, "Exported Statping index page: 'index.html'")
log.Infoln("Exported Statping index page: 'index.html'")
case "help":
HelpEcho()
return errors.New("end")
case "export":
var err error
var data []byte
if err := utils.InitLogs(); err != nil {
if err = runLogs(); err != nil {
return err
}
if core.Configs, err = core.LoadConfigFile(dir); err != nil {
if err = runAssets(); err != nil {
return err
}
if err = core.Configs.Connect(false, dir); err != nil {
if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil {
return err
}
if err = core.CoreApp.Connect(false, dir); err != nil {
return err
}
if data, err = core.ExportSettings(); err != nil {
@ -133,16 +141,28 @@ func catchCLI(args []string) error {
}
return errors.New("end")
case "run":
utils.Log(1, "Running 1 time and saving to database...")
RunOnce()
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
log.Infoln("Running 1 time and saving to database...")
runOnce()
//core.CloseDB()
fmt.Println("Check is complete.")
return errors.New("end")
case "env":
fmt.Println("Statping Environment Variable")
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
envs, err := godotenv.Read(".env")
if err != nil {
utils.Log(4, "No .env file found in current directory.")
log.Errorln("No .env file found in current directory.")
return err
}
for k, e := range envs {
@ -157,7 +177,7 @@ func catchCLI(args []string) error {
// ExportIndexHTML returns the HTML of the index page as a string
func ExportIndexHTML() []byte {
source.Assets()
core.Configs.Connect(false, utils.Directory)
core.CoreApp.Connect(false, utils.Directory)
core.CoreApp.SelectAllServices(false)
core.CoreApp.UseCdn = types.NewNullBool(true)
for _, srv := range core.CoreApp.Services {
@ -170,16 +190,31 @@ func ExportIndexHTML() []byte {
return w.Body.Bytes()
}
// RunOnce will initialize the Statping application and check each service 1 time, will not run HTTP server
func RunOnce() {
func updateDisplay() error {
var err error
core.Configs, err = core.LoadConfigFile(utils.Directory)
if err != nil {
utils.Log(4, "config.yml file not found")
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
}
err = core.Configs.Connect(false, utils.Directory)
if VERSION != gitCurrent.TagName[1:] {
fmt.Printf("\nNew 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")
}
return nil
}
// 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)
if err != nil {
utils.Log(4, err)
log.Errorln("config.yml file not found")
}
err = core.CoreApp.Connect(false, utils.Directory)
if err != nil {
log.Errorln(err)
}
core.CoreApp, err = core.SelectCore()
if err != nil {
@ -187,7 +222,7 @@ func RunOnce() {
}
_, err = core.CoreApp.SelectAllServices(true)
if err != nil {
utils.Log(4, err)
log.Errorln(err)
}
for _, out := range core.CoreApp.Services {
out.Check(true)
@ -214,9 +249,12 @@ func HelpEcho() {
fmt.Printf("Flags:\n")
fmt.Println(" -ip 127.0.0.1 - Run HTTP server on specific IP address (default: localhost)")
fmt.Println(" -port 8080 - Run HTTP server on Port (default: 8080)")
fmt.Println(" -verbose 1 - Verbose mode levels 1 - 4 (default: 1)")
fmt.Println(" -env path/debug.env - Optional .env file to set as environment variables while running server")
fmt.Printf("Environment Variables:\n")
fmt.Println(" PORT - Set the outgoing port for the HTTP server (or use -port)")
fmt.Println(" IP - Bind a specific IP address to the HTTP server (or use -ip)")
fmt.Println(" VERBOSE - Display more logs in verbose mode. (1 - 4)")
fmt.Println(" STATPING_DIR - Set a absolute path for the root path of Statping server (logs, assets, SQL db)")
fmt.Println(" DISABLE_LOGS - Disable viewing and writing to the log file (default is false)")
fmt.Println(" DB_CONN - Automatic Database connection (sqlite, postgres, mysql)")
@ -226,7 +264,7 @@ func HelpEcho() {
fmt.Println(" DB_PORT - Database port (5432, 3306, ...)")
fmt.Println(" DB_DATABASE - Database connection's database name")
fmt.Println(" POSTGRES_SSLMODE - Enable Postgres SSL Mode 'ssl_mode=VALUE' (enable/disable/verify-full/verify-ca)")
fmt.Println(" GO_ENV - Run Statping in testmode, will bypass HTTP authentication (if set as 'true')")
fmt.Println(" GO_ENV - Run Statping in testmode, will bypass HTTP authentication (if set as 'test')")
fmt.Println(" NAME - Set a name for the Statping status page")
fmt.Println(" DESCRIPTION - Set a description for the Statping status page")
fmt.Println(" DOMAIN - Set a URL for the Statping status page")
@ -234,17 +272,20 @@ func HelpEcho() {
fmt.Println(" ADMIN_PASS - Password for administrator account (default: admin)")
fmt.Println(" SASS - Set the absolute path to the sass binary location")
fmt.Println(" HTTP_PROXY - Use a HTTP Proxy for HTTP Requests")
fmt.Println(" AUTH_USERNAME - HTTP Basic Authentication username")
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")
}
func checkGithubUpdates() (githubResponse, error) {
var gitResp githubResponse
url := "https://api.github.com/repos/hunterlong/statping/releases/latest"
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(10*time.Second))
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true)
if err != nil {
return githubResponse{}, err
}
var gitResp githubResponse
err = json.Unmarshal(contents, &gitResp)
return gitResp, err
}

View File

@ -17,7 +17,6 @@ package main
import (
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"github.com/rendon/testcli"
"github.com/stretchr/testify/assert"
@ -52,7 +51,7 @@ func TestStartServerCommand(t *testing.T) {
func TestVersionCommand(t *testing.T) {
c := testcli.Command("statping", "version")
c.Run()
assert.True(t, c.StdoutContains("Statping v"+VERSION))
assert.True(t, c.StdoutContains(VERSION))
}
func TestHelpCommand(t *testing.T) {
@ -90,7 +89,7 @@ func TestUpdateCommand(t *testing.T) {
commandAndSleep(cmd, time.Duration(15*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, "Statping")
assert.Contains(t, gg, VERSION)
}
func TestAssetsCommand(t *testing.T) {
@ -98,6 +97,7 @@ func TestAssetsCommand(t *testing.T) {
c.Run()
t.Log(c.Stdout())
t.Log("Directory for Assets: ", dir)
time.Sleep(1 * time.Second)
assert.FileExists(t, dir+"/assets/robots.txt")
assert.FileExists(t, dir+"/assets/scss/base.scss")
}
@ -166,7 +166,6 @@ func TestRunOnceCLI(t *testing.T) {
func TestEnvCLI(t *testing.T) {
run := catchCLI([]string{"env"})
assert.Error(t, run)
Clean()
}
func commandAndSleep(cmd *exec.Cmd, duration time.Duration, out chan<- string) {
@ -186,15 +185,3 @@ func runCommand(c *exec.Cmd, out chan<- string) {
bout, _ := c.CombinedOutput()
out <- string(bout)
}
func Clean() {
utils.DeleteFile(dir + "/config.yml")
utils.DeleteFile(dir + "/statping.db")
utils.DeleteDirectory(dir + "/assets")
utils.DeleteDirectory(dir + "/logs")
core.CoreApp = core.NewCore()
source.Assets()
//core.CloseDB()
os.Unsetenv("DB_CONN")
time.Sleep(2 * time.Second)
}

View File

@ -16,16 +16,18 @@
package main
import (
"github.com/hunterlong/statping/utils"
"flag"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers"
_ "github.com/hunterlong/statping/notifiers"
"github.com/hunterlong/statping/plugin"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"github.com/joho/godotenv"
"os"
"os/signal"
"syscall"
)
var (
@ -34,8 +36,10 @@ var (
// COMMIT stores the git commit hash for this version of Statping
COMMIT string
ipAddress string
UsingDotEnv bool
envFile string
verboseMode int
port int
log = utils.Log.WithField("type", "cmd")
)
func init() {
@ -46,28 +50,33 @@ func init() {
// -ip = 0.0.0.0 IP address for outgoing HTTP server
// -port = 8080 Port number for outgoing HTTP server
func parseFlags() {
ip := flag.String("ip", "0.0.0.0", "IP address to run the Statping HTTP server")
p := flag.Int("port", 8080, "Port to run the HTTP server")
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()
ipAddress = *ip
port = *p
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")))
}
}
// main will run the Statping application
func main() {
var err error
go sigterm()
parseFlags()
loadDotEnvs()
source.Assets()
utils.VerboseMode = verboseMode
if err := utils.InitLogs(); err != nil {
fmt.Printf("Statping Log Error: \n %v\n", err)
os.Exit(2)
log.Errorf("Statping Log Error: %v\n", err)
}
args := flag.Args()
@ -81,49 +90,70 @@ func main() {
os.Exit(1)
}
}
utils.Log(1, fmt.Sprintf("Starting Statping v%v", VERSION))
log.Info(fmt.Sprintf("Starting Statping v%v", VERSION))
updateDisplay()
core.Configs, err = core.LoadConfigFile(utils.Directory)
configs, err := core.LoadConfigFile(utils.Directory)
if err != nil {
utils.Log(3, err)
log.Errorln(err)
core.SetupMode = true
utils.Log(1, handlers.RunHTTPServer(ipAddress, port))
os.Exit(1)
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)
}
}
mainProcess()
core.CoreApp.Config = configs
if err := mainProcess(); err != nil {
log.Fatalln(err)
}
}
// Close will gracefully stop the database connection, and log file
func Close() {
core.CloseDB()
utils.CloseLogs()
}
// 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)
<-sigs
Close()
os.Exit(1)
}
// loadDotEnvs attempts to load database configs from a '.env' file in root directory
func loadDotEnvs() error {
err := godotenv.Load()
err := godotenv.Load(envFile)
if err == nil {
utils.Log(1, "Environment file '.env' Loaded")
UsingDotEnv = true
log.Infoln("Environment file '.env' Loaded")
}
return err
}
// mainProcess will initialize the Statping application and run the HTTP server
func mainProcess() {
func mainProcess() error {
dir := utils.Directory
var err error
err = core.Configs.Connect(false, dir)
err = core.CoreApp.Connect(false, dir)
if err != nil {
utils.Log(4, fmt.Sprintf("could not connect to database: %v", err))
log.Errorln(fmt.Sprintf("could not connect to database: %v", err))
return err
}
core.Configs.MigrateDatabase()
core.CoreApp.MigrateDatabase()
core.InitApp()
if !core.SetupMode {
plugin.LoadPlugins()
fmt.Println(handlers.RunHTTPServer(ipAddress, port))
os.Exit(1)
}
}
func ForEachPlugin() {
if len(core.CoreApp.Plugins) > 0 {
//for _, p := range core.Plugins {
// p.OnShutdown()
//}
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
}
}
return err
}

View File

@ -32,7 +32,7 @@ import (
// checkServices will start the checking go routine for each service
func checkServices() {
utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
for _, ser := range CoreApp.Services {
//go obj.StartCheckins()
go ser.CheckQueue(true)
@ -40,6 +40,7 @@ func checkServices() {
}
// Check will run checkHttp for HTTP services and checkTcp for TCP services
// if record param is set to true, it will add a record into the database.
func (s *Service) Check(record bool) {
switch s.Type {
case "http":
@ -59,7 +60,7 @@ CheckLoop:
for {
select {
case <-s.Running:
utils.Log(1, fmt.Sprintf("Stopping service: %v", s.Name))
log.Infoln(fmt.Sprintf("Stopping service: %v", s.Name))
break CheckLoop
case <-time.After(s.SleepDuration):
s.Check(record)
@ -210,9 +211,9 @@ func (s *Service) checkHttp(record bool) *Service {
}
if s.Method == "POST" {
content, res, err = utils.HttpRequest(s.Domain, s.Method, "application/json", headers, bytes.NewBuffer([]byte(s.PostData.String)), timeout)
content, res, err = utils.HttpRequest(s.Domain, s.Method, "application/json", headers, bytes.NewBuffer([]byte(s.PostData.String)), timeout, s.VerifySSL.Bool)
} else {
content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, headers, nil, timeout)
content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, headers, nil, timeout, s.VerifySSL.Bool)
}
if err != nil {
if record {
@ -228,7 +229,7 @@ func (s *Service) checkHttp(record bool) *Service {
if s.Expected.String != "" {
match, err := regexp.MatchString(s.Expected.String, string(content))
if err != nil {
utils.Log(2, fmt.Sprintf("Service %v expected: %v to match %v", s.Name, string(content), s.Expected.String))
log.Warnln(fmt.Sprintf("Service %v expected: %v to match %v", s.Name, string(content), s.Expected.String))
}
if !match {
if record {
@ -243,7 +244,6 @@ func (s *Service) checkHttp(record bool) *Service {
}
return s
}
s.Online = true
if record {
recordSuccess(s)
}
@ -252,30 +252,35 @@ func (s *Service) checkHttp(record bool) *Service {
// recordSuccess will create a new 'hit' record in the database for a successful/online service
func recordSuccess(s *Service) {
s.Online = true
s.LastOnline = utils.Timezoner(time.Now().UTC(), CoreApp.Timezone)
hit := &types.Hit{
Service: s.Id,
Latency: s.Latency,
PingTime: s.PingTime,
CreatedAt: time.Now(),
CreatedAt: time.Now().UTC(),
}
utils.Log(1, fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
s.CreateHit(hit)
log.WithFields(utils.ToFields(hit, s.Select())).Infoln(fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
notifier.OnSuccess(s.Service)
s.Online = true
s.SuccessNotified = true
}
// recordFailure will create a new 'Failure' record in the database for a offline service
func recordFailure(s *Service, issue string) {
s.Online = false
fail := &Failure{&types.Failure{
fail := &types.Failure{
Service: s.Id,
Issue: issue,
PingTime: s.PingTime,
CreatedAt: time.Now(),
CreatedAt: time.Now().UTC(),
ErrorCode: s.LastStatusCode,
}}
utils.Log(2, fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
}
log.WithFields(utils.ToFields(fail, s.Select())).
Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
s.CreateFailure(fail)
notifier.OnFailure(s.Service, fail.Failure)
s.Online = false
s.SuccessNotified = false
s.UpdateNotify = CoreApp.UpdateNotify.Bool
s.DownText = s.DowntimeText()
notifier.OnFailure(s.Service, fail)
}

View File

@ -47,14 +47,14 @@ CheckinLoop:
for {
select {
case <-c.Running:
utils.Log(1, fmt.Sprintf("Stopping checkin routine: %v", c.Name))
log.Infoln(fmt.Sprintf("Stopping checkin routine: %v", c.Name))
c.Failing = false
break CheckinLoop
case <-time.After(reCheck):
utils.Log(1, fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())))
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)
utils.Log(3, issue)
log.Errorln(issue)
c.CreateFailure()
}
reCheck = c.Period()
@ -143,7 +143,7 @@ func (c *Checkin) Grace() time.Duration {
// Expected returns the duration of when the serviec should receive a Checkin
func (c *Checkin) Expected() time.Duration {
last := c.Last().CreatedAt
now := time.Now()
now := utils.Now()
lastDir := now.Sub(last)
sub := time.Duration(c.Period() - lastDir)
return sub
@ -213,7 +213,7 @@ func (c *Checkin) Create() (int64, error) {
c.ApiKey = utils.RandomString(7)
row := checkinDB().Create(&c)
if row.Error != nil {
utils.Log(2, row.Error)
log.Warnln(row.Error)
return 0, row.Error
}
service := SelectService(c.ServiceId)
@ -227,7 +227,7 @@ func (c *Checkin) Create() (int64, error) {
func (c *Checkin) Update() (int64, error) {
row := checkinDB().Update(&c)
if row.Error != nil {
utils.Log(2, row.Error)
log.Warnln(row.Error)
return 0, row.Error
}
return c.Id, row.Error
@ -236,11 +236,11 @@ func (c *Checkin) Update() (int64, error) {
// Create will create a new successful checkinHit
func (c *CheckinHit) Create() (int64, error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now()
c.CreatedAt = utils.Now()
}
row := checkinHitsDB().Create(&c)
if row.Error != nil {
utils.Log(2, row.Error)
log.Warnln(row.Error)
return 0, row.Error
}
return c.Id, row.Error
@ -248,13 +248,13 @@ func (c *CheckinHit) Create() (int64, error) {
// Ago returns the duration of time between now and the last successful checkinHit
func (c *CheckinHit) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), c.CreatedAt)
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 := time.Now().Sub(time.Now()).Seconds()
between := utils.Now().Sub(utils.Now()).Seconds()
if between > float64(c.Interval) {
fmt.Println("rechecking every 15 seconds!")
time.Sleep(15 * time.Second)

View File

@ -31,12 +31,13 @@ type ErrorResponse struct {
}
// LoadConfigFile will attempt to load the 'config.yml' file in a specific directory
func LoadConfigFile(directory string) (*DbConfig, error) {
var configs *DbConfig
func LoadConfigFile(directory string) (*types.DbConfig, error) {
var configs *types.DbConfig
if os.Getenv("DB_CONN") != "" {
utils.Log(1, "DB_CONN environment variable was found, waiting for database...")
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")
@ -45,12 +46,13 @@ func LoadConfigFile(directory string) (*DbConfig, error) {
if err != nil {
return nil, err
}
Configs = configs
return Configs, 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() (*DbConfig, error) {
func LoadUsingEnv() (*types.DbConfig, error) {
Configs, err := EnvToConfig()
if err != nil {
return Configs, err
@ -61,23 +63,22 @@ func LoadUsingEnv() (*DbConfig, error) {
} else {
CoreApp.Domain = os.Getenv("DOMAIN")
}
CoreApp.DbConnection = Configs.DbConn
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
err = Configs.Connect(true, utils.Directory)
err = CoreApp.Connect(true, utils.Directory)
if err != nil {
utils.Log(4, err)
log.Errorln(err)
return nil, err
}
Configs.Save()
CoreApp.SaveConfig(Configs)
exists := DbSession.HasTable("core")
if !exists {
utils.Log(1, fmt.Sprintf("Core database does not exist, creating now!"))
Configs.DropDatabase()
Configs.CreateDatabase()
CoreApp, err = Configs.InsertCore()
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))
CoreApp.DropDatabase()
CoreApp.CreateDatabase()
CoreApp, err = CoreApp.InsertCore(Configs)
if err != nil {
utils.Log(3, err)
log.Errorln(err)
}
username := os.Getenv("ADMIN_USER")
@ -103,8 +104,8 @@ func LoadUsingEnv() (*DbConfig, error) {
return Configs, nil
}
// DefaultPort accepts a database type and returns its default port
func DefaultPort(db string) int64 {
// defaultPort accepts a database type and returns its default port
func defaultPort(db string) int64 {
switch db {
case "mysql":
return 3306
@ -118,29 +119,28 @@ func DefaultPort(db string) int64 {
}
// EnvToConfig converts environment variables to a DbConfig variable
func EnvToConfig() (*DbConfig, error) {
Configs = new(DbConfig)
func EnvToConfig() (*types.DbConfig, error) {
var err error
if os.Getenv("DB_CONN") == "" {
return Configs, errors.New("Missing DB_CONN environment variable")
return nil, errors.New("Missing DB_CONN environment variable")
}
if os.Getenv("DB_CONN") != "sqlite" {
if os.Getenv("DB_HOST") == "" {
return Configs, errors.New("Missing DB_HOST environment variable")
return nil, errors.New("Missing DB_HOST environment variable")
}
if os.Getenv("DB_USER") == "" {
return Configs, errors.New("Missing DB_USER environment variable")
return nil, errors.New("Missing DB_USER environment variable")
}
if os.Getenv("DB_PASS") == "" {
return Configs, errors.New("Missing DB_PASS environment variable")
return nil, errors.New("Missing DB_PASS environment variable")
}
if os.Getenv("DB_DATABASE") == "" {
return Configs, errors.New("Missing DB_DATABASE environment variable")
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"))
port = defaultPort(os.Getenv("DB_PORT"))
}
name := os.Getenv("NAME")
if name == "" {
@ -161,7 +161,7 @@ func EnvToConfig() (*DbConfig, error) {
adminPass = "admin"
}
Configs = &DbConfig{
configs := &types.DbConfig{
DbConn: os.Getenv("DB_CONN"),
DbHost: os.Getenv("DB_HOST"),
DbUser: os.Getenv("DB_USER"),
@ -176,16 +176,20 @@ func EnvToConfig() (*DbConfig, error) {
Password: adminPass,
Error: nil,
Location: utils.Directory,
SqlFile: os.Getenv("SQL_FILE"),
}
return Configs, err
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
@ -193,9 +197,10 @@ func SampleData() error {
// DeleteConfig will delete the 'config.yml' file
func DeleteConfig() error {
err := os.Remove(utils.Directory + "/config.yml")
log.Debugln("deleting config yaml file", utils.Directory+"/config.yml")
err := utils.DeleteFile(utils.Directory + "/config.yml")
if err != nil {
utils.Log(3, err)
log.Errorln(err)
return err
}
return nil

View File

@ -18,7 +18,9 @@ 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"
@ -35,10 +37,10 @@ type Core struct {
}
var (
Configs *DbConfig // Configs holds all of the config.yml and database info
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
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() {
@ -47,9 +49,10 @@ func init() {
// NewCore return a new *core.Core struct
func NewCore() *Core {
CoreApp = new(Core)
CoreApp.Core = new(types.Core)
CoreApp.Started = time.Now()
CoreApp = &Core{&types.Core{
Started: time.Now().UTC(),
},
}
return CoreApp
}
@ -64,14 +67,17 @@ func InitApp() {
InsertNotifierDB()
CoreApp.SelectAllServices(true)
checkServices()
CoreApp.Notifications = notifier.Load()
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 := Configs.Connect(false, utils.Directory)
err := CoreApp.Connect(false, utils.Directory)
if err != nil {
return errors.New("database connection has not been created")
}
@ -144,17 +150,18 @@ func (c Core) AllOnline() bool {
// 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.DbConnection = Configs.DbConn
CoreApp.Version = VERSION
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
return CoreApp, db.Error
@ -177,6 +184,21 @@ func GetLocalIP() 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

View File

@ -19,6 +19,7 @@ import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)
@ -37,31 +38,27 @@ func init() {
}
func TestNewCore(t *testing.T) {
if skipNewDb {
t.SkipNow()
}
utils.DeleteFile(dir + "/config.yml")
utils.DeleteFile(dir + "/statup.db")
CoreApp = NewCore()
assert.NotNil(t, CoreApp)
CoreApp.Name = "Tester"
err := TmpRecords("core.db")
require.Nil(t, err)
require.NotNil(t, CoreApp)
}
func TestDbConfig_Save(t *testing.T) {
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)
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) {
@ -71,43 +68,44 @@ func TestLoadDbConfig(t *testing.T) {
}
func TestDbConnection(t *testing.T) {
err := Configs.Connect(false, dir)
err := CoreApp.Connect(false, dir)
assert.Nil(t, err)
}
func TestDropDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
err := Configs.DropDatabase()
err := CoreApp.DropDatabase()
assert.Nil(t, err)
}
func TestSeedSchemaDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
err := Configs.CreateDatabase()
err := CoreApp.CreateDatabase()
assert.Nil(t, err)
}
func TestMigrateDatabase(t *testing.T) {
err := Configs.MigrateDatabase()
t.SkipNow()
err := CoreApp.MigrateDatabase()
assert.Nil(t, err)
}
func TestSeedDatabase(t *testing.T) {
if skipNewDb {
t.SkipNow()
}
t.SkipNow()
err := InsertLargeSampleData()
assert.Nil(t, err)
}
func TestReLoadDbConfig(t *testing.T) {
err := Configs.Connect(false, dir)
err := CoreApp.Connect(false, dir)
assert.Nil(t, err)
assert.Equal(t, "sqlite", Configs.DbConn)
assert.Equal(t, "sqlite", CoreApp.Config.DbConn)
}
func TestSelectCore(t *testing.T) {
@ -117,6 +115,7 @@ func TestSelectCore(t *testing.T) {
}
func TestInsertNotifierDB(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
@ -134,6 +133,7 @@ func TestEnvToConfig(t *testing.T) {
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")

View File

@ -26,6 +26,7 @@ import (
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"os"
"path/filepath"
"time"
)
@ -37,6 +38,10 @@ var (
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
@ -100,7 +105,7 @@ func incidentsUpdatesDB() *gorm.DB {
// 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.DbConnection == "postgres" {
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))
@ -114,137 +119,166 @@ func CloseDB() {
}
}
// 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
}
//// 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 (db *DbConfig) InsertCore() (*Core, error) {
func (c *Core) InsertCore(db *types.DbConfig) (*Core, error) {
CoreApp = &Core{Core: &types.Core{
Name: db.Project,
Description: db.Description,
Config: "config.yml",
ConfigFile: "config.yml",
ApiKey: utils.NewSHA1Hash(9),
ApiSecret: utils.NewSHA1Hash(16),
Domain: db.Domain,
MigrationId: time.Now().Unix(),
Config: db,
}}
CoreApp.DbConnection = db.DbConn
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 (db *DbConfig) Connect(retry bool, location string) error {
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 = Configs.DbConn
if Configs.DbPort == 0 {
Configs.DbPort = DefaultPort(dbType)
dbType = CoreApp.Config.DbConn
if CoreApp.Config.DbPort == 0 {
CoreApp.Config.DbPort = defaultPort(dbType)
}
switch dbType {
case "sqlite":
conn = location + "/statup.db"
sqlFilename := findDbFile()
conn = sqlFilename
log.Infof("SQL database file at: %v/%v", utils.Directory, conn)
dbType = "sqlite3"
case "mysql":
host := fmt.Sprintf("%v:%v", Configs.DbHost, Configs.DbPort)
conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", Configs.DbUser, Configs.DbPass, host, Configs.DbData)
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", Configs.DbHost, Configs.DbPort, Configs.DbUser, Configs.DbData, Configs.DbPass, sslMode)
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", Configs.DbHost, Configs.DbPort)
conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", Configs.DbUser, Configs.DbPass, host, Configs.DbData)
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 {
utils.Log(1, fmt.Sprintf("Database connection to '%v' is not available, trying again in 5 seconds...", conn))
return db.waitForDb()
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
}
}
if dbType == "sqlite3" {
dbSession.DB().SetMaxOpenConns(1)
}
err = dbSession.DB().Ping()
if err == nil {
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
utils.Log(1, fmt.Sprintf("Database %v connection '%v@%v:%v' at %v was successful.", dbType, Configs.DbUser, Configs.DbHost, Configs.DbPort, Configs.DbData))
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 (db *DbConfig) waitForDb() error {
func (c *Core) waitForDb() error {
time.Sleep(5 * time.Second)
return db.Connect(true, utils.Directory)
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) {
utils.Log(1, "Checking for database records older than 3 months...")
log.Infoln("Checking for database records older than 3 months...")
since := time.Now().AddDate(0, -3, 0).UTC()
DeleteAllSince("failures", since)
DeleteAllSince("hits", since)
@ -256,21 +290,21 @@ 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 {
utils.Log(2, db.Error)
log.Warnln(db.Error)
}
}
// Update will save the config.yml file
func (db *DbConfig) Update() error {
func (c *Core) UpdateConfig() error {
var err error
config, err := os.Create(utils.Directory + "/config.yml")
if err != nil {
utils.Log(4, err)
log.Errorln(err)
return err
}
data, err := yaml.Marshal(db)
data, err := yaml.Marshal(c.Config)
if err != nil {
utils.Log(3, err)
log.Errorln(err)
return err
}
config.WriteString(string(data))
@ -279,31 +313,33 @@ func (db *DbConfig) Update() error {
}
// Save will initially create the config.yml file
func (db *DbConfig) Save() (*DbConfig, error) {
var err error
func (c *Core) SaveConfig(configs *types.DbConfig) (*types.DbConfig, error) {
config, err := os.Create(utils.Directory + "/config.yml")
if err != nil {
utils.Log(4, err)
log.Errorln(err)
return nil, err
}
db.ApiKey = utils.NewSHA1Hash(16)
db.ApiSecret = utils.NewSHA1Hash(16)
data, err := yaml.Marshal(db)
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 {
utils.Log(3, err)
log.Errorln(err)
return nil, err
}
config.WriteString(string(data))
defer config.Close()
return db, err
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 *DbConfig) CreateCore() *Core {
func (c *Core) CreateCore() *Core {
newCore := &types.Core{
Name: c.Project,
Name: c.Name,
Description: c.Description,
Config: "config.yml",
ConfigFile: utils.Directory + "/config.yml",
ApiKey: c.ApiKey,
ApiSecret: c.ApiSecret,
Domain: c.Domain,
@ -315,14 +351,14 @@ func (c *DbConfig) CreateCore() *Core {
}
CoreApp, err := SelectCore()
if err != nil {
utils.Log(4, err)
log.Errorln(err)
}
return CoreApp
}
// DropDatabase will DROP each table Statping created
func (db *DbConfig) DropDatabase() error {
utils.Log(1, "Dropping Database Tables...")
func (c *Core) DropDatabase() error {
log.Infoln("Dropping Database Tables...")
err := DbSession.DropTableIfExists("checkins")
err = DbSession.DropTableIfExists("checkin_hits")
err = DbSession.DropTableIfExists("notifications")
@ -338,9 +374,9 @@ func (db *DbConfig) DropDatabase() error {
}
// CreateDatabase will CREATE TABLES for each of the Statping elements
func (db *DbConfig) CreateDatabase() error {
func (c *Core) CreateDatabase() error {
var err error
utils.Log(1, "Creating Database Tables...")
log.Infoln("Creating Database Tables...")
for _, table := range DbModels {
if err := DbSession.CreateTable(table); err.Error != nil {
return err.Error
@ -349,15 +385,15 @@ func (db *DbConfig) CreateDatabase() error {
if err := DbSession.Table("core").CreateTable(&types.Core{}); err.Error != nil {
return err.Error
}
utils.Log(1, "Statping Database Created")
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 (db *DbConfig) MigrateDatabase() error {
utils.Log(1, "Migrating Database Tables...")
func (c *Core) MigrateDatabase() error {
log.Infoln("Migrating Database Tables...")
tx := DbSession.Begin()
defer func() {
if r := recover(); r != nil {
@ -365,6 +401,7 @@ func (db *DbConfig) MigrateDatabase() error {
}
}()
if tx.Error != nil {
log.Errorln(tx.Error)
return tx.Error
}
for _, table := range DbModels {
@ -372,9 +409,9 @@ func (db *DbConfig) MigrateDatabase() error {
}
if err := tx.Table("core").AutoMigrate(&types.Core{}); err.Error != nil {
tx.Rollback()
utils.Log(3, fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error))
log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error))
return tx.Error
}
utils.Log(1, "Statping Database Migrated")
log.Infoln("Statping Database Migrated")
return tx.Commit().Error
}

View File

@ -20,7 +20,6 @@ import (
"encoding/json"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
)
@ -28,7 +27,7 @@ import (
func ExportChartsJs() string {
render, err := source.JsBox.String("charts.js")
if err != nil {
utils.Log(4, err)
log.Errorln(err)
}
t := template.New("charts")
t.Funcs(template.FuncMap{
@ -39,7 +38,7 @@ func ExportChartsJs() string {
t.Parse(render)
var tpl bytes.Buffer
if err := t.Execute(&tpl, CoreApp.Services); err != nil {
utils.Log(3, err)
log.Errorln(err)
}
result := tpl.String()
return result

View File

@ -19,7 +19,6 @@ import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sort"
"strings"
"time"
@ -35,16 +34,15 @@ const (
)
// CreateFailure will create a new Failure record for a service
func (s *Service) CreateFailure(fail types.FailureInterface) (int64, error) {
f := fail.(*Failure)
func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
f.Service = s.Id
row := failuresDB().Create(f)
if row.Error != nil {
utils.Log(3, row.Error)
log.Errorln(row.Error)
return 0, row.Error
}
sort.Sort(types.FailSort(s.Failures))
s.Failures = append(s.Failures, f)
//s.Failures = append(s.Failures, f)
if len(s.Failures) > limitedFailures {
s.Failures = s.Failures[1:]
}
@ -57,7 +55,7 @@ func (s *Service) AllFailures() []*Failure {
col := failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc")
err := col.Find(&fails)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
log.Errorln(fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
return nil
}
return fails
@ -67,7 +65,7 @@ func (s *Service) AllFailures() []*Failure {
func (s *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("failed to delete all failures: %v", err))
log.Errorln(fmt.Sprintf("failed to delete all failures: %v", err))
}
s.Failures = nil
}
@ -88,7 +86,7 @@ func (s *Service) LimitedCheckinFailures(amount int64) []*Failure {
// Ago returns a human readable timestamp for a Failure
func (f *Failure) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt)
got, _ := timeago.TimeAgoWithTime(time.Now().UTC(), f.CreatedAt)
return got
}
@ -119,7 +117,7 @@ func CountFailures() uint64 {
var count uint64
err := failuresDB().Count(&count)
if err.Error != nil {
utils.Log(2, err.Error)
log.Warnln(err.Error)
return 0
}
return count
@ -137,7 +135,7 @@ func (s *Service) TotalFailuresOnDate(ago time.Time) (uint64, error) {
// TotalFailures24 returns the amount of failures for a service within the last 24 hours
func (s *Service) TotalFailures24() (uint64, error) {
ago := time.Now().Add(-24 * time.Hour)
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.TotalFailuresSince(ago)
}
@ -151,7 +149,7 @@ func (s *Service) TotalFailures() (uint64, error) {
// FailuresDaysAgo returns the amount of failures since days ago
func (s *Service) FailuresDaysAgo(days int) uint64 {
ago := time.Now().Add((-24 * time.Duration(days)) * time.Hour)
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
count, _ := s.TotalFailuresSince(ago)
return count
}

View File

@ -22,14 +22,14 @@ func (g *Group) Delete() error {
// Create will create a group and insert it into the database
func (g *Group) Create() (int64, error) {
g.CreatedAt = time.Now()
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()
g.UpdatedAt = time.Now().UTC()
db := groupsDb().Update(g)
return g.Id, db.Error
}

View File

@ -17,7 +17,6 @@ package core
import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"time"
)
@ -29,7 +28,7 @@ type Hit struct {
func (s *Service) CreateHit(h *types.Hit) (int64, error) {
db := hitsDB().Create(&h)
if db.Error != nil {
utils.Log(2, db.Error)
log.Errorln(db.Error)
return 0, db.Error
}
return h.Id, db.Error

View File

@ -47,14 +47,14 @@ func (i *Incident) Delete() error {
// Create will create a incident and insert it into the database
func (i *Incident) Create() (int64, error) {
i.CreatedAt = time.Now()
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()
i.UpdatedAt = time.Now().UTC()
db := incidentsDB().Update(i)
return i.Id, db.Error
}
@ -67,7 +67,7 @@ func (i *IncidentUpdate) Delete() error {
// Create will create a incident update and insert it into the database
func (i *IncidentUpdate) Create() (int64, error) {
i.CreatedAt = time.Now()
i.CreatedAt = time.Now().UTC()
db := incidentsUpdatesDB().Create(i)
return i.Id, db.Error
}

View File

@ -0,0 +1,130 @@
// 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

@ -0,0 +1,43 @@
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)
}
})
}

103
core/integrations/docker.go Normal file
View File

@ -0,0 +1,103 @@
// 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

@ -0,0 +1,40 @@
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

@ -0,0 +1,64 @@
// 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

@ -0,0 +1,15 @@
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

@ -0,0 +1,126 @@
// 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

@ -0,0 +1,27 @@
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

@ -18,7 +18,6 @@ package core
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"time"
)
@ -64,7 +63,7 @@ func (m *Message) Create() (int64, error) {
m.CreatedAt = time.Now().UTC()
db := messagesDb().Create(m)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error))
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
@ -80,7 +79,7 @@ func (m *Message) Delete() error {
func (m *Message) Update() (*Message, error) {
db := messagesDb().Update(m)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error))
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

@ -25,7 +25,7 @@ var (
)
func checkNotifierForm(n Notifier) error {
notifier := asNotification(n)
notifier := n.Select()
for _, f := range notifier.Form {
contains := contains(f.DbField, allowedVars)
if !contains {

View File

@ -15,7 +15,11 @@
package notifier
import "github.com/hunterlong/statping/types"
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) {
@ -34,8 +38,25 @@ 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) && inLimits(comm) {
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)
}
}
@ -46,8 +67,18 @@ 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) && inLimits(comm) {
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)
}
}
@ -57,6 +88,9 @@ func OnSuccess(s *types.Service) {
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)
}
}
@ -69,6 +103,7 @@ func OnUpdatedService(s *types.Service) {
}
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)
}
}
@ -81,6 +116,7 @@ func OnDeletedService(s *types.Service) {
}
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)
}
}
@ -90,6 +126,7 @@ func OnDeletedService(s *types.Service) {
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)
}
}
@ -99,6 +136,7 @@ func OnNewUser(u *types.User) {
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)
}
}
@ -108,6 +146,7 @@ func OnUpdatedUser(u *types.User) {
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)
}
}
@ -117,6 +156,7 @@ func OnDeletedUser(u *types.User) {
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)
}
}
@ -144,6 +184,7 @@ func OnNewNotifier(n *Notification) {
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

@ -18,7 +18,9 @@ package notifier
import (
"errors"
"fmt"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"time"
)
@ -83,7 +85,11 @@ var example = &ExampleNotifier{&Notification{
// init will be ran when Statping is loaded, AddNotifier will add the notifier instance to the system
func init() {
AddNotifier(example)
dir = utils.Directory
source.Assets()
utils.InitLogs()
injectDatabase()
AddNotifiers(example)
}
// Send is the main function to hold your notifier functionality
@ -209,14 +215,14 @@ func ExampleNotification() {
}}
// AddNotifier accepts a Notifier to load into the Statping Notification system
err := AddNotifier(example)
err := AddNotifiers(example)
fmt.Println(err)
// Output: <nil>
}
// Add a Notifier to the AddQueue function to insert it into the system
func ExampleAddNotifier() {
err := AddNotifier(example)
err := AddNotifiers(example)
fmt.Println(err)
// Output: <nil>
}
@ -226,7 +232,8 @@ 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
// Output:
// 1
}
// Add a new message into the queue OnSuccess
@ -252,7 +259,7 @@ func ExampleOnTest() {
func ExampleNotification_CanTest() {
testable := example.CanTest()
fmt.Print(testable)
// Output: false
// Output: true
}
// Add any type of interface to the AddQueue function to be ran in the queue
@ -261,7 +268,8 @@ func ExampleNotification_AddQueue() {
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
// Output:
// Example has 2 items in the queue
}
// The Send method will run the main functionality of your notifier

View File

@ -33,6 +33,7 @@ var (
// 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.
@ -62,7 +63,6 @@ type Notification struct {
Delay time.Duration `gorm:"-" json:"delay,string"`
Queue []*QueueData `gorm:"-" json:"-"`
Running chan bool `gorm:"-" json:"-"`
Online bool `gorm:"-" json:"online"`
testable bool `gorm:"-" json:"testable"`
}
@ -103,6 +103,7 @@ func (n *Notification) AfterFind() (err error) {
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
@ -127,29 +128,21 @@ func asNotification(n Notifier) *Notification {
}
// AddNotifier accept a Notifier interface to be added into the array
func AddNotifier(n Notifier) error {
if isType(n, new(Notifier)) {
err := checkNotifierForm(n)
if err != nil {
return err
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")
}
AllCommunications = append(AllCommunications, n)
} else {
return errors.New("notifier does not have the required methods")
}
return nil
}
// Load is called by core to add all the notifier into memory
func Load() []types.AllNotifiers {
var notifiers []types.AllNotifiers
for _, comm := range AllCommunications {
n := comm.(Notifier)
Init(n)
notifiers = append(notifiers, n)
}
startAllNotifiers()
return notifiers
return nil
}
// normalizeType will accept multiple interfaces and converts it into a string for logging
@ -177,10 +170,9 @@ func normalizeType(ty interface{}) string {
func (n *Notification) makeLog(msg interface{}) {
log := &NotificationLog{
Message: normalizeType(msg),
Time: utils.Timestamp(time.Now()),
Timestamp: time.Now(),
Time: utils.Timestamp(utils.Now()),
Timestamp: utils.Now(),
}
utils.Log(1, fmt.Sprintf("Notifier %v has sent a message %v", n.Method, log.Message))
n.logs = append(n.logs, log)
}
@ -198,8 +190,8 @@ func reverseLogs(input []*NotificationLog) []*NotificationLog {
}
// isInDatabase returns true if the notifier has already been installed
func isInDatabase(n *Notification) bool {
inDb := modelDb(n).RecordNotFound()
func isInDatabase(n Notifier) bool {
inDb := modelDb(n.Select()).RecordNotFound()
return !inDb
}
@ -225,13 +217,14 @@ func Update(n Notifier, notif *Notification) (*Notification, error) {
}
// insertDatabase will create a new record into the database for the notifier
func insertDatabase(n *Notification) (int64, error) {
n.Limits = 3
query := db.Create(n)
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 n.Id, query.Error
return noti.Id, query.Error
}
// SelectNotifier returns the Notification struct from the database
@ -246,7 +239,7 @@ func SelectNotifier(method string) (*Notification, Notifier, error) {
return notifier, comm.(Notifier), nil
}
}
return nil, nil, nil
return nil, nil, errors.New("cannot find notifier")
}
// Init accepts the Notifier interface to initialize the notifier
@ -298,7 +291,9 @@ CheckNotifier:
msg := notification.Queue[0]
err := n.Send(msg.Data)
if err != nil {
utils.Log(2, fmt.Sprintf("notifier %v had an error: %v", notification.Method, err))
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 {
@ -316,11 +311,14 @@ CheckNotifier:
// 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.Select())
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.Select())
_, err := insertDatabase(n)
if err != nil {
utils.Log(3, err)
log.Errorln(err)
return err
}
}
@ -339,13 +337,13 @@ func (n *Notification) LastSent() time.Duration {
// SentLastHour returns the total amount of notifications sent in last 1 hour
func (n *Notification) SentLastHour() int {
since := time.Now().Add(-1 * time.Hour)
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 := time.Now().Add(-1 * time.Minute)
since := utils.Now().Add(-1 * time.Minute)
return n.SentLast(since)
}
@ -467,3 +465,19 @@ func (n *Notification) IsRunning() bool {
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

@ -17,7 +17,6 @@ package notifier
import (
"fmt"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
@ -56,16 +55,9 @@ var core = &types.Core{
Name: "testing notifiers",
}
func init() {
dir = utils.Directory
source.Assets()
utils.InitLogs()
injectDatabase()
}
func injectDatabase() {
utils.DeleteFile(dir + "/statup.db")
db, _ = gorm.Open("sqlite3", dir+"/statup.db")
utils.DeleteFile(dir + "/notifier.db")
db, _ = gorm.Open("sqlite3", dir+"/notifier.db")
db.CreateTable(&Notification{})
}
@ -79,13 +71,8 @@ func TestIsBasicType(t *testing.T) {
assert.True(t, isType(example, new(Tester)))
}
func TestLoad(t *testing.T) {
notifiers := Load()
assert.Equal(t, 1, len(notifiers))
}
func TestIsInDatabase(t *testing.T) {
in := isInDatabase(example.Notification)
in := isInDatabase(example)
assert.True(t, in)
}
@ -101,14 +88,6 @@ 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))
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 2, len(example.Queue))
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 3, len(example.Queue))
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 4, len(example.Queue))
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 5, len(example.Queue))
}
func TestNotification_Update(t *testing.T) {

View File

@ -17,8 +17,10 @@ package core
import (
"fmt"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sync"
"time"
)
@ -29,7 +31,7 @@ var (
// InsertSampleData will create the example/dummy services for a brand new Statping installation
func InsertSampleData() error {
utils.Log(1, "Inserting Sample Data...")
log.Infoln("Inserting Sample Data...")
insertSampleGroups()
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
@ -44,6 +46,7 @@ func InsertSampleData() error {
Order: 1,
GroupId: 1,
Permalink: types.NewNullString("google"),
VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn,
})
s2 := ReturnService(&types.Service{
@ -56,6 +59,7 @@ func InsertSampleData() error {
Timeout: 20,
Order: 2,
Permalink: types.NewNullString("statping_github"),
VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn,
})
s3 := ReturnService(&types.Service{
@ -68,6 +72,7 @@ func InsertSampleData() error {
Timeout: 30,
Order: 3,
Public: types.NewNullBool(true),
VerifySSL: types.NewNullBool(true),
GroupId: 2,
CreatedAt: createdOn,
})
@ -83,6 +88,7 @@ func InsertSampleData() error {
Timeout: 30,
Order: 4,
Public: types.NewNullBool(true),
VerifySSL: types.NewNullBool(true),
GroupId: 2,
CreatedAt: createdOn,
})
@ -109,7 +115,7 @@ func InsertSampleData() error {
insertSampleIncidents()
utils.Log(1, "Sample data has finished importing")
log.Infoln("Sample data has finished importing")
return nil
}
@ -186,7 +192,7 @@ func insertSampleCheckins() error {
})
checkin2.Update()
checkTime := time.Now().Add(-24 * time.Hour)
checkTime := time.Now().UTC().Add(-24 * time.Hour)
for i := 0; i <= 60; i++ {
checkHit := ReturnCheckinHit(&types.CheckinHit{
Checkin: checkin1.Id,
@ -201,32 +207,35 @@ func insertSampleCheckins() error {
// 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()
utils.Log(1, fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name))
log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name))
createdAt := sampleStart
p := utils.NewPerlin(2., 2., 10, seed)
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,
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)
}
service.CreateHit(hit)
}
}()
}
return nil
sg.Wait()
err := tx.Commit().Error
if err != nil {
log.Errorln(err)
}
return err
}
// insertSampleCore will create a new Core for the seed
@ -238,7 +247,7 @@ func insertSampleCore() error {
ApiSecret: "samplesecret",
Domain: "http://localhost:8080",
Version: "test",
CreatedAt: time.Now(),
CreatedAt: time.Now().UTC(),
UseCdn: types.NewNullBool(false),
}
query := coreDB().Create(core)
@ -271,8 +280,8 @@ func insertMessages() error {
Title: "Routine Downtime",
Description: "This is an example a upcoming message for a service!",
ServiceId: 1,
StartOn: time.Now().Add(15 * time.Minute),
EndOn: time.Now().Add(2 * time.Hour),
StartOn: time.Now().UTC().Add(15 * time.Minute),
EndOn: time.Now().UTC().Add(2 * time.Hour),
})
if _, err := m1.Create(); err != nil {
return err
@ -307,7 +316,7 @@ func InsertLargeSampleData() error {
if err := insertMessages(); err != nil {
return err
}
createdOn := time.Now().Add((-24 * 90) * time.Hour).UTC()
createdOn := time.Now().UTC().Add((-24 * 90) * time.Hour)
s6 := ReturnService(&types.Service{
Name: "JSON Lint",
Domain: "https://jsonlint.com",
@ -438,7 +447,7 @@ func InsertLargeSampleData() error {
s14.Create(false)
s15.Create(false)
var dayAgo = time.Now().Add((-24 * 90) * time.Hour)
var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour)
insertHitRecords(dayAgo, 5450)
@ -451,17 +460,17 @@ func InsertLargeSampleData() error {
func insertFailureRecords(since time.Time, amount int64) {
for i := int64(14); i <= 15; i++ {
service := SelectService(i)
utils.Log(1, fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Name))
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 := &Failure{&types.Failure{
failure := &types.Failure{
Service: service.Id,
Issue: "testing right here",
CreatedAt: createdAt,
}}
}
service.CreateFailure(failure)
}
@ -472,7 +481,7 @@ func insertFailureRecords(since time.Time, amount int64) {
func insertHitRecords(since time.Time, amount int64) {
for i := int64(1); i <= 15; i++ {
service := SelectService(i)
utils.Log(1, fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name))
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++ {
@ -480,7 +489,7 @@ func insertHitRecords(since time.Time, amount int64) {
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{
Service: service.Id,
CreatedAt: createdAt,
CreatedAt: createdAt.UTC(),
Latency: latency,
}
service.CreateHit(hit)
@ -489,3 +498,99 @@ func insertHitRecords(since time.Time, amount int64) {
}
}
// 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

@ -101,7 +101,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
var services []*Service
db := servicesDB().Find(&services).Order("order_id desc")
if db.Error != nil {
utils.Log(3, fmt.Sprintf("service error: %v", db.Error))
log.Errorln(fmt.Sprintf("service error: %v", db.Error))
return nil, db.Error
}
CoreApp.Services = nil
@ -144,7 +144,7 @@ func (s *Service) AvgTime() string {
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *Service) OnlineDaysPercent(days int) float32 {
ago := time.Now().Add((-24 * time.Duration(days)) * time.Hour)
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
return s.OnlineSince(ago)
}
@ -207,7 +207,7 @@ func (s *Service) SmallText() string {
}
if len(last) > 0 {
lastFailure := s.lastFailure()
got, _ := timeago.TimeAgoWithTime(time.Now().Add(s.Downtime()), time.Now())
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)
@ -240,7 +240,7 @@ func Dbtimestamp(group string, column string) string {
default:
seconds = 60
}
switch CoreApp.DbConnection {
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":
@ -260,7 +260,7 @@ func (s *Service) Downtime() time.Duration {
if len(hits) == 0 {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(fail.CreatedAt.UTC())
since := fail.CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
}
@ -272,7 +272,7 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
model = model.Order("timeframe asc", false).Group("timeframe")
rows, err := model.Rows()
if err != nil {
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
log.Errorln(fmt.Errorf("issue fetching service chart data: %v", err))
}
for rows.Next() {
var gd DateScan
@ -281,10 +281,10 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
var createdTime time.Time
var err error
rows.Scan(&createdAt, &value)
if CoreApp.DbConnection == "postgres" {
if CoreApp.Config.DbConn == "postgres" {
createdTime, err = time.Parse(types.TIME_NANO, createdAt)
if err != nil {
utils.Log(4, fmt.Errorf("issue parsing time from database: %v to %v", createdAt, types.TIME_NANO))
log.Errorln(fmt.Errorf("issue parsing time from database: %v to %v", createdAt, types.TIME_NANO))
}
} else {
createdTime, err = time.Parse(types.TIME, createdAt)
@ -301,7 +301,7 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
func (d *DateScanObj) ToString() string {
data, err := json.Marshal(d.Array)
if err != nil {
utils.Log(2, err)
log.Warnln(err)
return "{}"
}
return string(data)
@ -309,7 +309,7 @@ func (d *DateScanObj) ToString() string {
// AvgUptime24 returns a service's average online status for last 24 hours
func (s *Service) AvgUptime24() string {
ago := time.Now().Add(-24 * time.Hour)
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.AvgUptime(ago)
}
@ -371,7 +371,7 @@ func (s *Service) Delete() error {
i := s.index()
err := servicesDB().Delete(s)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error))
log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error))
return err.Error
}
s.Close()
@ -386,7 +386,7 @@ func (s *Service) Delete() error {
func (s *Service) Update(restart bool) error {
err := servicesDB().Update(&s)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
return err.Error
}
// clear the notification queue for a service
@ -410,10 +410,10 @@ func (s *Service) Update(restart bool) error {
// Create will create a service and insert it into the database
func (s *Service) Create(check bool) (int64, error) {
s.CreatedAt = time.Now()
s.CreatedAt = time.Now().UTC()
db := servicesDB().Create(s)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error))
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error))
return 0, db.Error
}
s.Start()

View File

@ -17,6 +17,7 @@ package core
import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"testing"
"time"
@ -24,7 +25,6 @@ import (
var (
newServiceId int64
newGroupId int64
)
func TestSelectHTTPService(t *testing.T) {
@ -64,18 +64,15 @@ func TestSelectTCPService(t *testing.T) {
func TestUpdateService(t *testing.T) {
service := SelectService(1)
service2 := SelectService(2)
assert.Equal(t, "Google", service.Name)
assert.Equal(t, "Statping Github", service2.Name)
assert.True(t, service.Online)
assert.True(t, service2.Online)
service.Name = "Updated Google"
service.Interval = 5
err := service.Update(true)
assert.Nil(t, err)
// check if updating pointer array shutdown any other service
service2 = SelectService(2)
assert.True(t, service2.Online)
service = SelectService(1)
assert.Equal(t, "Updated Google", service.Name)
assert.Equal(t, 5, service.Interval)
}
func TestUpdateAllServices(t *testing.T) {
@ -121,7 +118,7 @@ func TestCheckTCPService(t *testing.T) {
}
func TestServiceOnline24Hours(t *testing.T) {
since := time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
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)
@ -137,7 +134,7 @@ func TestServiceSmallText(t *testing.T) {
}
func TestServiceAvgUptime(t *testing.T) {
since := time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
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)
@ -259,10 +256,10 @@ func TestServiceFailedTCPCheck(t *testing.T) {
}
func TestCreateServiceFailure(t *testing.T) {
fail := &Failure{&types.Failure{
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)
@ -353,13 +350,13 @@ func TestSelectServiceLink(t *testing.T) {
}
func TestDbtimestamp(t *testing.T) {
CoreApp.DbConnection = "mysql"
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.DbConnection = "postgres"
CoreApp.Config.DbConn = "postgres"
query = Dbtimestamp("minute", "latency")
assert.Equal(t, "date_trunc('minute', created_at) AS timeframe, AVG(latency) AS value", query)
CoreApp.DbConnection = "sqlite"
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)
}
@ -401,7 +398,7 @@ func TestService_TotalFailures24(t *testing.T) {
func TestService_TotalFailuresOnDate(t *testing.T) {
t.SkipNow()
ago := time.Now().UTC()
ago := utils.Now().UTC()
service := SelectService(8)
failures, err := service.TotalFailuresOnDate(ago)
assert.Nil(t, err)

View File

@ -9,7 +9,7 @@ import (
// SparklineDayFailures returns a string array of daily service failures
func (s *Service) SparklineDayFailures(days int) string {
var arr []string
ago := time.Now().Add((time.Duration(days) * -24) * time.Hour)
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)

View File

@ -68,7 +68,7 @@ func (u *User) Update() error {
// Create will insert a new User into the database
func (u *User) Create() (int64, error) {
u.CreatedAt = time.Now()
u.CreatedAt = time.Now().UTC()
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
@ -77,7 +77,7 @@ func (u *User) Create() (int64, error) {
return 0, db.Error
}
if u.Id == 0 {
utils.Log(3, fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error))
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error))
return 0, db.Error
}
return u.Id, db.Error
@ -88,7 +88,7 @@ func SelectAllUsers() ([]*User, error) {
var users []*User
db := usersDB().Find(&users)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to load all users. %v", db.Error))
log.Errorln(fmt.Sprintf("Failed to load all users. %v", db.Error))
return nil, db.Error
}
return users, db.Error
@ -99,7 +99,7 @@ func SelectAllUsers() ([]*User, error) {
func AuthUser(username, password string) (*User, bool) {
user, err := SelectUsername(username)
if err != nil {
utils.Log(2, fmt.Errorf("user %v not found", username))
log.Warnln(fmt.Errorf("user %v not found", username))
return nil, false
}
if CheckHash(password, user.Password) {

240
dev/COVERAGE.html vendored

File diff suppressed because one or more lines are too long

11
dev/Dockerfile vendored
View File

@ -1,15 +1,12 @@
FROM golang:1.11-alpine as base
MAINTAINER "Hunter Long (https://github.com/hunterlong)"
FROM golang:1.13.5-alpine as base
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
ENV DEP_VERSION v0.5.0
RUN apk add --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq
RUN curl -L -s https://github.com/golang/dep/releases/download/$DEP_VERSION/dep-linux-amd64 -o /go/bin/dep && \
chmod +x /go/bin/dep
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 make dep
RUN go mod vendor
RUN make dev-deps
RUN make install
@ -17,4 +14,4 @@ ENV IS_DOCKER=true
ENV STATPING_DIR=/app
WORKDIR /app
CMD ["statping"]
CMD ["statping"]

2
dev/README.md vendored
View File

@ -2496,7 +2496,7 @@ for application logging
```go
func Log(level int, err interface{}) error
```
Log creates a new entry in the Logger. Log has 1-5 levels depending on how
Log creates a new entry in the utils.Log. Log has 1-5 levels depending on how
critical the log/error is
#### func NewSHA1Hash

21
dev/docker-compose-nginx.yml vendored Normal file
View File

@ -0,0 +1,21 @@
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

12
dev/nginx.conf vendored Normal file
View File

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

View File

@ -1,15 +0,0 @@
{
"projectId": "bi8mhr",
"env": {
"DB_HOST": "localhost",
"DB_USER": "root",
"DB_DATABASE": "root",
"DB_PORT": "5432",
"DB_PASS": "password123",
"GO_ENV": "production"
},
"chromeWebSecurity": false,
"defaultCommandTimeout": 5000,
"requestTimeout": 5000,
"watchForFileChanges": false
}

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -1,65 +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/>.
*/
context('Setup Process', () => {
// it('should go to setup Statping with Postgres', () => {
// cy.visit('http://localhost:8080')
// cy.get('select[name=db_connection]').select('postgres')
// cy.get('input[name="db_host"]').clear().type(Cypress.env('DB_HOST'))
// cy.get('input[name="db_port"]').clear().type('5432')
// cy.get('input[name="db_user"]').clear().type(Cypress.env('DB_USER'))
// if (Cypress.env('TRAVIS')==="yes") {
// cy.get('input[name="db_password"]').clear()
// } else {
// cy.get('input[name="db_password"]').clear().type(Cypress.env('DB_PASS'))
// }
// cy.get('input[name="db_database"]').clear().type(Cypress.env('DB_DATABASE'))
// cy.get('input[name="project"]').clear().type('Demo Tester')
// cy.get('input[name="description"]').clear().type('This is a test from Crypress!')
// cy.get('input[name="domain"]').clear().type('http://localhost:8080')
// cy.get('input[name="username"]').clear().type('admin')
// cy.get('input[name="email"]').clear().type('info@domain.com')
// cy.get('input[name="password"]').clear().type('admin')
// cy.scrollTo('bottom')
// cy.get('#setup_button').click().wait(10000)
// cy.get('.header-title').should('contain', 'Demo Tester')
// cy.get('.header-desc').should('contain', 'This is a test from Crypress!')
// cy.scrollTo('bottom')
// cy.get('.service_li').should('have.length', 5)
// cy.get('.card').should('have.length', 5)
// })
it('should go to setup Statping with SQLite', () => {
cy.visit('http://localhost:8080')
cy.get('select[name=db_connection]').select('sqlite')
cy.get('input[name="project"]').clear().type('Demo Tester')
cy.get('input[name="description"]').clear().type('This is a test from Crypress!')
cy.get('input[name="domain"]').clear().type('http://localhost:8080')
cy.get('input[name="username"]').clear().type('admin')
cy.get('input[name="email"]').clear().type('info@domain.com')
cy.get('input[name="password"]').clear().type('admin')
cy.scrollTo('bottom')
cy.get('#setup_button').click()
cy.get('.header-title').should('contain', 'Demo Tester')
cy.get('.header-desc').should('contain', 'This is a test from Crypress!')
cy.scrollTo('bottom')
cy.get('.service_li').should('have.length', 5)
cy.get('.card').should('have.length', 5)
})
})

View File

@ -1,56 +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/>.
*/
context('Asset Tests', () => {
beforeEach(function () {
cy.visit('http://localhost:8080/dashboard')
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('admin')
cy.get('form').submit()
})
it('should create local assets', () => {
cy.visit('http://localhost:8080/settings/build')
cy.get('#v-pills-style-tab').click()
cy.wait(500)
cy.get(':nth-child(2) > .CodeMirror-line').should('contain', '$background-color')
})
it('should save assets form', () => {
cy.request({method: 'POST', url: 'http://localhost:8080/settings/css', form: true, body: {
variables: '$tester: #bababa',
theme: '@import \'variables\'; .test-var { color: $tester; }'
}})
})
it('should confirm sass variable in css', () => {
cy.request('http://localhost:8080/css/base.css').its('body').should('contain', '.test-var')
})
it('should delete assets', () => {
cy.visit('http://localhost:8080/settings')
cy.get('#v-pills-style-tab').click()
cy.wait(500)
cy.get('.btn-danger').click()
})
it('should check css file after delete', () => {
cy.request('http://localhost:8080/css/base.css').its('body').should('contain', 'BODY')
})
});

View File

@ -1,41 +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/>.
*/
context('Dashboard Tests', () => {
beforeEach(function() {
cy.visit('http://localhost:8080/dashboard')
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('admin')
cy.get('form').submit()
})
it('should view logs', () => {
cy.visit('http://localhost:8080/settings')
cy.get(':nth-child(5) > .nav-link').click()
cy.wait(10000)
cy.get('#live_logs').should('contain', 'Service')
})
it('should view help', () => {
cy.visit('http://localhost:8080/settings')
cy.get(':nth-child(6) > .nav-link').click()
cy.title().should('eq', 'Statping | Help')
cy.get('.col-12 > :nth-child(1)').should('contain', 'Statping')
})
});

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/>.
*/
context('Service Tests', () => {
beforeEach(function () {
cy.visit('http://localhost:8080/dashboard')
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('admin')
cy.get('form').submit()
})
it('should view services', () => {
cy.visit('http://localhost:8080/services')
cy.get('tr').should('have.length', 6)
cy.title().should('eq', 'Statping | Services')
})
it('should create HTTP GET service', () => {
cy.visit('http://localhost:8080/services')
cy.get('select[name="method"]').select('GET')
cy.get('input[name="name"]').clear().type('Google.com')
cy.get('select[name="check_type"]').select('http')
cy.get('input[name="domain"]').clear().type('https://google.com')
cy.get('input[name="expected_status"]').clear().type('200')
cy.get('input[name="interval"]').clear().type('25')
cy.get('input[name="timeout"]').clear().type('30')
cy.get('form').submit()
cy.title().should('eq', 'Statping | Services')
cy.get('tr').should('have.length', 7)
})
it('should create HTTP POST service', () => {
cy.visit('http://localhost:8080/services')
cy.get('select[name="method"]').select('POST')
cy.get('input[name="name"]').clear().type('JSON Regex Test')
cy.get('select[name="check_type"]').select('http')
cy.get('input[name="domain"]').clear().type('https://jsonplaceholder.typicode.com/posts')
cy.get('textarea[name="post_data"]').clear().type(`(title)": "((\\"|[statping])*)"`)
cy.get('input[name="expected_status"]').clear().type('201')
cy.get('input[name="interval"]').clear().type('15')
cy.get('input[name="timeout"]').clear().type('45')
cy.get('form').submit()
cy.title().should('eq', 'Statping | Services')
cy.get('tr').should('have.length', 8)
})
it('should create TCP service', () => {
cy.visit('http://localhost:8080/services')
cy.get('select[name="check_type"]').select('tcp')
cy.get('input[name="name"]').clear().type('Google DNS')
cy.get('input[name="domain"]').clear().type('8.8.8.8')
cy.get('input[name="port"]').clear().type('53')
cy.get('input[name="interval"]').clear().type('25')
cy.get('input[name="timeout"]').clear().type('15')
cy.get('form').submit()
cy.title().should('eq', 'Statping | Services')
cy.get('tr').should('have.length', 9)
})
it('should view HTTP GET service', () => {
cy.visit('http://localhost:8080/service/6')
cy.title().should('eq', 'Statping | Google.com Service')
})
it('should view HTTP POST service', () => {
cy.visit('http://localhost:8080/service/7')
cy.title().should('eq', 'Statping | JSON Regex Test Service')
})
it('should view TCP service', () => {
cy.visit('http://localhost:8080/service/8')
cy.title().should('eq', 'Statping | Google DNS Service')
})
it('should update HTTP service', () => {
cy.visit('http://localhost:8080/service/6')
cy.title().should('eq', 'Statping | Google.com Service')
cy.get('#service_name').clear().type('Google Updated')
cy.get('#service_interval').clear().type('60')
cy.get(':nth-child(3) > form').submit()
cy.title().should('eq', 'Statping | Google Updated Service')
cy.get('#service_name').should('have.value', 'Google Updated')
});
it('should check the updated service', () => {
cy.visit('http://localhost:8080/service/6')
cy.title().should('eq', 'Statping | Google Updated Service')
cy.get('#service_name').should('have.value', 'Google Updated')
cy.get('#service_interval').should('have.value', '60')
})
it('should delete a service', () => {
cy.visit('http://localhost:8080/services')
cy.get(':nth-child(5) > .text-right > .btn-group > .btn-danger').click()
cy.title().should('eq', 'Statping | Services')
cy.get('tr').should('have.length', 8)
})
});

View File

@ -1,48 +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/>.
*/
context('Settings Forms', () => {
beforeEach(function() {
cy.visit('http://localhost:8080/dashboard')
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('admin')
cy.get('form').submit()
})
it('should edit main settings', () => {
cy.visit('http://localhost:8080/settings')
cy.get('input[name="project"]').clear().type('Project Updated')
cy.get('input[name="description"]').clear().type('This is an awesome page')
cy.get('input[name="domain"]').clear().type('http://0.0.0.0:8080')
cy.get('textarea[name="footer"]').clear().type('This is a custom footer')
cy.get('#v-pills-home > form').submit()
cy.title().should('eq', 'Statping | Settings')
cy.get('input[name="project"]').should('have.value', 'Project Updated')
cy.get('input[name="description"]').should('have.value', 'This is an awesome page')
cy.get('input[name="domain"]').should('have.value', 'http://0.0.0.0:8080')
cy.get('.footer').should('contain', 'This is a custom footer')
})
// it('should check index page for changes', () => {
// cy.visit('http://localhost:8080/')
// cy.title().should('eq', 'Project Updated Status')
// cy.get('.header-title').should('contain', 'Project Updated')
// cy.get('.header-desc').should('contain', 'This is an awesome page')
// })
});

View File

@ -1,69 +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/>.
*/
context('User Testing', () => {
beforeEach(function () {
cy.visit('http://localhost:8080/dashboard')
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('admin')
cy.get('form').submit()
})
it('should view users', () => {
cy.visit('http://localhost:8080/users')
cy.get('tr').should('have.length', 2)
cy.title().should('eq', 'Statping | Users')
})
it('should create a new user', () => {
cy.visit('http://localhost:8080/users')
cy.get('input[name="username"]').type('hunterlong')
cy.get('input[name="email"]').type('info@yayaya.com')
cy.get('input[name="password"]').type('admin')
cy.get('input[name="password_confirm"]').type('admin')
cy.get('form').submit()
cy.get('tr').should('have.length', 3)
})
it('should create a edit user', () => {
cy.visit('http://localhost:8080/user/2')
cy.get('input[name="password"]').type('password567')
cy.get('input[name="password_confirm"]').type('password567')
cy.get('form').submit()
cy.get('tr').should('have.length', 3)
})
// it('should logout and login with new password', () => {
// cy.visit('http://localhost:8080/logout')
// cy.title().should('eq', 'Statping | Users')
// cy.get('#user_2 > .btn-group > .btn-danger').click()
// cy.get('tr').should('have.length', 2)
// cy.visit('http://localhost:8080/login')
// cy.get('input[name="username"]').type('hunterlong')
// cy.get('input[name="password"]').type('password567')
// cy.get('form').submit()
// cy.title().should('eq', 'Project Updated Status')
// })
it('should delete a user', () => {
cy.visit('http://localhost:8080/users')
cy.get('#user_2 > .btn-group > .btn-danger').click()
cy.get('tr').should('have.length', 2)
})
});

View File

@ -1,34 +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/>.
*/
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -1,42 +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/>.
*/
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -1,37 +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/>.
*/
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
{
"name": "statping-testing",
"version": "1.0.0",
"description": "Statping Application Testing using Cypress!",
"main": "index.js",
"dependencies": {
"cypress": "^3.1.0"
},
"devDependencies": {
"start-server-and-test": "^1.7.0"
},
"scripts": {
"start": "curl -I -X GET http://localhost:8080/robots.txt",
"cy:run-video": "cypress run --record --key $CYPRESS_KEY",
"cy:run": "cypress run",
"test": "bash -c \"./test.sh\"",
"test-docker": "bash -c \"./test-docker.sh\"",
"testnovid": "cypress run",
"open": "cypress open"
},
"author": "Hunter Long",
"license": "ISC"
}

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
RELEASE_MASTER='{ "request": { "branch": "master", "config": { "script": "make docker-run-test", "services": ["docker"], "before_script": [], "after_deploy": [], "after_success": ["make publish-homebrew", "make publish-latest"], "deploy": [], "before_deploy": [], "env": { "VERSION": "$(VERSION)" } } } }'
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(RELEASE_MASTER) https://api.travis-ci.com/repo/hunterlong%2Fstatping/requests

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
statping > /dev/null &
./node_modules/.bin/start-server-and-test start http://localhost:8080/robots.txt cy:run

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
DIR=`pwd`
DOCKER=`which docker`
$DOCKER build -t hunterlong/statping:dev -f ../Dockerfile ../../
$DOCKER run -it -d -p 8080:8080 -v $DIR/app:/app --name statping_dev hunterlong/statping:dev
./node_modules/.bin/start-server-and-test start http://localhost:8080/robots.txt cy:run
$DOCKER stop statping_dev || true && $DOCKER rm -f statping_dev || true
sudo rm -rf $DIR/app

4
doc.go
View File

@ -1,4 +1,4 @@
// Package statping is a server monitoring application that includs a status page server. Visit the Statping repo at
// 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.
//
// Install Statping
@ -12,7 +12,7 @@
// brew install statping
//
// // Linux installation
// bash <(curl -s https://assets.statping.com/install.sh)
// curl -o- -L https://statping.com/install.sh | bash
// statping version
//
// Docker

42
go.mod Normal file
View File

@ -0,0 +1,42 @@
module github.com/hunterlong/statping
go 1.13.5
require (
github.com/99designs/gqlgen v0.10.1
github.com/GeertJohan/go.rice v1.0.0
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/agnivade/levenshtein v1.0.2 // indirect
github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d
github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/structs v1.1.0
github.com/go-mail/mail v2.3.1+incompatible
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.1 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/jinzhu/gorm v1.9.11
github.com/joho/godotenv v1.3.0
github.com/lib/pq v1.2.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.4.1
github.com/stretchr/testify v1.4.0
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
github.com/vektah/gqlparser v1.1.2
golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
google.golang.org/appengine v1.6.5 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.7 // indirect
)

265
go.sum Normal file
View File

@ -0,0 +1,265 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
github.com/99designs/gqlgen v0.10.1 h1:1BgB6XKGTHq7uH4G1/PYyKe2Kz7/vw3AlvMZlD3TEEY=
github.com/99designs/gqlgen v0.10.1/go.mod h1:IviubpnyI4gbBcj8IcxSSc/Q/+af5riwCmJmwF0uaPE=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.2 h1:xKF7WlEzoa+ZVkzBxy0ukdzI2etYiWGlTPMNTBGncKI=
github.com/agnivade/levenshtein v1.0.2/go.mod h1:JLvzGblJATanj48SD0YhHTEFGkWvw3ASLFWSiMIFXsE=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d h1:ZX0t+GA3MWiP7LWt5xWOphWRQd5JwL4VW5uLW83KM8g=
github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d/go.mod h1:EcJ034SpbWy4heOSDiBZJRn3b5wKJM1b4sFfYeVAkI4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M=
github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f h1:onGP+qmYmjKs7pkmi9j0mwyr97/D5wki80e74aKIOxg=
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f/go.mod h1:cq57a4l475CeMvE7RRpSui1MEqCmhirIt1E7kl8BC2Q=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba h1:9bFeDpN3gTqNanMVqNcoR/pJQuP5uroC3t1D7eXozTE=
golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

View File

@ -54,11 +54,11 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
CacheStorage = NewStorage()
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
}
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
log.Warnln(fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
output := apiResponse{
Status: "error",
Error: err.Error(),
@ -131,6 +131,7 @@ func sendUnauthorizedJson(w http.ResponseWriter, r *http.Request) {
Status: "error",
Error: errors.New("not authorized").Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
returnJson(output, w, r)
}

View File

@ -2,10 +2,13 @@ package handlers
import (
"fmt"
"github.com/hunterlong/statping/core"
_ "github.com/hunterlong/statping/notifiers"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -29,7 +32,9 @@ func init() {
}
func TestResetDatabase(t *testing.T) {
Clean()
err := core.TmpRecords("handlers.db")
require.Nil(t, err)
require.NotNil(t, core.CoreApp)
}
func TestFailedHTTPServer(t *testing.T) {
@ -59,7 +64,7 @@ func TestSetupRoutes(t *testing.T) {
Name: "Statping Setup Check",
URL: "/setup",
Method: "GET",
ExpectedStatus: 200,
ExpectedStatus: 303,
},
{
Name: "Statping Run Setup",
@ -68,7 +73,7 @@ func TestSetupRoutes(t *testing.T) {
Body: form.Encode(),
ExpectedStatus: 303,
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
ExpectedFiles: []string{utils.Directory + "/config.yml", utils.Directory + "/statup.db"},
ExpectedFiles: []string{dir + "/config.yml", dir + "/tmp/" + types.SqliteFilename},
}}
for _, v := range tests {
@ -89,7 +94,7 @@ func TestMainApiRoutes(t *testing.T) {
URL: "/api",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`"name":"Tester","description":"This is an awesome test"`},
ExpectedContains: []string{`"name":"Statping Sample Data","description":"This data is only used to testing"`},
},
{
Name: "Statping Renew API Keys",
@ -113,11 +118,7 @@ func TestMainApiRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -234,11 +235,7 @@ func TestApiServiceRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -278,11 +275,7 @@ func TestGroupAPIRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -332,11 +325,7 @@ func TestApiUsersRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -369,11 +358,7 @@ func TestApiNotifiersRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -439,11 +424,7 @@ func TestMessagesApiRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -478,11 +459,7 @@ func TestApiCheckinRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}
@ -514,10 +491,7 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
}
rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req)
if err != nil {
assert.Nil(t, err)
return "", t, err
}
body, err := ioutil.ReadAll(rr.Result().Body)
if err != nil {
assert.Nil(t, err)
@ -540,9 +514,3 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
}
return stringBody, t, err
}
func Clean() {
utils.DeleteFile(dir + "/config.yml")
utils.DeleteFile(dir + "/statup.db")
utils.DeleteDirectory(dir + "/logs")
}

View File

@ -1,6 +1,7 @@
package handlers
import (
"github.com/hunterlong/statping/utils"
"sync"
"time"
)
@ -25,7 +26,7 @@ func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
return utils.Now().UnixNano() > item.Expiration
}
//Storage mecanism for caching strings in memory
@ -68,6 +69,6 @@ func (s Storage) Set(key string, content []byte, duration time.Duration) {
defer s.mu.Unlock()
s.items[key] = Item{
Content: content,
Expiration: time.Now().Add(duration).UnixNano(),
Expiration: utils.Now().Add(duration).UnixNano(),
}
}

View File

@ -24,7 +24,6 @@ import (
"github.com/hunterlong/statping/utils"
"net"
"net/http"
"time"
)
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
@ -81,7 +80,7 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
hit := &types.CheckinHit{
Checkin: checkin.Id,
From: ip,
CreatedAt: time.Now().UTC(),
CreatedAt: utils.Now().UTC(),
}
checkinHit := core.ReturnCheckinHit(hit)
if checkin.Last() == nil {
@ -95,7 +94,7 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
return
}
checkin.Failing = false
checkin.LastHit = utils.Timezoner(time.Now().UTC(), core.CoreApp.Timezone)
checkin.LastHit = utils.Timezoner(utils.Now().UTC(), core.CoreApp.Timezone)
sendJsonAction(checkinHit, "update", w, r)
}

View File

@ -24,7 +24,6 @@ import (
"github.com/hunterlong/statping/utils"
"net/http"
"strconv"
"time"
)
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
@ -50,8 +49,8 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
session.Values["user_id"] = user.Id
session.Values["admin"] = user.Admin.Bool
session.Save(r, w)
utils.Log(1, fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr))
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
utils.Log.Infoln(fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr))
http.Redirect(w, r, basePath+"dashboard", http.StatusSeeOther)
} else {
err := core.ErrorResponse{Error: "Incorrect login information submitted, try again."}
ExecuteResponse(w, r, "login.gohtml", err, nil)
@ -64,12 +63,12 @@ func logoutHandler(w http.ResponseWriter, r *http.Request) {
session.Values["admin"] = false
session.Values["user_id"] = 0
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
}
func helpHandler(w http.ResponseWriter, r *http.Request) {
if !IsUser(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
return
}
help := source.HelpMarkdown()
@ -115,6 +114,6 @@ func exportHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(fileSize))
w.Header().Set("Content-Control", "private, no-transform, no-store, must-revalidate")
http.ServeContent(w, r, "export.json", time.Now(), bytes.NewReader(export))
http.ServeContent(w, r, "export.json", utils.Now(), bytes.NewReader(export))
}

View File

@ -2,7 +2,7 @@ package handlers
import (
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
@ -27,7 +27,7 @@ func TestGenericRoutes(t *testing.T) {
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{
`<title>Tester Status</title>`,
`<title>Statping Sample Data Status</title>`,
`<footer>`,
},
},
@ -100,7 +100,7 @@ func TestGenericRoutes(t *testing.T) {
URL: "/metrics",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{"statping_total_services 5"},
ExpectedContains: []string{"statping_total_services 15"},
},
{
Name: "Last Log Line",
@ -140,10 +140,7 @@ func TestGenericRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}

View File

@ -12,6 +12,10 @@ import (
"time"
)
var (
basePath = "/"
)
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
return template.FuncMap{
"js": func(html interface{}) template.JS {
@ -63,6 +67,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool
},
"UPDATENOTIFY": func() bool {
return core.CoreApp.UpdateNotify.Bool
},
"QrAuth": func() string {
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
},
@ -85,7 +92,7 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
return utils.UnderScoreString(html)
},
"URL": func() string {
return r.URL.String()
return basePath + r.URL.String()
},
"CHART_DATA": func() string {
return ""
@ -142,5 +149,8 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"NewGroup": func() *types.Group {
return new(types.Group)
},
"BasePath": func() string {
return basePath
},
}
}

View File

@ -16,19 +16,22 @@
package handlers
import (
"crypto/subtle"
"crypto/tls"
"encoding/json"
"fmt"
"html/template"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
"net/http"
"os"
"strings"
"time"
)
const (
@ -41,9 +44,8 @@ var (
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
javascripts = []string{"charts.js", "chart_index.js"}
mainTemplate *template.Template
)
// RunHTTPServer will start a HTTP server on a specific IP and port
@ -54,11 +56,11 @@ func RunHTTPServer(ip string, port int) error {
cert := utils.FileExists(utils.Directory + "/server.crt")
if key && cert {
utils.Log(1, "server.cert and server.key was found in root directory! Starting in SSL mode.")
utils.Log(1, fmt.Sprintf("Statping Secure HTTPS Server running on https://%v:%v", ip, 443))
log.Infoln("server.cert and server.key was found in root directory! Starting in SSL mode.")
log.Infoln(fmt.Sprintf("Statping Secure HTTPS Server running on https://%v:%v", ip, 443))
usingSSL = true
} else {
utils.Log(1, "Statping HTTP Server running on http://"+host)
log.Infoln("Statping HTTP Server running on http://" + host)
}
router = Router()
@ -102,17 +104,20 @@ func RunHTTPServer(ip string, port int) error {
// IsReadAuthenticated will allow Read Only authentication for some routes
func IsReadAuthenticated(r *http.Request) bool {
if core.SetupMode {
return false
}
var token string
query := r.URL.Query()
key := query.Get("api")
if key == core.CoreApp.ApiKey {
if subtle.ConstantTimeCompare([]byte(key), []byte(core.CoreApp.ApiSecret)) == 1 {
return true
}
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
if token == core.CoreApp.ApiKey {
if subtle.ConstantTimeCompare([]byte(token), []byte(core.CoreApp.ApiSecret)) == 1 {
return true
}
}
@ -128,6 +133,9 @@ func IsFullAuthenticated(r *http.Request) bool {
if core.CoreApp == nil {
return true
}
if core.SetupMode {
return false
}
if sessionStore == nil {
return true
}
@ -136,7 +144,7 @@ func IsFullAuthenticated(r *http.Request) bool {
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
if token == core.CoreApp.ApiSecret {
if subtle.ConstantTimeCompare([]byte(token), []byte(core.CoreApp.ApiSecret)) == 1 {
return true
}
}
@ -145,6 +153,9 @@ func IsFullAuthenticated(r *http.Request) bool {
// IsAdmin returns true if the user session is an administrator
func IsAdmin(r *http.Request) bool {
if core.SetupMode {
return false
}
session, err := sessionStore.Get(r, cookieKey)
if err != nil {
return false
@ -157,6 +168,9 @@ func IsAdmin(r *http.Request) bool {
// IsUser returns true if the user is registered
func IsUser(r *http.Request) bool {
if core.SetupMode {
return false
}
if os.Getenv("GO_ENV") == "test" {
return true
}
@ -170,23 +184,22 @@ func IsUser(r *http.Request) bool {
return session.Values["authenticated"].(bool)
}
func loadTemplate(w http.ResponseWriter, r *http.Request) error {
func loadTemplate(w http.ResponseWriter, r *http.Request) (*template.Template, error) {
var err error
mainTemplate = template.New("main")
mainTemplate.Funcs(handlerFuncs(w, r))
mainTemplate := template.New("main")
mainTemplate, err = mainTemplate.Parse(mainTmpl)
if err != nil {
utils.Log(4, err)
return err
log.Errorln(err)
return nil, err
}
// render all templates
mainTemplate.Funcs(handlerFuncs(w, r))
// render all templates
for _, temp := range templates {
tmp, _ := source.TmplBox.String(temp)
mainTemplate, err = mainTemplate.Parse(tmp)
if err != nil {
utils.Log(4, err)
return err
log.Errorln(err)
return nil, err
}
}
// render all javascript files
@ -194,35 +207,37 @@ func loadTemplate(w http.ResponseWriter, r *http.Request) error {
tmp, _ := source.JsBox.String(temp)
mainTemplate, err = mainTemplate.Parse(tmp)
if err != nil {
utils.Log(4, err)
return err
log.Errorln(err)
return nil, err
}
}
return err
return mainTemplate, err
}
// ExecuteResponse will render a HTTP response for the front end user
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
utils.Http(r)
if url, ok := redirect.(string); ok {
http.Redirect(w, r, url, http.StatusSeeOther)
http.Redirect(w, r, path.Join(basePath, url), http.StatusSeeOther)
return
}
if usingSSL {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
loadTemplate(w, r)
mainTemplate, err := loadTemplate(w, r)
if err != nil {
log.Errorln(err)
}
render, err := source.TmplBox.String(file)
if err != nil {
utils.Log(4, err)
log.Errorln(err)
}
// render the page requested
if _, err := mainTemplate.Parse(render); err != nil {
utils.Log(4, err)
log.Errorln(err)
}
// execute the template
if err := mainTemplate.Execute(w, data); err != nil {
utils.Log(4, err)
log.Errorln(err)
}
}
@ -230,7 +245,7 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}) {
render, err := source.JsBox.String(file)
if err != nil {
utils.Log(4, err)
log.Errorln(err)
}
if usingSSL {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
@ -245,10 +260,10 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data
},
})
if _, err := t.Parse(render); err != nil {
utils.Log(4, err)
log.Errorln(err)
}
if err := t.Execute(w, data); err != nil {
utils.Log(4, err)
log.Errorln(err)
}
}

View File

@ -21,7 +21,7 @@ import (
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
if core.Configs == nil {
if core.CoreApp.Config == nil {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
@ -32,7 +32,7 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
health := map[string]interface{}{
"services": len(core.Services()),
"online": true,
"setup": core.Configs != nil,
"setup": core.CoreApp.Config != nil,
}
returnJson(health, w, r)
}

33
handlers/integrations.go Normal file
View File

@ -0,0 +1,33 @@
package handlers
import (
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core/integrations"
"net/http"
)
func apiAllIntegrationsHandler(w http.ResponseWriter, r *http.Request) {
integrations := integrations.Integrations
returnJson(integrations, w, r)
}
func apiIntegrationHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
intg := vars["name"]
r.ParseForm()
for k, v := range r.PostForm {
log.Info(k, v)
}
integration, err := integrations.Find(intg)
if err != nil {
sendErrorJson(err, w, r)
return
}
list, err := integration.List()
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(list, w, r)
}

View File

@ -27,7 +27,7 @@ import (
func messagesHandler(w http.ResponseWriter, r *http.Request) {
if !IsUser(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
return
}
messages, _ := core.SelectMessages()

View File

@ -1,7 +1,7 @@
package handlers
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
@ -25,10 +25,7 @@ func TestMessageRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}

View File

@ -1,18 +1,60 @@
package handlers
import (
"crypto/subtle"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/utils"
"net/http"
"net/http/httptest"
"time"
)
var (
authUser string
authPass string
)
// basicAuthHandler is a middleware to implement HTTP basic authentication using
// AUTH_USERNAME and AUTH_PASSWORD environment variables
func basicAuthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user),
[]byte(authUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass),
[]byte(authPass)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="statping"`)
w.WriteHeader(401)
w.Write([]byte("You are unauthorized to access the application.\n"))
return
}
next.ServeHTTP(w, r)
})
}
// sendLog is a http middleware that will log the duration of request and other useful fields
func sendLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t1 := utils.Now()
t2 := utils.Now().Sub(t1)
if r.RequestURI == "/logs/line" {
return
}
log.WithFields(utils.ToFields(w, r)).
WithField("url", r.RequestURI).
WithField("method", r.Method).
WithField("load_micro_seconds", t2.Microseconds()).
Infoln(fmt.Sprintf("%v (%v) | IP: %v", r.RequestURI, r.Method, r.Host))
next.ServeHTTP(w, r)
})
}
// authenticated is a middleware function to check if user is an Admin before running original request
func authenticated(handler func(w http.ResponseWriter, r *http.Request), redirect bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
if redirect {
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
} else {
sendUnauthorizedJson(w, r)
}
@ -27,7 +69,7 @@ func readOnly(handler func(w http.ResponseWriter, r *http.Request), redirect boo
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
if redirect {
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
} else {
sendUnauthorizedJson(w, r)
}
@ -43,7 +85,7 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
content := CacheStorage.Get(r.RequestURI)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Access-Control-Allow-Origin", "*")
if core.Configs == nil {
if core.CoreApp.Config == nil {
handler(w, r)
return
}

View File

@ -85,8 +85,8 @@ func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
fakeNotifer, notif, err := notifier.SelectNotifier(method)
if err != nil {
utils.Log(3, fmt.Sprintf("issue saving notifier %v: %v", method, err))
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
log.Errorln(fmt.Sprintf("issue saving notifier %v: %v", method, err))
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings")
return
}

View File

@ -25,11 +25,12 @@ import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"net/http"
"time"
"os"
)
var (
router *mux.Router
log = utils.Log.WithField("type", "handlers")
)
// Router returns all of the routes used in Statping.
@ -37,23 +38,38 @@ var (
func Router() *mux.Router {
dir := utils.Directory
CacheStorage = NewStorage()
r := mux.NewRouter()
r.Handle("/", http.HandlerFunc(indexHandler))
r := mux.NewRouter().StrictSlash(true)
if os.Getenv("AUTH_USERNAME") != "" && os.Getenv("AUTH_PASSWORD") != "" {
authUser = os.Getenv("AUTH_USERNAME")
authPass = os.Getenv("AUTH_PASSWORD")
r.Use(basicAuthHandler)
}
if os.Getenv("BASE_PATH") != "" {
basePath = "/" + os.Getenv("BASE_PATH") + "/"
r = r.PathPrefix("/" + os.Getenv("BASE_PATH")).Subrouter()
r.Handle("", http.HandlerFunc(indexHandler))
} else {
r.Handle("/", http.HandlerFunc(indexHandler))
}
r.Use(sendLog)
if source.UsingAssets(dir) {
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
r.PathPrefix("/font/").Handler(http.StripPrefix("/font/", http.FileServer(http.Dir(dir+"/assets/font"))))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir(dir+"/assets/js"))))
r.PathPrefix("/robots.txt").Handler(indexHandler)
r.PathPrefix("/favicon.ico").Handler(indexHandler)
r.PathPrefix("/banner.png").Handler(indexHandler)
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath+"css/", http.FileServer(http.Dir(dir+"/assets/css"))))
r.PathPrefix("/font/").Handler(http.StripPrefix(basePath+"font/", http.FileServer(http.Dir(dir+"/assets/font"))))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath+"js/", http.FileServer(http.Dir(dir+"/assets/js"))))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/favicon.ico").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, indexHandler))
} else {
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(source.CssBox.HTTPBox())))
r.PathPrefix("/font/").Handler(http.StripPrefix("/font/", http.FileServer(source.FontBox.HTTPBox())))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(source.JsBox.HTTPBox())))
r.PathPrefix("/robots.txt").Handler(http.FileServer(source.TmplBox.HTTPBox()))
r.PathPrefix("/favicon.ico").Handler(http.FileServer(source.TmplBox.HTTPBox()))
r.PathPrefix("/banner.png").Handler(http.FileServer(source.TmplBox.HTTPBox()))
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath+"css/", http.FileServer(source.CssBox.HTTPBox())))
r.PathPrefix("/font/").Handler(http.StripPrefix(basePath+"font/", http.FileServer(source.FontBox.HTTPBox())))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath+"js/", http.FileServer(source.JsBox.HTTPBox())))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/favicon.ico").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
}
r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler))
r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET")
@ -86,15 +102,26 @@ func Router() *mux.Router {
r.Handle("/settings/delete_assets", authenticated(deleteAssetsHandler, true)).Methods("GET")
r.Handle("/settings/export", authenticated(exportHandler, true)).Methods("GET")
r.Handle("/settings/bulk_import", authenticated(bulkImportHandler, true)).Methods("POST")
r.Handle("/settings/integrator/{name}", authenticated(integratorHandler, true)).Methods("POST")
// SERVICE Routes
r.Handle("/services", authenticated(servicesHandler, true)).Methods("GET")
r.Handle("/service/{id}", http.HandlerFunc(servicesViewHandler)).Methods("GET")
r.Handle("/service/create", authenticated(createServiceHandler, true)).Methods("GET")
r.Handle("/service/{id}", readOnly(servicesViewHandler, true)).Methods("GET")
r.Handle("/service/{id}/edit", authenticated(servicesViewHandler, true)).Methods("GET")
r.Handle("/service/{id}/delete_failures", authenticated(servicesDeleteFailuresHandler, true)).Methods("GET")
r.Handle("/group/{id}", http.HandlerFunc(groupViewHandler)).Methods("GET")
// API Routes
r.Handle("/api", authenticated(apiIndexHandler, false))
r.Handle("/api/renew", authenticated(apiRenewHandler, false))
r.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
r.Handle("/api/integrations", authenticated(apiAllIntegrationsHandler, false)).Methods("GET")
r.Handle("/api/integrations/{name}", authenticated(apiIntegrationHandler, false)).Methods("GET")
r.Handle("/api/integrations/{name}", authenticated(apiIntegrationHandler, false)).Methods("POST")
// API GROUPS Routes
r.Handle("/api/groups", readOnly(apiAllGroupHandler, false)).Methods("GET")
r.Handle("/api/groups", authenticated(apiCreateGroupHandler, false)).Methods("POST")
@ -103,20 +130,15 @@ func Router() *mux.Router {
r.Handle("/api/groups/{id}", authenticated(apiGroupDeleteHandler, false)).Methods("DELETE")
r.Handle("/api/reorder/groups", authenticated(apiGroupReorderHandler, false)).Methods("POST")
// API Routes
r.Handle("/api", authenticated(apiIndexHandler, false))
r.Handle("/api/renew", authenticated(apiRenewHandler, false))
r.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
// API SERVICE Routes
r.Handle("/api/services", readOnly(apiAllServicesHandler, false)).Methods("GET")
r.Handle("/api/services", authenticated(apiCreateServiceHandler, false)).Methods("POST")
r.Handle("/api/services/{id}", readOnly(apiServiceHandler, false)).Methods("GET")
r.Handle("/api/reorder/services", authenticated(reorderServiceHandler, false)).Methods("POST")
r.Handle("/api/services/{id}/running", authenticated(apiServiceRunningHandler, false)).Methods("POST")
r.Handle("/api/services/{id}/data", cached("30s", "application/json", http.HandlerFunc(apiServiceDataHandler))).Methods("GET")
r.Handle("/api/services/{id}/ping", cached("30s", "application/json", http.HandlerFunc(apiServicePingDataHandler))).Methods("GET")
r.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", http.HandlerFunc(apiServiceHeatmapHandler))).Methods("GET")
r.Handle("/api/services/{id}/data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET")
r.Handle("/api/services/{id}/ping", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET")
r.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
r.Handle("/api/services/{id}", authenticated(apiServiceUpdateHandler, false)).Methods("POST")
r.Handle("/api/services/{id}", authenticated(apiServiceDeleteHandler, false)).Methods("DELETE")
r.Handle("/api/services/{id}/failures", authenticated(apiServiceFailuresHandler, false)).Methods("GET")
@ -174,7 +196,7 @@ func resetRouter() {
func resetCookies() {
if core.CoreApp != nil {
cookie := fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, time.Now().Nanosecond())
cookie := fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, utils.Now().Nanosecond())
sessionStore = sessions.NewCookieStore([]byte(cookie))
} else {
sessionStore = sessions.NewCookieStore([]byte("secretinfo"))

View File

@ -33,8 +33,8 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60")
end := time.Now().UTC()
start := time.Now().Add((-24 * 7) * time.Hour).UTC()
end := utils.Now().UTC()
start := utils.Now().Add((-24 * 7) * time.Hour).UTC()
var srvs []*core.Service
for _, s := range services {
srvs = append(srvs, s.(*core.Service))
@ -94,7 +94,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
endField := utils.ToInt(fields.Get("end"))
group := r.Form.Get("group")
end := time.Now().UTC()
end := utils.Now().UTC()
start := end.Add((-24 * 7) * time.Hour).UTC()
if startField != 0 {
@ -243,7 +243,7 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
var monthOutput []*dataXyMonth
start := service.CreatedAt
//now := time.Now()
//now := utils.Now()
sY, sM, _ := start.Date()
@ -252,10 +252,10 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
month := int(sM)
maxMonth := 12
for year := int(sY); year <= time.Now().Year(); year++ {
for year := int(sY); year <= utils.Now().Year(); year++ {
if year == time.Now().Year() {
maxMonth = int(time.Now().Month())
if year == utils.Now().Year() {
maxMonth = int(utils.Now().Month())
}
for m := month; m <= maxMonth; m++ {
@ -335,3 +335,7 @@ func apiServiceHitsHandler(w http.ResponseWriter, r *http.Request) {
returnJson(hits, w, r)
}
func createServiceHandler(w http.ResponseWriter, r *http.Request) {
ExecuteResponse(w, r, "service_create.gohtml", core.CoreApp, nil)
}

View File

@ -1,7 +1,7 @@
package handlers
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
@ -31,10 +31,7 @@ func TestServiceRoutes(t *testing.T) {
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}

View File

@ -18,7 +18,9 @@ package handlers
import (
"bytes"
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/core/integrations"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
@ -62,13 +64,16 @@ func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
timeFloat, _ := strconv.ParseFloat(timezone, 10)
app.Timezone = float32(timeFloat)
app.UpdateNotify = types.NewNullBool(form.Get("update_notify") == "true")
app.UseCdn = types.NewNullBool(form.Get("enable_cdn") == "on")
core.CoreApp, err = core.UpdateCore(app)
if err != nil {
utils.Log(3, fmt.Sprintf("issue updating Core: %v", err.Error()))
log.Errorln(fmt.Sprintf("issue updating Core: %v", err.Error()))
}
//notifiers.OnSettingsSaved(core.CoreApp.ToCore())
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings")
}
func saveSASSHandler(w http.ResponseWriter, r *http.Request) {
@ -81,37 +86,37 @@ func saveSASSHandler(w http.ResponseWriter, r *http.Request) {
source.SaveAsset([]byte(mobile), utils.Directory, "scss/mobile.scss")
source.CompileSASS(utils.Directory)
resetRouter()
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings")
}
func saveAssetsHandler(w http.ResponseWriter, r *http.Request) {
dir := utils.Directory
if err := source.CreateAllAssets(dir); err != nil {
utils.Log(3, err)
log.Errorln(err)
sendErrorJson(err, w, r)
return
}
if err := source.CompileSASS(dir); err != nil {
source.CopyToPublic(source.CssBox, dir+"/assets/css", "base.css")
utils.Log(3, "Default 'base.css' was inserted because SASS did not work.")
log.Errorln("Default 'base.css' was inserted because SASS did not work.")
}
resetRouter()
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings")
}
func deleteAssetsHandler(w http.ResponseWriter, r *http.Request) {
if err := source.DeleteAllAssets(utils.Directory); err != nil {
utils.Log(3, fmt.Errorf("error deleting all assets %v", err))
log.Errorln(fmt.Errorf("error deleting all assets %v", err))
}
resetRouter()
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings")
}
func bulkImportHandler(w http.ResponseWriter, r *http.Request) {
var fileData bytes.Buffer
file, _, err := r.FormFile("file")
if err != nil {
utils.Log(3, fmt.Errorf("error bulk import services: %v", err))
log.Errorln(fmt.Errorf("error bulk import services: %v", err))
w.Write([]byte(err.Error()))
return
}
@ -125,26 +130,74 @@ func bulkImportHandler(w http.ResponseWriter, r *http.Request) {
newService, err := commaToService(col)
if err != nil {
utils.Log(3, fmt.Errorf("issue with row %v: %v", i, err))
log.Errorln(fmt.Errorf("issue with row %v: %v", i, err))
continue
}
service := core.ReturnService(newService)
_, err = service.Create(true)
if err != nil {
utils.Log(3, fmt.Errorf("cannot create service %v: %v", col[0], err))
log.Errorln(fmt.Errorf("cannot create service %v: %v", col[0], err))
continue
}
utils.Log(1, fmt.Sprintf("Created new service %v", service.Name))
log.Infoln(fmt.Sprintf("Created new service %v", service.Name))
}
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
}
type integratorOut struct {
Integrator *types.Integration `json:"integrator"`
Services []*types.Service `json:"services"`
Error error
}
func integratorHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
integratorName := vars["name"]
r.ParseForm()
integrator, err := integrations.Find(integratorName)
if err != nil {
log.Errorln(err)
ExecuteResponse(w, r, "integrator.gohtml", integratorOut{
Error: err,
}, nil)
return
}
log.Info(r.PostForm)
for _, v := range integrator.Get().Fields {
log.Info(v.Name, v.Value)
}
integrations.SetFields(integrator, r.PostForm)
for _, v := range integrator.Get().Fields {
log.Info(v.Name, v.Value)
}
services, err := integrator.List()
if err != nil {
log.Errorln(err)
ExecuteResponse(w, r, "integrator.gohtml", integratorOut{
Integrator: integrator.Get(),
Error: err,
}, nil)
return
}
ExecuteResponse(w, r, "integrator.gohtml", integratorOut{
Integrator: integrator.Get(),
Services: 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) != 16 {
if len(s) != 17 {
err := fmt.Errorf("does not have the expected amount of %v columns for a service", 16)
return nil, err
}
@ -169,6 +222,11 @@ func commaToService(s []string) (*types.Service, error) {
return nil, err
}
verifySsl, err := strconv.ParseBool(s[16])
if err != nil {
return nil, err
}
newService := &types.Service{
Name: s[0],
Domain: s[1],
@ -185,6 +243,7 @@ func commaToService(s []string) (*types.Service, error) {
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

@ -26,7 +26,7 @@ import (
func setupHandler(w http.ResponseWriter, r *http.Request) {
if core.CoreApp.Services != nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
return
}
var data interface{}
@ -39,8 +39,8 @@ func setupHandler(w http.ResponseWriter, r *http.Request) {
func processSetupHandler(w http.ResponseWriter, r *http.Request) {
var err error
if core.Services() != nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
if !core.SetupMode {
http.Redirect(w, r, basePath, http.StatusSeeOther)
return
}
r.ParseForm()
@ -57,10 +57,9 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
domain := r.PostForm.Get("domain")
email := r.PostForm.Get("email")
sample := r.PostForm.Get("sample_data") == "on"
utils.Log(2, sample)
dir := utils.Directory
config := &core.DbConfig{
config := &types.DbConfig{
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
@ -77,34 +76,36 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
Location: utils.Directory,
}
if core.Configs, err = config.Save(); err != nil {
utils.Log(4, err)
log.WithFields(utils.ToFields(core.CoreApp, config)).Debugln("new configs posted")
if _, err := core.CoreApp.SaveConfig(config); err != nil {
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
return
}
if core.Configs, err = core.LoadConfigFile(dir); err != nil {
utils.Log(3, err)
if _, err = core.LoadConfigFile(dir); err != nil {
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
return
}
if err = core.Configs.Connect(false, dir); err != nil {
utils.Log(4, err)
if err = core.CoreApp.Connect(false, dir); err != nil {
log.Errorln(err)
core.DeleteConfig()
config.Error = err
setupResponseError(w, r, config)
return
}
config.DropDatabase()
config.CreateDatabase()
core.CoreApp.DropDatabase()
core.CoreApp.CreateDatabase()
core.CoreApp, err = config.InsertCore()
core.CoreApp, err = core.CoreApp.InsertCore(config)
if err != nil {
utils.Log(4, err)
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
return
@ -125,7 +126,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
CacheStorage.Delete("/")
resetCookies()
time.Sleep(2 * time.Second)
http.Redirect(w, r, "/", http.StatusSeeOther)
http.Redirect(w, r, basePath, http.StatusSeeOther)
}
func setupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) {

View File

@ -1,7 +1,7 @@
package handlers
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
@ -19,16 +19,13 @@ func TestUserRoutes(t *testing.T) {
URL: "/user/2",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`<title>Statping | adminuser2</title>`},
ExpectedContains: []string{`<title>Statping | testadmin2</title>`},
}}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
if err != nil {
t.FailNow()
}
require.Nil(t, err)
})
}
}

View File

@ -1,5 +0,0 @@
build:
docker:
web: Dockerfile
run:
web: statping -port $PORT

175
install.sh Executable file
View File

@ -0,0 +1,175 @@
#!/bin/bash
# Statping installation script for Linux, Mac, and maybe Windows.
#
# This installation script is a modification of Yarn's installation
set -e
reset="\033[0m"
red="\033[31m"
green="\033[32m"
yellow="\033[33m"
cyan="\033[36m"
white="\033[37m"
gpg_key=64B9C6AAE2D55278
gpgurl=https://statping.com/statping.gpg
repo=https://github.com/hunterlong/statping
statping_get_tarball() {
fext='tar.gz'
if [ ${OS} == 'windows' ]; then
fext='zip'
ARCH='x64'
fi
url="$repo/releases/latest/download/statping-$1-$2.$fext"
printf "$cyan> Downloading latest version for $OS $ARCH...\n$url $reset\n"
# Get both the tarball and its GPG signature
tarball_tmp=`mktemp -t statping.tar.gz.XXXXXXXXXX`
if curl --fail -L -o "$tarball_tmp" "$url"; then
# All this dance is because `tar --strip=1` does not work everywhere
temp=$(mktemp -d statping.XXXXXXXXXX)
if [ ${OS} == 'windows' ]; then
unzip $tarball_tmp -d "$temp"
else
tar xzf $tarball_tmp -C "$temp"
fi
statping_verify_integrity "$temp"/statping
printf "$green> Installing to $DEST/statping\n"
mv "$temp"/statping "$DEST"
newversion=`$DEST/statping version`
rm -rf "$temp"
rm $tarball_tmp*
printf "$cyan> Statping is now installed! $reset\n"
printf "$white> Version: $newversion $reset\n"
printf "$white> Repo: $repo $reset\n"
printf "$white> Wiki: $repo/wiki $reset\n"
printf "$white> Issues: $repo/issues $reset\n"
printf "$cyan> Try to run \"statping help\" $reset\n"
else
printf "$red> Failed to download $url.$reset\n"
exit 1;
fi
}
# Verifies the GPG signature of the tarball
statping_verify_integrity() {
# Check if GPG is installed
if [[ -z "$(command -v gpg)" ]]; then
printf "$yellow> WARNING: GPG is not installed, integrity can not be verified!$reset\n"
return
fi
if [ "$statping_GPG" == "no" ]; then
printf "$cyan> WARNING: Skipping GPG integrity check!$reset\n"
return
fi
printf "$cyan> Verifying integrity with gpg key from $gpgurl...$reset\n"
# Grab the public key if it doesn't already exist
gpg --list-keys $gpg_key >/dev/null 2>&1 || (curl -sS -L $gpgurl | gpg --import)
if [ ! -f "$1.asc" ]; then
printf "$red> Could not download GPG signature for this Statping release. This means the release can not be verified!$reset\n"
statping_verify_or_quit "> Do you really want to continue?"
return
fi
# Actually perform the verification
if gpg --verify "$1.asc" $1 &> /dev/null; then
printf "$green> GPG signature looks good$reset\n"
else
printf "$red> GPG signature for this Statping release is invalid! This is BAD and may mean the release has been tampered with. It is strongly recommended that you report this to the Statping developers.$reset\n"
statping_verify_or_quit "> Do you really want to continue?"
fi
}
statping_reset() {
unset -f statping_install statping_reset statping_get_tarball statping_verify_integrity statping_verify_or_quit statping_brew_install getOS getArch
}
statping_brew_install() {
if [[ -z "$(command -v brew --version)" ]]; then
printf "${white}Using Brew to install!$reset\n"
printf "${yellow}---> brew tap hunterlong/statping$reset\n"
brew tap hunterlong/statping
printf "${yellow}---> brew install statping$reset\n"
brew install statping
printf "${green}Brew installation is complete!$reset\n"
printf "${yellow}You can use 'brew upgrade' to upgrade Statping next time.$reset\n"
else
statping_get_tarball $OS $ARCH
fi
}
statping_install() {
printf "${white}Installing Statping!$reset\n"
getOS
getArch
if [ "$OS" == "osx" ]; then
statping_brew_install
else
statping_get_tarball $OS $ARCH
fi
statping_reset
}
statping_verify_or_quit() {
read -p "$1 [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
printf "$red> Aborting$reset\n"
exit 1
fi
}
# get the users operating system
getOS() {
OS="`uname`"
case $OS in
'Linux')
OS='linux'
DEST=/usr/local/bin
alias ls='ls --color=auto'
;;
'FreeBSD')
OS='freebsd'
DEST=/usr/local/bin
alias ls='ls -G'
;;
'WindowsNT')
OS='windows'
DEST=/usr/local/bin
;;
'MINGW*')
OS='windows'
DEST=/usr/local/bin
;;
'CYGWIN*')
OS='windows'
DEST=/usr/local/bin
;;
'Darwin')
OS='osx'
DEST=/usr/local/bin
;;
'SunOS')
OS='solaris'
DEST=/usr/local/bin
;;
'AIX') ;;
*) ;;
esac
}
# get 64x or 32 machine arch
getArch() {
MACHINE_TYPE=`uname -m`
if [ ${MACHINE_TYPE} == 'x86_64' ]; then
ARCH="x64"
else
ARCH="x32"
fi
}
cd ~
statping_install $1 $2

View File

@ -27,10 +27,10 @@ type commandLine struct {
*notifier.Notification
}
var command = &commandLine{&notifier.Notification{
Method: "command",
var Command = &commandLine{&notifier.Notification{
Method: "Command",
Title: "Shell Command",
Description: "Shell Command allows you to run a customized shell/bash command on the local machine it's running on.",
Description: "Shell Command allows you to run a customized shell/bash Command on the local machine it's running on.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(1 * time.Second),
@ -47,24 +47,16 @@ var command = &commandLine{&notifier.Notification{
Title: "Command to Run on OnSuccess",
Placeholder: "curl google.com",
DbField: "var1",
SmallText: "This command will run every time a service is receiving a Successful event.",
SmallText: "This Command will run every time a service is receiving a Successful event.",
}, {
Type: "text",
Title: "Command to Run on OnFailure",
Placeholder: "curl offline.com",
DbField: "var2",
SmallText: "This command will run every time a service is receiving a Failing event.",
SmallText: "This Command will run every time a service is receiving a Failing event.",
}}},
}
// init the command notifier
func init() {
err := notifier.AddNotifier(command)
if err != nil {
panic(err)
}
}
func runCommand(app, cmd string) (string, string, error) {
outStr, errStr, err := utils.Command(cmd)
return outStr, errStr, err
@ -77,16 +69,14 @@ func (u *commandLine) Select() *notifier.Notification {
// OnFailure for commandLine will trigger failing service
func (u *commandLine) OnFailure(s *types.Service, f *types.Failure) {
u.AddQueue(fmt.Sprintf("service_%v", s.Id), u.Var2)
u.Online = false
}
// OnSuccess for commandLine will trigger successful service
func (u *commandLine) OnSuccess(s *types.Service) {
if !u.Online {
if !s.Online {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
u.AddQueue(fmt.Sprintf("service_%v", s.Id), u.Var1)
}
u.Online = true
}
// OnSave for commandLine triggers when this notifier has been saved
@ -99,12 +89,12 @@ func (u *commandLine) OnSave() error {
// OnTest for commandLine triggers when this notifier has been saved
func (u *commandLine) OnTest() error {
in, out, err := runCommand(u.Host, u.Var1)
utils.Log(1, in)
utils.Log(1, out)
utils.Log.Infoln(in)
utils.Log.Infoln(out)
return err
}
// Send for commandLine will send message to expo command push notifications endpoint
// Send for commandLine will send message to expo Command push notifications endpoint
func (u *commandLine) Send(msg interface{}) error {
cmd := msg.(string)
_, _, err := runCommand(u.Host, cmd)

View File

@ -28,96 +28,75 @@ const (
func TestCommandNotifier(t *testing.T) {
t.Parallel()
command.Host = "sh"
command.Var1 = commandTest
command.Var2 = commandTest
Command.Host = "sh"
Command.Var1 = commandTest
Command.Var2 = commandTest
currentCount = CountNotifiers()
t.Run("Load command", func(t *testing.T) {
command.Host = "sh"
command.Var1 = commandTest
command.Var2 = commandTest
command.Delay = time.Duration(100 * time.Millisecond)
command.Limits = 99
err := notifier.AddNotifier(command)
t.Run("Load Command", func(t *testing.T) {
Command.Host = "sh"
Command.Var1 = commandTest
Command.Var2 = commandTest
Command.Delay = time.Duration(100 * time.Millisecond)
Command.Limits = 99
err := notifier.AddNotifiers(Command)
assert.Nil(t, err)
assert.Equal(t, "Hunter Long", command.Author)
assert.Equal(t, "sh", command.Host)
assert.Equal(t, commandTest, command.Var1)
assert.Equal(t, commandTest, command.Var2)
assert.Equal(t, "Hunter Long", Command.Author)
assert.Equal(t, "sh", Command.Host)
assert.Equal(t, commandTest, Command.Var1)
assert.Equal(t, commandTest, Command.Var2)
})
t.Run("Load command Notifier", func(t *testing.T) {
notifier.Load()
t.Run("Command Notifier Tester", func(t *testing.T) {
assert.True(t, Command.CanTest())
})
t.Run("command Notifier Tester", func(t *testing.T) {
assert.True(t, command.CanTest())
})
t.Run("command Within Limits", func(t *testing.T) {
ok, err := command.WithinLimits()
t.Run("Command Within Limits", func(t *testing.T) {
ok, err := Command.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("command OnFailure", func(t *testing.T) {
command.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(command.Queue))
t.Run("Command OnFailure", func(t *testing.T) {
Command.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(Command.Queue))
})
t.Run("command OnFailure multiple times", func(t *testing.T) {
for i := 0; i <= 50; i++ {
command.OnFailure(TestService, TestFailure)
}
assert.Equal(t, 52, len(command.Queue))
t.Run("Command OnSuccess", func(t *testing.T) {
Command.OnSuccess(TestService)
assert.Equal(t, 1, len(Command.Queue))
})
t.Run("command Check Offline", func(t *testing.T) {
assert.False(t, command.Online)
t.Run("Command OnSuccess Again", func(t *testing.T) {
Command.OnSuccess(TestService)
assert.Equal(t, 1, len(Command.Queue))
go notifier.Queue(Command)
time.Sleep(20 * time.Second)
assert.Equal(t, 0, len(Command.Queue))
})
t.Run("command OnSuccess", func(t *testing.T) {
command.OnSuccess(TestService)
assert.Equal(t, 1, len(command.Queue))
})
t.Run("command Queue after being online", func(t *testing.T) {
assert.True(t, command.Online)
assert.Equal(t, 1, len(command.Queue))
})
t.Run("command OnSuccess Again", func(t *testing.T) {
assert.True(t, command.Online)
command.OnSuccess(TestService)
assert.Equal(t, 1, len(command.Queue))
go notifier.Queue(command)
time.Sleep(5 * time.Second)
assert.Equal(t, 0, len(command.Queue))
})
t.Run("command Within Limits again", func(t *testing.T) {
ok, err := command.WithinLimits()
t.Run("Command Within Limits again", func(t *testing.T) {
ok, err := Command.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("command Send", func(t *testing.T) {
command.Send(commandTest)
assert.Equal(t, 0, len(command.Queue))
t.Run("Command Send", func(t *testing.T) {
Command.Send(commandTest)
assert.Equal(t, 0, len(Command.Queue))
})
t.Run("command Test", func(t *testing.T) {
command.OnTest()
t.Run("Command Test", func(t *testing.T) {
Command.OnTest()
})
t.Run("command Queue", func(t *testing.T) {
go notifier.Queue(command)
t.Run("Command Queue", func(t *testing.T) {
go notifier.Queue(Command)
time.Sleep(5 * time.Second)
assert.Equal(t, "sh", command.Host)
assert.Equal(t, commandTest, command.Var1)
assert.Equal(t, commandTest, command.Var2)
assert.Equal(t, 0, len(command.Queue))
assert.Equal(t, "sh", Command.Host)
assert.Equal(t, commandTest, Command.Var1)
assert.Equal(t, commandTest, Command.Var2)
assert.Equal(t, 0, len(Command.Queue))
})
}

View File

@ -31,10 +31,10 @@ type discord struct {
*notifier.Notification
}
var discorder = &discord{&notifier.Notification{
var Discorder = &discord{&notifier.Notification{
Method: "discord",
Title: "discord",
Description: "Send notifications to your discord channel using discord webhooks. Insert your discord channel webhook URL to receive notifications. Based on the <a href=\"https://discordapp.com/developers/docs/resources/webhook\">discord webhooker API</a>.",
Description: "Send notifications to your discord channel using discord webhooks. Insert your discord channel Webhook URL to receive notifications. Based on the <a href=\"https://discordapp.com/developers/docs/resources/Webhook\">discord webhooker API</a>.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(5 * time.Second),
@ -43,23 +43,15 @@ var discorder = &discord{&notifier.Notification{
Form: []notifier.NotificationForm{{
Type: "text",
Title: "discord webhooker URL",
Placeholder: "Insert your webhook URL here",
Placeholder: "Insert your Webhook URL here",
DbField: "host",
}}},
}
// init the discord notifier
func init() {
err := notifier.AddNotifier(discorder)
if err != nil {
panic(err)
}
}
// Send will send a HTTP Post to the discord API. It accepts type: []byte
func (u *discord) Send(msg interface{}) error {
message := msg.(string)
_, _, err := utils.HttpRequest(discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second))
_, _, err := utils.HttpRequest(Discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second), true)
return err
}
@ -71,17 +63,20 @@ func (u *discord) Select() *notifier.Notification {
func (u *discord) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf(`{"content": "Your service '%v' is currently failing! Reason: %v"}`, s.Name, f.Issue)
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
u.Online = false
}
// OnSuccess will trigger successful service
func (u *discord) OnSuccess(s *types.Service) {
if !u.Online {
if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
msg := fmt.Sprintf(`{"content": "Your service '%v' is back online!"}`, s.Name)
var msg interface{}
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved
@ -95,7 +90,7 @@ func (u *discord) OnSave() error {
func (u *discord) OnTest() error {
outError := errors.New("Incorrect discord URL, please confirm URL is correct")
message := `{"content": "Testing the discord notifier"}`
contents, _, err := utils.HttpRequest(discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second))
contents, _, err := utils.HttpRequest(Discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second), true)
if string(contents) == "" {
return nil
}

View File

@ -30,7 +30,7 @@ var (
func init() {
DISCORD_URL = os.Getenv("DISCORD_URL")
discorder.Host = DISCORD_URL
Discorder.Host = DISCORD_URL
}
func TestDiscordNotifier(t *testing.T) {
@ -42,66 +42,58 @@ func TestDiscordNotifier(t *testing.T) {
currentCount = CountNotifiers()
t.Run("Load discord", func(t *testing.T) {
discorder.Host = DISCORD_URL
discorder.Delay = time.Duration(100 * time.Millisecond)
err := notifier.AddNotifier(discorder)
Discorder.Host = DISCORD_URL
Discorder.Delay = time.Duration(100 * time.Millisecond)
err := notifier.AddNotifiers(Discorder)
assert.Nil(t, err)
assert.Equal(t, "Hunter Long", discorder.Author)
assert.Equal(t, DISCORD_URL, discorder.Host)
})
t.Run("Load discord Notifier", func(t *testing.T) {
notifier.Load()
assert.Equal(t, "Hunter Long", Discorder.Author)
assert.Equal(t, DISCORD_URL, Discorder.Host)
})
t.Run("discord Notifier Tester", func(t *testing.T) {
assert.True(t, discorder.CanTest())
assert.True(t, Discorder.CanTest())
})
t.Run("discord Within Limits", func(t *testing.T) {
ok, err := discorder.WithinLimits()
ok, err := Discorder.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("discord OnFailure", func(t *testing.T) {
discorder.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(discorder.Queue))
})
t.Run("discord Check Offline", func(t *testing.T) {
assert.False(t, discorder.Online)
Discorder.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(Discorder.Queue))
})
t.Run("discord OnSuccess", func(t *testing.T) {
discorder.OnSuccess(TestService)
assert.Equal(t, 1, len(discorder.Queue))
Discorder.OnSuccess(TestService)
assert.Equal(t, 1, len(Discorder.Queue))
})
t.Run("discord Check Back Online", func(t *testing.T) {
assert.True(t, discorder.Online)
assert.True(t, TestService.Online)
})
t.Run("discord OnSuccess Again", func(t *testing.T) {
discorder.OnSuccess(TestService)
assert.Equal(t, 1, len(discorder.Queue))
Discorder.OnSuccess(TestService)
assert.Equal(t, 1, len(Discorder.Queue))
})
t.Run("discord Send", func(t *testing.T) {
err := discorder.Send(discordMessage)
err := Discorder.Send(discordMessage)
assert.Nil(t, err)
})
t.Run("discord Test", func(t *testing.T) {
err := discorder.OnTest()
err := Discorder.OnTest()
assert.Nil(t, err)
})
t.Run("discord Queue", func(t *testing.T) {
go notifier.Queue(discorder)
go notifier.Queue(Discorder)
time.Sleep(1 * time.Second)
assert.Equal(t, DISCORD_URL, discorder.Host)
assert.Equal(t, 0, len(discorder.Queue))
assert.Equal(t, DISCORD_URL, Discorder.Host)
assert.Equal(t, 0, len(Discorder.Queue))
})
}

View File

@ -24,7 +24,6 @@ import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
"net/smtp"
"time"
)
@ -112,7 +111,7 @@ type email struct {
*notifier.Notification
}
var emailer = &email{&notifier.Notification{
var Emailer = &email{&notifier.Notification{
Method: "email",
Title: "email",
Description: "Send emails via SMTP when services are online or offline.",
@ -158,13 +157,6 @@ var emailer = &email{&notifier.Notification{
}},
}}
func init() {
err := notifier.AddNotifier(emailer)
if err != nil {
panic(err)
}
}
// Send will send the SMTP email with your authentication It accepts type: *emailOutgoing
func (u *email) Send(msg interface{}) error {
email := msg.(*emailOutgoing)
@ -195,23 +187,27 @@ func (u *email) OnFailure(s *types.Service, f *types.Failure) {
From: u.Var1,
}
u.AddQueue(fmt.Sprintf("service_%v", s.Id), email)
u.Online = false
}
// OnSuccess will trigger successful service
func (u *email) OnSuccess(s *types.Service) {
if !u.Online {
if !s.Online || !s.SuccessNotified {
var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
email := &emailOutgoing{
To: u.Var2,
Subject: fmt.Sprintf("Service %v is Back Online", s.Name),
Subject: msg,
Template: mainEmailTemplate,
Data: interface{}(s),
From: u.Var1,
}
u.AddQueue(fmt.Sprintf("service_%v", s.Id), email)
}
u.Online = true
}
func (u *email) Select() *notifier.Notification {
@ -220,31 +216,13 @@ func (u *email) Select() *notifier.Notification {
// OnSave triggers when this notifier has been saved
func (u *email) OnSave() error {
utils.Log(1, fmt.Sprintf("Notification %v is receiving updated information.", u.Method))
utils.Log.Infoln(fmt.Sprintf("Notification %v is receiving updated information.", u.Method))
// Do updating stuff here
return nil
}
// OnTest triggers when this notifier has been saved
func (u *email) OnTest() error {
host := fmt.Sprintf("%v:%v", u.Host, u.Port)
dial, err := smtp.Dial(host)
if err != nil {
return err
}
if u.ApiKey != "true" {
err = dial.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return err
}
}
if u.Username != "" || u.Password != "" {
auth := smtp.PlainAuth("", u.Username, u.Password, host)
err = dial.Auth(auth)
if err != nil {
return err
}
}
testService := &types.Service{
Id: 1,
Name: "Example Service",
@ -257,21 +235,20 @@ func (u *email) OnTest() error {
LastStatusCode: 200,
Expected: types.NewNullString("test example"),
LastResponse: "<html>this is an example response</html>",
CreatedAt: time.Now().Add(-24 * time.Hour),
CreatedAt: utils.Now().Add(-24 * time.Hour),
}
email := &emailOutgoing{
To: u.Var2,
Subject: fmt.Sprintf("Service %v is Back Online", testService.Name),
Template: mainEmailTemplate,
Data: interface{}(testService),
Data: testService,
From: u.Var1,
}
err = u.Send(email)
return err
return u.dialSend(email)
}
func (u *email) dialSend(email *emailOutgoing) error {
mailer = mail.NewDialer(emailer.Host, emailer.Port, emailer.Username, emailer.Password)
mailer = mail.NewDialer(Emailer.Host, Emailer.Port, Emailer.Username, Emailer.Password)
emailSource(email)
m := mail.NewMessage()
// if email setting TLS is Disabled
@ -285,7 +262,7 @@ func (u *email) dialSend(email *emailOutgoing) error {
m.SetHeader("Subject", email.Subject)
m.SetBody("text/html", email.Source)
if err := mailer.DialAndSend(m); err != nil {
utils.Log(3, fmt.Sprintf("email '%v' sent to: %v (size: %v) %v", email.Subject, email.To, len([]byte(email.Source)), err))
utils.Log.Errorln(fmt.Sprintf("email '%v' sent to: %v (size: %v) %v", email.Subject, email.To, len([]byte(email.Source)), err))
return err
}
return nil
@ -300,11 +277,11 @@ func emailTemplate(contents string, data interface{}) string {
t := template.New("email")
t, err := t.Parse(contents)
if err != nil {
utils.Log(3, err)
utils.Log.Errorln(err)
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, data); err != nil {
utils.Log(2, err)
utils.Log.Warnln(err)
}
result := tpl.String()
return result

View File

@ -44,12 +44,12 @@ func init() {
EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO")
EMAIL_PORT = utils.ToInt(os.Getenv("EMAIL_PORT"))
emailer.Host = EMAIL_HOST
emailer.Username = EMAIL_USER
emailer.Password = EMAIL_PASS
emailer.Var1 = EMAIL_OUTGOING
emailer.Var2 = EMAIL_SEND_TO
emailer.Port = int(EMAIL_PORT)
Emailer.Host = EMAIL_HOST
Emailer.Username = EMAIL_USER
Emailer.Password = EMAIL_PASS
Emailer.Var1 = EMAIL_OUTGOING
Emailer.Var2 = EMAIL_SEND_TO
Emailer.Port = int(EMAIL_PORT)
}
func TestEmailNotifier(t *testing.T) {
@ -61,36 +61,32 @@ func TestEmailNotifier(t *testing.T) {
currentCount = CountNotifiers()
t.Run("New Emailer", func(t *testing.T) {
emailer.Host = EMAIL_HOST
emailer.Username = EMAIL_USER
emailer.Password = EMAIL_PASS
emailer.Var1 = EMAIL_OUTGOING
emailer.Var2 = EMAIL_SEND_TO
emailer.Port = int(EMAIL_PORT)
emailer.Delay = time.Duration(100 * time.Millisecond)
Emailer.Host = EMAIL_HOST
Emailer.Username = EMAIL_USER
Emailer.Password = EMAIL_PASS
Emailer.Var1 = EMAIL_OUTGOING
Emailer.Var2 = EMAIL_SEND_TO
Emailer.Port = int(EMAIL_PORT)
Emailer.Delay = time.Duration(100 * time.Millisecond)
testEmail = &emailOutgoing{
To: emailer.GetValue("var2"),
To: Emailer.GetValue("var2"),
Subject: fmt.Sprintf("Service %v is Failing", TestService.Name),
Template: mainEmailTemplate,
Data: TestService,
From: emailer.GetValue("var1"),
From: Emailer.GetValue("var1"),
}
})
t.Run("Add email Notifier", func(t *testing.T) {
err := notifier.AddNotifier(emailer)
err := notifier.AddNotifiers(Emailer)
assert.Nil(t, err)
assert.Equal(t, "Hunter Long", emailer.Author)
assert.Equal(t, EMAIL_HOST, emailer.Host)
})
t.Run("Emailer Load", func(t *testing.T) {
notifier.Load()
assert.Equal(t, "Hunter Long", Emailer.Author)
assert.Equal(t, EMAIL_HOST, Emailer.Host)
})
t.Run("email Within Limits", func(t *testing.T) {
ok, err := emailer.WithinLimits()
ok, err := Emailer.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
@ -101,44 +97,40 @@ func TestEmailNotifier(t *testing.T) {
})
t.Run("email OnFailure", func(t *testing.T) {
emailer.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(emailer.Queue))
})
t.Run("email Check Offline", func(t *testing.T) {
assert.False(t, emailer.Online)
Emailer.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(Emailer.Queue))
})
t.Run("email OnSuccess", func(t *testing.T) {
emailer.OnSuccess(TestService)
assert.Equal(t, 1, len(emailer.Queue))
Emailer.OnSuccess(TestService)
assert.Equal(t, 1, len(Emailer.Queue))
})
t.Run("email Check Back Online", func(t *testing.T) {
assert.True(t, emailer.Online)
assert.True(t, TestService.Online)
})
t.Run("email OnSuccess Again", func(t *testing.T) {
emailer.OnSuccess(TestService)
assert.Equal(t, 1, len(emailer.Queue))
Emailer.OnSuccess(TestService)
assert.Equal(t, 1, len(Emailer.Queue))
})
t.Run("email Send", func(t *testing.T) {
err := emailer.Send(testEmail)
err := Emailer.Send(testEmail)
assert.Nil(t, err)
})
t.Run("emailer Test", func(t *testing.T) {
t.Run("Emailer Test", func(t *testing.T) {
t.SkipNow()
err := emailer.OnTest()
err := Emailer.OnTest()
assert.Nil(t, err)
})
t.Run("email Run Queue", func(t *testing.T) {
go notifier.Queue(emailer)
go notifier.Queue(Emailer)
time.Sleep(6 * time.Second)
assert.Equal(t, EMAIL_HOST, emailer.Host)
assert.Equal(t, 0, len(emailer.Queue))
assert.Equal(t, EMAIL_HOST, Emailer.Host)
assert.Equal(t, 0, len(Emailer.Queue))
})
}

View File

@ -33,7 +33,7 @@ type lineNotifier struct {
*notifier.Notification
}
var lineNotify = &lineNotifier{&notifier.Notification{
var LineNotify = &lineNotifier{&notifier.Notification{
Method: lineNotifyMethod,
Title: "LINE Notify",
Description: "LINE Notify will send notifications to your LINE Notify account when services are offline or online. Based on the <a href=\"https://notify-bot.line.me/doc/en/\">LINE Notify API</a>.",
@ -48,21 +48,13 @@ var lineNotify = &lineNotifier{&notifier.Notification{
}}},
}
// DEFINE YOUR NOTIFICATION HERE.
func init() {
err := notifier.AddNotifier(lineNotify)
if err != nil {
panic(err)
}
}
// Send will send a HTTP Post with the Authorization to the notify-api.line.me server. It accepts type: string
func (u *lineNotifier) Send(msg interface{}) error {
message := msg.(string)
v := url.Values{}
v.Set("message", message)
headers := []string{fmt.Sprintf("Authorization=Bearer %v", u.ApiSecret)}
_, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second))
_, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second), true)
return err
}
@ -74,22 +66,26 @@ func (u *lineNotifier) Select() *notifier.Notification {
func (u *lineNotifier) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
u.Online = false
}
// OnSuccess will trigger successful service
func (u *lineNotifier) OnSuccess(s *types.Service) {
if !u.Online {
if !s.Online || !s.SuccessNotified {
var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
msg := fmt.Sprintf("Your service '%v' is back online!", s.Name)
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved
func (u *lineNotifier) OnSave() error {
utils.Log(1, fmt.Sprintf("Notification %v is receiving updated information.", u.Method))
// Do updating stuff here
msg := fmt.Sprintf("Notification %v is receiving updated information.", u.Method)
utils.Log.Infoln(msg)
u.AddQueue("saved", msg)
return nil
}

View File

@ -31,10 +31,10 @@ type mobilePush struct {
*notifier.Notification
}
var mobile = &mobilePush{&notifier.Notification{
var Mobile = &mobilePush{&notifier.Notification{
Method: "mobile",
Title: "Mobile Notifications",
Description: `Receive push notifications on your mobile device using the Statping App. You can scan the Authentication QR Code found in Settings to get the mobile app setup in seconds.
Description: `Receive push notifications on your Mobile device using the Statping App. You can scan the Authentication QR Code found in Settings to get the Mobile app setup in seconds.
<p align="center"><a href="https://play.google.com/store/apps/details?id=com.statping"><img src="https://img.cjx.io/google-play.svg"></a><a href="https://itunes.apple.com/us/app/apple-store/id1445513219"><img src="https://img.cjx.io/app-store-badge.svg"></a></p>`,
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
@ -43,7 +43,7 @@ var mobile = &mobilePush{&notifier.Notification{
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Device Identifiers",
Placeholder: "A list of your mobile device push notification ID's.",
Placeholder: "A list of your Mobile device push notification ID's.",
DbField: "var1",
IsHidden: true,
}, {
@ -55,14 +55,6 @@ var mobile = &mobilePush{&notifier.Notification{
}}},
}
// init the discord notifier
func init() {
err := notifier.AddNotifier(mobile)
if err != nil {
panic(err)
}
}
func (u *mobilePush) Select() *notifier.Notification {
return u.Notification
}
@ -100,33 +92,31 @@ func (u *mobilePush) OnFailure(s *types.Service, f *types.Failure) {
Data: data,
}
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
u.Online = false
}
// OnSuccess will trigger successful service
func (u *mobilePush) OnSuccess(s *types.Service) {
data := dataJson(s, nil)
if !u.Online {
if !s.Online || !s.SuccessNotified {
var msgStr string
if s.UpdateNotify {
s.UpdateNotify = false
}
msgStr = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
msg := &pushArray{
Message: fmt.Sprintf("Your service '%v' is back online!", s.Name),
Message: msgStr,
Title: "Service Online",
Topic: mobileIdentifier,
Data: data,
}
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved
func (u *mobilePush) OnSave() error {
msg := &pushArray{
Message: "The Mobile Notifier has been saved",
Title: "Notification Saved",
Topic: mobileIdentifier,
}
u.AddQueue("saved", msg)
return nil
}
@ -178,7 +168,7 @@ func pushRequest(msg *pushArray) ([]byte, error) {
return nil, err
}
url := "https://push.statping.com/api/push"
body, _, err = utils.HttpRequest(url, "POST", "application/json", nil, bytes.NewBuffer(body), time.Duration(20*time.Second))
body, _, err = utils.HttpRequest(url, "POST", "application/json", nil, bytes.NewBuffer(body), time.Duration(20*time.Second), true)
return body, err
}

View File

@ -31,99 +31,79 @@ var (
func init() {
MOBILE_ID = os.Getenv("MOBILE_ID")
MOBILE_NUMBER = os.Getenv("MOBILE_NUMBER")
mobile.Var1 = MOBILE_ID
Mobile.Var1 = MOBILE_ID
}
func TestMobileNotifier(t *testing.T) {
t.Parallel()
mobile.Var1 = MOBILE_ID
mobile.Var2 = os.Getenv("MOBILE_NUMBER")
Mobile.Var1 = MOBILE_ID
Mobile.Var2 = os.Getenv("MOBILE_NUMBER")
if MOBILE_ID == "" {
t.Log("mobile notifier testing skipped, missing MOBILE_ID environment variable")
t.Log("Mobile notifier testing skipped, missing MOBILE_ID environment variable")
t.SkipNow()
}
currentCount = CountNotifiers()
t.Run("Load mobile", func(t *testing.T) {
mobile.Var1 = MOBILE_ID
mobile.Var2 = MOBILE_NUMBER
mobile.Delay = time.Duration(100 * time.Millisecond)
mobile.Limits = 10
err := notifier.AddNotifier(mobile)
t.Run("Load Mobile", func(t *testing.T) {
Mobile.Var1 = MOBILE_ID
Mobile.Var2 = MOBILE_NUMBER
Mobile.Delay = time.Duration(100 * time.Millisecond)
Mobile.Limits = 10
err := notifier.AddNotifiers(Mobile)
assert.Nil(t, err)
assert.Equal(t, "Hunter Long", mobile.Author)
assert.Equal(t, MOBILE_ID, mobile.Var1)
assert.Equal(t, MOBILE_NUMBER, mobile.Var2)
assert.Equal(t, "Hunter Long", Mobile.Author)
assert.Equal(t, MOBILE_ID, Mobile.Var1)
assert.Equal(t, MOBILE_NUMBER, Mobile.Var2)
})
t.Run("Load mobile Notifier", func(t *testing.T) {
notifier.Load()
t.Run("Mobile Notifier Tester", func(t *testing.T) {
assert.True(t, Mobile.CanTest())
})
t.Run("mobile Notifier Tester", func(t *testing.T) {
assert.True(t, mobile.CanTest())
})
t.Run("mobile Within Limits", func(t *testing.T) {
ok, err := mobile.WithinLimits()
t.Run("Mobile Within Limits", func(t *testing.T) {
ok, err := Mobile.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("mobile OnFailure", func(t *testing.T) {
mobile.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(mobile.Queue))
t.Run("Mobile OnFailure", func(t *testing.T) {
Mobile.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(Mobile.Queue))
})
t.Run("mobile OnFailure multiple times", func(t *testing.T) {
for i := 0; i <= 50; i++ {
mobile.OnFailure(TestService, TestFailure)
}
assert.Equal(t, 52, len(mobile.Queue))
t.Run("Mobile OnSuccess", func(t *testing.T) {
Mobile.OnSuccess(TestService)
assert.Equal(t, 1, len(Mobile.Queue))
})
t.Run("mobile Check Offline", func(t *testing.T) {
assert.False(t, mobile.Online)
})
t.Run("mobile OnSuccess", func(t *testing.T) {
mobile.OnSuccess(TestService)
assert.Equal(t, 1, len(mobile.Queue))
})
t.Run("mobile Queue after being online", func(t *testing.T) {
assert.True(t, mobile.Online)
assert.Equal(t, 1, len(mobile.Queue))
})
t.Run("mobile OnSuccess Again", func(t *testing.T) {
t.Run("Mobile OnSuccess Again", func(t *testing.T) {
t.SkipNow()
assert.True(t, mobile.Online)
mobile.OnSuccess(TestService)
assert.Equal(t, 1, len(mobile.Queue))
go notifier.Queue(mobile)
assert.True(t, TestService.Online)
Mobile.OnSuccess(TestService)
assert.Equal(t, 1, len(Mobile.Queue))
go notifier.Queue(Mobile)
time.Sleep(20 * time.Second)
assert.Equal(t, 1, len(mobile.Queue))
assert.Equal(t, 1, len(Mobile.Queue))
})
t.Run("mobile Within Limits again", func(t *testing.T) {
ok, err := mobile.WithinLimits()
t.Run("Mobile Within Limits again", func(t *testing.T) {
ok, err := Mobile.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("mobile Test", func(t *testing.T) {
t.Run("Mobile Test", func(t *testing.T) {
t.SkipNow()
err := mobile.OnTest()
err := Mobile.OnTest()
assert.Nil(t, err)
})
t.Run("mobile Queue", func(t *testing.T) {
t.Run("Mobile Queue", func(t *testing.T) {
t.SkipNow()
go notifier.Queue(mobile)
go notifier.Queue(Mobile)
time.Sleep(15 * time.Second)
assert.Equal(t, MOBILE_ID, mobile.Var1)
assert.Equal(t, 0, len(mobile.Queue))
assert.Equal(t, MOBILE_ID, Mobile.Var1)
assert.Equal(t, 0, len(Mobile.Queue))
})
}

View File

@ -42,14 +42,15 @@ var TestService = &types.Service{
Method: "GET",
Timeout: 20,
LastStatusCode: 404,
Online: true,
LastResponse: "<html>this is an example response</html>",
CreatedAt: time.Now().Add(-24 * time.Hour),
CreatedAt: utils.Now().Add(-24 * time.Hour),
}
var TestFailure = &types.Failure{
Issue: "testing",
Service: 1,
CreatedAt: time.Now().Add(-12 * time.Hour),
CreatedAt: utils.Now().Add(-12 * time.Hour),
}
var TestUser = &types.User{
@ -73,8 +74,8 @@ func init() {
}
func injectDatabase() {
utils.DeleteFile(dir + "/statup.db")
db, err := gorm.Open("sqlite3", dir+"/statup.db")
utils.DeleteFile(dir + "/notifiers.db")
db, err := gorm.Open("sqlite3", dir+"/notifiers.db")
if err != nil {
panic(err)
}

View File

@ -29,23 +29,16 @@ import (
const (
slackMethod = "slack"
failingTemplate = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "Your Statping service <{{.Service.Domain}}|{{.Service.Name}}> has just received a Failure notification based on your expected results. {{.Service.Name}} responded with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected Status Code", "value": "{{.Service.ExpectedStatus}}", "short": true }, { "title": "Received Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#FF0000", "thumb_url": "https://statping.com", "footer": "Statping", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }`
failingTemplate = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "Your Statping service <{{.Service.Domain}}|{{.Service.Name}}> has just received a Failure notification based on your expected results. {{.Service.Name}} responded with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected Status Code", "value": "{{.Service.ExpectedStatus}}", "short": true }, { "title": "Received Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ,{ "title": "Error Message", "value": "{{.Issue}}", "short": false } ], "color": "#FF0000", "thumb_url": "https://statping.com", "footer": "Statping", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }`
successTemplate = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "Your Statping service <{{.Service.Domain}}|{{.Service.Name}}> is now back online and meets your expected responses.", "color": "#00FF00", "thumb_url": "https://statping.com", "footer": "Statping", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }`
slackText = `{"text":"{{.}}"}`
)
func init() {
err := notifier.AddNotifier(slacker)
if err != nil {
panic(err)
}
}
type slack struct {
*notifier.Notification
}
var slacker = &slack{&notifier.Notification{
var Slacker = &slack{&notifier.Notification{
Method: slackMethod,
Title: "slack",
Description: "Send notifications to your slack channel when a service is offline. Insert your Incoming webhooker URL for your channel to receive notifications. Based on the <a href=\"https://api.slack.com/incoming-webhooks\">slack API</a>.",
@ -57,7 +50,7 @@ var slacker = &slack{&notifier.Notification{
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Incoming webhooker Url",
Placeholder: "Insert your slack webhook URL here.",
Placeholder: "Insert your slack Webhook URL here.",
SmallText: "Incoming webhooker URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">slack Apps</a>",
DbField: "Host",
Required: true,
@ -71,7 +64,7 @@ func parseSlackMessage(id int64, temp string, data interface{}) error {
if err != nil {
return err
}
slacker.AddQueue(fmt.Sprintf("service_%v", id), buf.String())
Slacker.AddQueue(fmt.Sprintf("service_%v", id), buf.String())
return nil
}
@ -79,12 +72,13 @@ type slackMessage struct {
Service *types.Service
Template string
Time int64
Issue string
}
// Send will send a HTTP Post to the slack webhooker API. It accepts type: string
func (u *slack) Send(msg interface{}) error {
message := msg.(string)
_, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second))
_, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second), true)
return err
}
@ -93,7 +87,7 @@ func (u *slack) Select() *notifier.Notification {
}
func (u *slack) OnTest() error {
contents, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second))
contents, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second), true)
if string(contents) != "ok" {
return errors.New("The slack response was incorrect, check the URL")
}
@ -105,24 +99,23 @@ func (u *slack) OnFailure(s *types.Service, f *types.Failure) {
message := slackMessage{
Service: s,
Template: failingTemplate,
Time: time.Now().Unix(),
Time: utils.Now().Unix(),
Issue: f.Issue,
}
parseSlackMessage(s.Id, failingTemplate, message)
u.Online = false
}
// OnSuccess will trigger successful service
func (u *slack) OnSuccess(s *types.Service) {
if !u.Online {
if !s.Online {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
message := slackMessage{
Service: s,
Template: successTemplate,
Time: time.Now().Unix(),
Time: utils.Now().Unix(),
}
parseSlackMessage(s.Id, successTemplate, message)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved

View File

@ -30,13 +30,13 @@ var (
func init() {
SLACK_URL = os.Getenv("SLACK_URL")
slacker.Host = SLACK_URL
Slacker.Host = SLACK_URL
}
func TestSlackNotifier(t *testing.T) {
t.Parallel()
SLACK_URL = os.Getenv("SLACK_URL")
slacker.Host = SLACK_URL
Slacker.Host = SLACK_URL
if SLACK_URL == "" {
t.Log("slack notifier testing skipped, missing SLACK_URL environment variable")
t.SkipNow()
@ -44,92 +44,72 @@ func TestSlackNotifier(t *testing.T) {
currentCount = CountNotifiers()
t.Run("Load slack", func(t *testing.T) {
slacker.Host = SLACK_URL
slacker.Delay = time.Duration(100 * time.Millisecond)
slacker.Limits = 3
err := notifier.AddNotifier(slacker)
Slacker.Host = SLACK_URL
Slacker.Delay = time.Duration(100 * time.Millisecond)
Slacker.Limits = 3
err := notifier.AddNotifiers(Slacker)
assert.Nil(t, err)
assert.Equal(t, "Hunter Long", slacker.Author)
assert.Equal(t, SLACK_URL, slacker.Host)
})
t.Run("Load slack Notifier", func(t *testing.T) {
notifier.Load()
assert.Equal(t, "Hunter Long", Slacker.Author)
assert.Equal(t, SLACK_URL, Slacker.Host)
})
t.Run("slack Notifier Tester", func(t *testing.T) {
assert.True(t, slacker.CanTest())
assert.True(t, Slacker.CanTest())
})
//t.Run("slack parse message", func(t *testing.T) {
// err := parseSlackMessage(slackText, "this is a test!")
// assert.Nil(t, err)
// assert.Equal(t, 1, len(slacker.Queue))
// assert.Equal(t, 1, len(Slacker.Queue))
//})
t.Run("slack Within Limits", func(t *testing.T) {
ok, err := slacker.WithinLimits()
ok, err := Slacker.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("slack OnFailure", func(t *testing.T) {
slacker.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(slacker.Queue))
})
t.Run("slack OnFailure multiple times", func(t *testing.T) {
for i := 0; i <= 50; i++ {
slacker.OnFailure(TestService, TestFailure)
}
assert.Equal(t, 52, len(slacker.Queue))
})
t.Run("slack Check Offline", func(t *testing.T) {
assert.False(t, slacker.Online)
Slacker.OnFailure(TestService, TestFailure)
assert.Equal(t, 1, len(Slacker.Queue))
})
t.Run("slack OnSuccess", func(t *testing.T) {
slacker.OnSuccess(TestService)
assert.Equal(t, 1, len(slacker.Queue))
})
t.Run("slack Queue after being online", func(t *testing.T) {
assert.True(t, slacker.Online)
assert.Equal(t, 1, len(slacker.Queue))
Slacker.OnSuccess(TestService)
assert.Equal(t, 1, len(Slacker.Queue))
})
t.Run("slack OnSuccess Again", func(t *testing.T) {
assert.True(t, slacker.Online)
slacker.OnSuccess(TestService)
assert.Equal(t, 1, len(slacker.Queue))
go notifier.Queue(slacker)
time.Sleep(6 * time.Second)
assert.Equal(t, 0, len(slacker.Queue))
assert.True(t, TestService.Online)
Slacker.OnSuccess(TestService)
assert.Equal(t, 1, len(Slacker.Queue))
go notifier.Queue(Slacker)
time.Sleep(15 * time.Second)
assert.Equal(t, 0, len(Slacker.Queue))
})
t.Run("slack Within Limits again", func(t *testing.T) {
ok, err := slacker.WithinLimits()
ok, err := Slacker.WithinLimits()
assert.Nil(t, err)
assert.True(t, ok)
})
t.Run("slack Send", func(t *testing.T) {
err := slacker.Send(slackTestMessage)
err := Slacker.Send(slackTestMessage)
assert.Nil(t, err)
assert.Equal(t, 0, len(slacker.Queue))
assert.Equal(t, 0, len(Slacker.Queue))
})
t.Run("slack Test", func(t *testing.T) {
err := slacker.OnTest()
err := Slacker.OnTest()
assert.Nil(t, err)
})
t.Run("slack Queue", func(t *testing.T) {
go notifier.Queue(slacker)
time.Sleep(5 * time.Second)
assert.Equal(t, SLACK_URL, slacker.Host)
assert.Equal(t, 0, len(slacker.Queue))
go notifier.Queue(Slacker)
time.Sleep(10 * time.Second)
assert.Equal(t, SLACK_URL, Slacker.Host)
assert.Equal(t, 0, len(Slacker.Queue))
})
}

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