Merge branch 'master' into patch-1

pull/143/head
Hunter Long 2019-10-15 08:58:09 -07:00 committed by GitHub
commit ef0d9594b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 10159 additions and 825 deletions

View File

@ -2,10 +2,7 @@ os:
- linux
language: go
go:
- "1.11.1"
go: 1.13
go_import_path: github.com/hunterlong/statping
cache:

View File

@ -1,27 +1,28 @@
FROM golang:1.11-alpine as base
MAINTAINER "Hunter Long (https://github.com/hunterlong)"
FROM golang:1.12-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 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 . /go/src/github.com/hunterlong/statping
ADD Makefile Gopkg.* /go/src/github.com/hunterlong/statping/
RUN make dep && \
make dev-deps && \
make install
make dev-deps
ADD . /go/src/github.com/hunterlong/statping
RUN make install
# Statping :latest Docker Image
FROM alpine:latest
MAINTAINER "Hunter Long (https://github.com/hunterlong)"
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
ENV IS_DOCKER=true
ENV STATPING_DIR=/app
ENV PORT=8080
RUN apk --no-cache add curl jq
RUN apk --no-cache add curl jq libsass
COPY --from=base /usr/local/bin/sass /usr/local/bin/sass
COPY --from=base /go/bin/statping /usr/local/bin/statping

175
Gopkg.lock generated
View File

@ -2,15 +2,36 @@
[[projects]]
branch = "master"
digest = "1:6212ce7f839b40213d1ff75c3df7816c4023faf4d29da630ce1dfdd1ccd5fb02"
digest = "1:d4bfd57449b0bdfe927ec45c8463afd8f5b6012d4bcd5a9da8581a408c23e57c"
name = "github.com/99designs/gqlgen"
packages = [
"complexity",
"graphql",
"graphql/introspection",
"handler",
]
pruneopts = "UT"
revision = "a7bc468ca1b184a5ce1b07ea331e0121fc56ae82"
version = "v0.9.3"
[[projects]]
digest = "1:07f7314344b2771963ada0b2a4a426c59d782dac227dcfff2499188a186446c0"
name = "github.com/GeertJohan/go.rice"
packages = [
".",
"embedded",
]
pruneopts = "UT"
revision = "0af3f3b09a0a8b391f63ab52ba5ab50f84fabd30"
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"
@ -21,12 +42,12 @@
version = "0.0.1"
[[projects]]
branch = "master"
digest = "1:5fd5c4d4282935b7a575299494f2c09e9d2cacded7815c83aff7c1602aff3154"
digest = "1:8e8da6cc8cca12851d4e089d970a0f7387b3a6bcc8c459ff432213b03076a66d"
name = "github.com/daaku/go.zipexe"
packages = ["."]
pruneopts = "UT"
revision = "a5fe2436ffcb3236e175e5149162b41cd28bd27d"
revision = "74d766ac1dde7458348221869a7d1e7e5fa0597e"
version = "v1.0.1"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
@ -61,20 +82,12 @@
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:ca59b1175189b3f0e9f1793d2c350114be36eaabbe5b9f554b35edee1de50aea"
digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "a7962380ca08b5a188038c69871b8d3fbdf31e89"
version = "v1.7.0"
revision = "00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15"
version = "v1.7.3"
[[projects]]
digest = "1:e72d1ebb8d395cf9f346fd9cbc652e5ae222dd85e0ac842dc57f175abed6d195"
@ -85,15 +98,34 @@
version = "v1.1.1"
[[projects]]
digest = "1:e5bf52fd66a2e984b57b4c0f2c4ee024ed749a19886246240629998dc0cf31ce"
digest = "1:172c862eabc72e90f461bcef223c49869628bec6d989386dfb03281ae3222148"
name = "github.com/gorilla/sessions"
packages = ["."]
pruneopts = "UT"
revision = "f57b7e2d29c6211d16ffa52a0998272f75799030"
version = "v1.1.3"
revision = "4355a998706e83fe1d71c31b07af94e34f68d74a"
version = "v1.2.0"
[[projects]]
digest = "1:ff312c4d510c67954a6fc6a11c9ff72a2b2169584776b7419c7b8c729e2b13ac"
digest = "1:e62657cca9badaa308d86e7716083e4c5933bb78e30a17743fc67f50be26f6f4"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "c3e18be99d19e6b3e8f1559eea2c161a665c4b6b"
version = "v1.4.1"
[[projects]]
digest = "1:c77361e611524ec8f2ad37c408c3c916111a70b6acf806a1200855696bf8fa4d"
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru",
]
pruneopts = "UT"
revision = "7f827b33c0f158ec5dfbba01bb0b14a4541fd81d"
version = "v0.5.3"
[[projects]]
digest = "1:b0c1770be8c52cf00117b98049de1e4df91c8df588102198364b09669bb60178"
name = "github.com/jinzhu/gorm"
packages = [
".",
@ -102,16 +134,16 @@
"dialects/sqlite",
]
pruneopts = "UT"
revision = "472c70caa40267cb89fd8facb07fe6454b578626"
version = "v1.9.2"
revision = "836fb2c19d84dac7b0272958dfb9af7cf0d0ade4"
version = "v1.9.10"
[[projects]]
branch = "master"
digest = "1:fd97437fbb6b7dce04132cf06775bd258cce305c44add58eb55ca86c6c325160"
digest = "1:01ed62f8f4f574d8aff1d88caee113700a2b44c42351943fa73cc1808f736a50"
name = "github.com/jinzhu/inflection"
packages = ["."]
pruneopts = "UT"
revision = "04140366298a54a039076d798123ffa108fff46c"
revision = "f5c5f50e6090ae76a29240b61ae2a90dd810112e"
version = "v1.0.0"
[[projects]]
digest = "1:ecd9aa82687cf31d1585d4ac61d0ba180e42e8a6182b85bd785fcca8dfeefc1b"
@ -122,24 +154,25 @@
version = "v1.3.0"
[[projects]]
digest = "1:b18ffc558326ebaed3b4a175617f1e12ed4e3f53d6ebfe5ba372a3de16d22278"
digest = "1:0ead8e64fe356bd9221605e3ec40b4438509868018cbbbaaaff3ebae1b69b78b"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid",
"scram",
]
pruneopts = "UT"
revision = "4ded0e9383f75c197b3a2aaa6d590ac52df6fd79"
version = "v1.0.0"
revision = "3427c32cb71afc948325f299f040e53c1dd78979"
version = "v1.2.0"
[[projects]]
digest = "1:4a49346ca45376a2bba679ca0e83bec949d780d4e927931317904bad482943ec"
digest = "1:79e87abf06b873987dee86598950f5b51732ac454d5a5cab6445a14330e6c9e3"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = "UT"
revision = "c7c4067b79cc51e6dfdcef5c702e74b1e0fa7c75"
version = "v1.10.0"
revision = "b612a2feea6aa87c6d052d9086572551df06497e"
version = "v1.11.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
@ -166,31 +199,81 @@
version = "v1.0.0"
[[projects]]
digest = "1:972c2427413d41a1e06ca4897e8528e5a1622894050e2f527b38ddf0f343f759"
digest = "1:8548c309c65a85933a625be5e7d52b6ac927ca30c56869fae58123b8a77a75e1"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0"
revision = "221dbe5ed46703ee255b1da0dec05086f5035f62"
version = "v1.4.0"
[[projects]]
branch = "master"
digest = "1:1ecf2a49df33be51e757d0033d5d51d5f784f35f68e5a38f797b2d3f03357d71"
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 = "b8fe1690c61389d7d2a8074a507d1d40c5d30448"
revision = "9756ffdc24725223350eb3266ffb92590d28f278"
[[projects]]
branch = "master"
digest = "1:caffb9a4f8c756941de4b3eb577abd167e7fd4b570f2078c05ceb8835a1514cb"
name = "golang.org/x/net"
packages = [
"bpf",
"icmp",
"internal/iana",
"internal/socket",
"ipv4",
"ipv6",
]
pruneopts = "UT"
revision = "ba9fcec4b297b415637633c5a6e8fa592e4a16c3"
[[projects]]
branch = "master"
digest = "1:d94059c196c160bd1c4030d49ffaa39a456be516501e5916bea663f5d79a75ec"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "9109b7679e13aa34a54834cfb4949cac4b96e576"
[[projects]]
digest = "1:c25289f43ac4a68d88b02245742347c94f1e108c534dda442188015ff80669b3"
name = "google.golang.org/appengine"
packages = ["cloudsql"]
pruneopts = "UT"
revision = "e9657d882bb81064595ca3b56cbe2546bbabf7b1"
version = "v1.4.0"
revision = "5f2a59506353b8d5ba8cbbcd9f3c1f41f1eaf079"
version = "v1.6.2"
[[projects]]
branch = "v3"
@ -216,10 +299,21 @@
revision = "d3b5b032dc8e8927d31a5071b56e14c89f045135"
version = "v2.0.1"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[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",
@ -234,6 +328,9 @@
"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",

View File

@ -26,8 +26,12 @@
[[constraint]]
branch = "master"
name = "github.com/99designs/gqlgen"
version = "0.9.3"
[[constraint]]
name = "github.com/GeertJohan/go.rice"
version = "1.0.0"
[[constraint]]
name = "github.com/ararog/timeago"
@ -43,15 +47,15 @@
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.7.0"
version = "1.7.3"
[[constraint]]
name = "github.com/gorilla/sessions"
version = "1.1.3"
version = "1.2.0"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.2"
version = "1.9.10"
[[constraint]]
name = "github.com/joho/godotenv"
@ -63,7 +67,15 @@
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.3.0"
version = "1.4.0"
[[constraint]]
branch = "master"
name = "github.com/tatsushid/go-fastping"
[[constraint]]
name = "github.com/vektah/gqlparser"
version = "1.1.2"
[[constraint]]
branch = "master"

View File

@ -3,16 +3,17 @@ SIGN_KEY=B76D61FAA6DB759466E83D9964B9C6AAE2D55278
BINARY_NAME=statping
GOPATH:=$(GOPATH)
GOCMD=go
GOBUILD=$(GOCMD) build
GOBUILD=$(GOCMD) build -a
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOVERSION=1.12.x
GOINSTALL=$(GOCMD) install
XGO=GOPATH=$(GOPATH) xgo -go 1.11 --dest=build
XGO=GOPATH=$(GOPATH) 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)
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": [ "1.10.x" ], "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}" }, "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" ] } } }'
TEST_DIR=$(GOPATH)/src/github.com/hunterlong/statping
PATH:=$(PATH)
@ -90,7 +91,7 @@ test: clean compile install build-plugin
test-api:
DB_CONN=sqlite DB_HOST=localhost DB_DATABASE=sqlite DB_PASS=none DB_USER=none statping &
sleep 15 && newman run source/tmpl/postman.json -e dev/postman_environment.json --delay-request 500
sleep 300 && newman run source/tmpl/postman.json -e dev/postman_environment.json --delay-request 500
# report coverage to Coveralls
coverage:
@ -156,7 +157,7 @@ docker-build-dev:
# build Cypress UI testing :cypress docker tag
docker-build-cypress: clean
GOPATH=$(GOPATH) xgo -out statping -go 1.10.x -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" --targets=linux/amd64 ./cmd
GOPATH=$(GOPATH) xgo -out statping -go $(GOVERSION) -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" --targets=linux/amd64 ./cmd
docker build -t hunterlong/statping:cypress -f dev/Dockerfile-cypress .
rm -f statping
@ -231,6 +232,7 @@ dev-deps:
$(GOGET) github.com/ararog/timeago
$(GOGET) gopkg.in/natefinch/lumberjack.v2
$(GOGET) golang.org/x/crypto/bcrypt
$(GOGET) github.com/99designs/gqlgen/...
# remove files for a clean compile/build
clean:
@ -260,6 +262,7 @@ tag:
generate:
cd source && go generate
cd handlers/graphql && go generate
# compress built binaries into tar.gz and zip formats
compress:

View File

@ -163,3 +163,10 @@ Statping accepts Push Requests! Feel free to add your own features and notifiers
[![Go Report Card](https://goreportcard.com/badge/github.com/hunterlong/statping)](https://goreportcard.com/report/github.com/hunterlong/statping)
[![Build Status](https://travis-ci.com/hunterlong/statping.svg?branch=master)](https://travis-ci.com/hunterlong/statping) [![Cypress.io tests](https://img.shields.io/badge/cypress.io-tests-green.svg?style=flat-square)](https://dashboard.cypress.io/#/projects/bi8mhr/runs)
[![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/) [![Godoc](https://godoc.org/github.com/hunterlong/statping?status.svg)](https://godoc.org/github.com/hunterlong/statping)[![Coverage Status](https://coveralls.io/repos/github/hunterlong/statping/badge.svg?branch=master)](https://coveralls.io/github/hunterlong/statping?branch=master)
<p align="center">
<a href="https://www.buymeacoffee.com/hunterlong" target="_blank">
<img height="55" src="https://img.cjx.io/buy-me-redbull.png" >
</a>
</p>

View File

@ -218,6 +218,7 @@ func HelpEcho() {
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(" 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)")
fmt.Println(" DB_HOST - Database hostname or IP address")
fmt.Println(" DB_USER - Database username")
@ -240,7 +241,7 @@ func HelpEcho() {
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(10*time.Second), true)
if err != nil {
return githubResponse{}, err
}

View File

@ -33,13 +33,15 @@ var (
func init() {
dir = utils.Directory
core.SampleHits = 480
}
func TestStartServerCommand(t *testing.T) {
t.SkipNow()
os.Setenv("DB_CONN", "sqlite")
cmd := helperCommand(nil, "")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(8*time.Second), got)
commandAndSleep(cmd, time.Duration(60*time.Second), got)
os.Unsetenv("DB_CONN")
gg, _ := <-got
assert.Contains(t, gg, "DB_CONN environment variable was found")
@ -61,6 +63,7 @@ func TestHelpCommand(t *testing.T) {
}
func TestStaticCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "static")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(10*time.Second), got)
@ -72,6 +75,7 @@ func TestStaticCommand(t *testing.T) {
}
func TestExportCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "export")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(10*time.Second), got)
@ -99,6 +103,7 @@ func TestAssetsCommand(t *testing.T) {
}
func TestRunCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "run")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
@ -120,8 +125,8 @@ func TestVersionCLI(t *testing.T) {
}
func TestAssetsCLI(t *testing.T) {
run := catchCLI([]string{"assets"})
assert.EqualError(t, run, "end")
catchCLI([]string{"assets"})
//assert.EqualError(t, run, "end")
assert.FileExists(t, dir+"/assets/css/base.css")
assert.FileExists(t, dir+"/assets/scss/base.scss")
}
@ -153,6 +158,7 @@ func TestHelpCLI(t *testing.T) {
}
func TestRunOnceCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"run"})
assert.EqualError(t, run, "end")
}

View File

@ -65,7 +65,10 @@ func main() {
parseFlags()
loadDotEnvs()
source.Assets()
utils.InitLogs()
if err := utils.InitLogs(); err != nil {
fmt.Printf("Statping Log Error: \n %v\n", err)
os.Exit(2)
}
args := flag.Args()
if len(args) >= 1 {

View File

@ -21,6 +21,7 @@ import (
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/tatsushid/go-fastping"
"net"
"net/http"
"net/url"
@ -38,6 +39,19 @@ 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":
s.checkHttp(record)
case "tcp", "udp":
s.checkTcp(record)
case "icmp":
s.checkIcmp(record)
}
}
// CheckQueue is the main go routine for checking a service
func (s *Service) CheckQueue(record bool) {
s.Checkpoint = time.Now()
@ -77,17 +91,11 @@ func (s *Service) parseHost() string {
if s.Type == "tcp" || s.Type == "udp" {
return s.Domain
} else {
domain := s.Domain
hasPort, _ := regexp.MatchString(`\:([0-9]+)`, domain)
if hasPort {
splitDomain := strings.Split(s.Domain, ":")
domain = splitDomain[len(splitDomain)-2]
}
host, err := url.Parse(domain)
u, err := url.Parse(s.Domain)
if err != nil {
return s.Domain
}
return host.Host
return strings.Split(u.Host, ":")[0]
}
}
@ -109,6 +117,36 @@ func (s *Service) dnsCheck() (float64, error) {
return subTime, err
}
func isIPv6(address string) bool {
return strings.Count(address, ":") >= 2
}
// checkIcmp will send a ICMP ping packet to the service
func (s *Service) checkIcmp(record bool) *Service {
p := fastping.NewPinger()
resolveIP := "ip4:icmp"
if isIPv6(s.Domain) {
resolveIP = "ip6:icmp"
}
ra, err := net.ResolveIPAddr(resolveIP, s.Domain)
if err != nil {
recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
return s
}
p.AddIPAddr(ra)
p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) {
s.Latency = rtt.Seconds()
recordSuccess(s)
}
err = p.Run()
if err != nil {
recordFailure(s, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err))
return s
}
s.LastResponse = ""
return s
}
// checkTcp will check a TCP service
func (s *Service) checkTcp(record bool) *Service {
dnsLookup, err := s.dnsCheck()
@ -123,17 +161,20 @@ func (s *Service) checkTcp(record bool) *Service {
domain := fmt.Sprintf("%v", s.Domain)
if s.Port != 0 {
domain = fmt.Sprintf("%v:%v", s.Domain, s.Port)
if isIPv6(s.Domain) {
domain = fmt.Sprintf("[%v]:%v", s.Domain, s.Port)
}
}
conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second)
if err != nil {
if record {
recordFailure(s, fmt.Sprintf("%v Dial Error %v", s.Type, err))
recordFailure(s, fmt.Sprintf("Dial Error %v", err))
}
return s
}
if err := conn.Close(); err != nil {
if record {
recordFailure(s, fmt.Sprintf("TCP Socket Close Error %v", err))
recordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err))
}
return s
}
@ -161,10 +202,18 @@ func (s *Service) checkHttp(record bool) *Service {
timeout := time.Duration(s.Timeout) * time.Second
var content []byte
var res *http.Response
if s.Method == "POST" {
content, res, err = utils.HttpRequest(s.Domain, s.Method, "application/json", nil, bytes.NewBuffer([]byte(s.PostData.String)), timeout)
var headers []string
if s.Headers.Valid {
headers = strings.Split(s.Headers.String, ",")
} else {
content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, nil, nil, timeout)
headers = nil
}
if s.Method == "POST" {
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, s.VerifySSL.Bool)
}
if err != nil {
if record {
@ -174,22 +223,13 @@ func (s *Service) checkHttp(record bool) *Service {
}
t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds()
if err != nil {
if record {
recordFailure(s, fmt.Sprintf("HTTP Error %v", err))
}
return s
}
s.LastResponse = string(content)
s.LastStatusCode = res.StatusCode
if s.Expected.String != "" {
if err != nil {
utils.Log(2, err)
}
match, err := regexp.MatchString(s.Expected.String, string(content))
if err != nil {
utils.Log(2, err)
utils.Log(2, fmt.Sprintf("Service %v expected: %v to match %v", s.Name, string(content), s.Expected.String))
}
if !match {
if record {
@ -204,26 +244,14 @@ func (s *Service) checkHttp(record bool) *Service {
}
return s
}
s.Online = true
if record {
recordSuccess(s)
}
return s
}
// Check will run checkHttp for HTTP services and checkTcp for TCP services
func (s *Service) Check(record bool) {
switch s.Type {
case "http":
s.checkHttp(record)
case "tcp", "udp":
s.checkTcp(record)
}
}
// 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,
@ -234,11 +262,12 @@ func recordSuccess(s *Service) {
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)
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{
Service: s.Id,
Issue: issue,
@ -248,5 +277,9 @@ func recordFailure(s *Service, issue string) {
}}
utils.Log(2, fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
s.CreateFailure(fail)
s.Online = false
s.SuccessNotified = false
s.UpdateNotify = CoreApp.UpdateNotify.Bool
s.DownText = s.DowntimeText()
notifier.OnFailure(s.Service, fail.Failure)
}

View File

@ -80,9 +80,18 @@ func LoadUsingEnv() (*DbConfig, error) {
utils.Log(3, err)
}
username := os.Getenv("ADMIN_USER")
if username == "" {
username = "admin"
}
password := os.Getenv("ADMIN_PASSWORD")
if password == "" {
password = "admin"
}
admin := ReturnUser(&types.User{
Username: "admin",
Password: "admin",
Username: username,
Password: password,
Email: "info@admin.com",
Admin: types.NewNullBool(true),
})

View File

@ -33,6 +33,7 @@ func init() {
utils.InitLogs()
source.Assets()
skipNewDb = false
SampleHits = 480
}
func TestNewCore(t *testing.T) {

View File

@ -36,7 +36,7 @@ var (
)
func init() {
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}}
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}}
}
// DbConfig stores the config.yml file for the statup configuration
@ -87,12 +87,21 @@ func groupsDb() *gorm.DB {
return DbSession.Model(&types.Group{})
}
// incidentsDB returns the 'incidents' database column
func incidentsDB() *gorm.DB {
return DbSession.Model(&types.Incident{})
}
// incidentsUpdatesDB returns the 'incidents updates' database column
func incidentsUpdatesDB() *gorm.DB {
return DbSession.Model(&types.IncidentUpdate{})
}
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
selector := Dbtimestamp(group, column)
if CoreApp.DbConnection == "postgres" {
timeQuery := fmt.Sprintf("service = %v AND created_at BETWEEN '%v.000000' AND '%v.000000'", s.Id, t1.UTC().Format(types.POSTGRES_TIME), t2.UTC().Format(types.POSTGRES_TIME))
return hitsDB().Select(selector).Where(timeQuery)
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))
}
@ -194,7 +203,7 @@ func (db *DbConfig) Connect(retry bool, location string) error {
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", Configs.DbUser, Configs.DbPass, host, Configs.DbData)
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)
case "postgres":
sslMode := "disable"
if postgresSSL != "" {
@ -208,16 +217,19 @@ func (db *DbConfig) Connect(retry bool, location string) error {
dbSession, err := gorm.Open(dbType, conn)
if err != nil {
if retry {
utils.Log(1, fmt.Sprintf("Database connection to '%v' is not available, trying again in 5 seconds...", conn))
utils.Log(1, fmt.Sprintf("Database connection to '%v' is not available, trying again in 5 seconds...", Configs.DbHost))
return db.waitForDb()
} else {
return err
}
}
if dbType == "sqlite3" {
dbSession.DB().SetMaxOpenConns(1)
}
err = dbSession.DB().Ping()
if err == 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))
utils.Log(1, fmt.Sprintf("Database %v connection was successful.", dbType))
}
return err
}
@ -320,6 +332,8 @@ func (db *DbConfig) DropDatabase() error {
err = DbSession.DropTableIfExists("services")
err = DbSession.DropTableIfExists("users")
err = DbSession.DropTableIfExists("messages")
err = DbSession.DropTableIfExists("incidents")
err = DbSession.DropTableIfExists("incident_updates")
return err.Error
}

View File

@ -129,7 +129,7 @@ func CountFailures() uint64 {
func (s *Service) TotalFailuresOnDate(ago time.Time) (uint64, error) {
var count uint64
date := ago.UTC().Format("2006-01-02 00:00:00")
dateend := ago.UTC().Format("2006-01-02 23:59:59")
dateend := ago.UTC().Format("2006-01-02") + " 23:59:59"
rows := failuresDB().Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, date, dateend).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error

View File

@ -45,15 +45,26 @@ func (g *Group) Services() []*Service {
return services
}
// VisibleServices returns all services based on authentication
func (g *Group) VisibleServices(auth bool) []*Service {
var services []*Service
for _, g := range g.Services() {
if !g.Public.Bool {
if auth {
services = append(services, g)
}
} else {
services = append(services, g)
}
}
return services
}
// SelectGroups returns all groups
func SelectGroups(includeAll bool, auth bool) []*Group {
var groups []*Group
var validGroups []*Group
groupsDb().Find(&groups).Order("order_id desc")
if includeAll {
emptyGroup := &Group{&types.Group{Id: 0, Public: types.NewNullBool(true)}}
groups = append(groups, emptyGroup)
}
for _, g := range groups {
if !g.Public.Bool {
if auth {
@ -64,6 +75,10 @@ func SelectGroups(includeAll bool, auth bool) []*Group {
}
}
sort.Sort(GroupOrder(validGroups))
if includeAll {
emptyGroup := &Group{&types.Group{Id: 0, Public: types.NewNullBool(true)}}
validGroups = append(validGroups, emptyGroup)
}
return validGroups
}

73
core/incidents.go Normal file
View File

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

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,23 @@ 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()
utils.Log(1, fmt.Sprintf("Sending failure %v notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnFailure(s, f)
}
}
@ -46,8 +65,16 @@ 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()
utils.Log(1, fmt.Sprintf("Sending successful %v notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnSuccess(s)
}
}
@ -57,6 +84,7 @@ func OnSuccess(s *types.Service) {
func OnNewService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending new service notification for service %v", s.Name))
comm.(ServiceEvents).OnNewService(s)
}
}
@ -69,6 +97,7 @@ func OnUpdatedService(s *types.Service) {
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending updated service notification for service %v", s.Name))
comm.(ServiceEvents).OnUpdatedService(s)
}
}
@ -81,6 +110,7 @@ func OnDeletedService(s *types.Service) {
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending deleted service notification for service %v", s.Name))
comm.(ServiceEvents).OnDeletedService(s)
}
}
@ -90,6 +120,7 @@ func OnDeletedService(s *types.Service) {
func OnNewUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending new user notification for user %v", u.Username))
comm.(UserEvents).OnNewUser(u)
}
}
@ -99,6 +130,7 @@ func OnNewUser(u *types.User) {
func OnUpdatedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending updated user notification for user %v", u.Username))
comm.(UserEvents).OnUpdatedUser(u)
}
}
@ -108,6 +140,7 @@ func OnUpdatedUser(u *types.User) {
func OnDeletedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending deleted user notification for user %v", u.Username))
comm.(UserEvents).OnDeletedUser(u)
}
}
@ -117,6 +150,7 @@ func OnDeletedUser(u *types.User) {
func OnUpdatedCore(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending updated core notification"))
comm.(CoreEvents).OnUpdatedCore(c)
}
}
@ -144,6 +178,7 @@ func OnNewNotifier(n *Notification) {
func OnUpdatedNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
utils.Log(1, fmt.Sprintf("Sending updated notifier for %v", n.Id))
comm.(NotifierEvents).OnUpdatedNotifier(n)
}
}

View File

@ -101,20 +101,20 @@ func (n *ExampleNotifier) Select() *Notification {
// OnSave is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnSave() error {
msg := fmt.Sprintf("received on save trigger")
n.AddQueue(0, msg)
n.AddQueue("onsave", msg)
return errors.New("onsave triggered")
}
// OnSuccess is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnSuccess(s *types.Service) {
msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnFailure is a required basic event for the Notifier interface
func (n *ExampleNotifier) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnTest is a option testing event for the Notifier interface
@ -126,61 +126,61 @@ func (n *ExampleNotifier) OnTest() error {
// OnNewService is a option event for new services
func (n *ExampleNotifier) OnNewService(s *types.Service) {
msg := fmt.Sprintf("received a new service trigger for service: %v\n", s.Name)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedService is a option event for updated services
func (n *ExampleNotifier) OnUpdatedService(s *types.Service) {
msg := fmt.Sprintf("received a update service trigger for service: %v\n", s.Name)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnDeletedService is a option event for deleted services
func (n *ExampleNotifier) OnDeletedService(s *types.Service) {
msg := fmt.Sprintf("received a delete service trigger for service: %v\n", s.Name)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnNewUser is a option event for new users
func (n *ExampleNotifier) OnNewUser(s *types.User) {
msg := fmt.Sprintf("received a new user trigger for user: %v\n", s.Username)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedUser is a option event for updated users
func (n *ExampleNotifier) OnUpdatedUser(s *types.User) {
msg := fmt.Sprintf("received a updated user trigger for user: %v\n", s.Username)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnDeletedUser is a option event for deleted users
func (n *ExampleNotifier) OnDeletedUser(s *types.User) {
msg := fmt.Sprintf("received a deleted user trigger for user: %v\n", s.Username)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnUpdatedCore is a option event when the settings are updated
func (n *ExampleNotifier) OnUpdatedCore(s *types.Core) {
msg := fmt.Sprintf("received a updated core trigger for core: %v\n", s.Name)
n.AddQueue(0, msg)
n.AddQueue("core", msg)
}
// OnStart is triggered when statup has been started
func (n *ExampleNotifier) OnStart(s *types.Core) {
msg := fmt.Sprintf("received a trigger on Statping boot: %v\n", s.Name)
n.AddQueue(0, msg)
n.AddQueue(fmt.Sprintf("core"), msg)
}
// OnNewNotifier is triggered when a new notifier has initialized
func (n *ExampleNotifier) OnNewNotifier(s *Notification) {
msg := fmt.Sprintf("received a new notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("notifier_%v", s.Id), msg)
}
// OnUpdatedNotifier is triggered when a notifier has been updated
func (n *ExampleNotifier) OnUpdatedNotifier(s *Notification) {
msg := fmt.Sprintf("received a update notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(s.Id, msg)
n.AddQueue(fmt.Sprintf("notifier_%v", s.Id), msg)
}
// Create a new notifier that includes a form for the end user to insert their own values
@ -224,7 +224,7 @@ func ExampleAddNotifier() {
// OnSuccess will be triggered everytime a service is online
func ExampleNotification_OnSuccess() {
msg := fmt.Sprintf("this is a successful message as a string passing into AddQueue function")
example.AddQueue(0, msg)
example.AddQueue("example", msg)
fmt.Println(len(example.Queue))
// Output: 1
}
@ -232,13 +232,13 @@ func ExampleNotification_OnSuccess() {
// Add a new message into the queue OnSuccess
func ExampleOnSuccess() {
msg := fmt.Sprintf("received a count trigger for service: %v\n", service.Name)
example.AddQueue(0, msg)
example.AddQueue("example", msg)
}
// Add a new message into the queue OnFailure
func ExampleOnFailure() {
msg := fmt.Sprintf("received a failing service: %v\n", service.Name)
example.AddQueue(0, msg)
example.AddQueue("example", msg)
}
// OnTest allows your notifier to be testable
@ -258,7 +258,7 @@ func ExampleNotification_CanTest() {
// Add any type of interface to the AddQueue function to be ran in the queue
func ExampleNotification_AddQueue() {
msg := fmt.Sprintf("this is a failing message as a string passing into AddQueue function")
example.AddQueue(0, msg)
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

View File

@ -62,13 +62,12 @@ 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"`
}
// QueueData is the struct for the messaging queue with service
type QueueData struct {
Id int64
Id string
Data interface{}
}
@ -100,7 +99,7 @@ func (n *Notification) AfterFind() (err error) {
}
// AddQueue will add any type of interface (json, string, struct, etc) into the Notifiers queue
func (n *Notification) AddQueue(uid int64, msg interface{}) {
func (n *Notification) AddQueue(uid string, msg interface{}) {
data := &QueueData{uid, msg}
n.Queue = append(n.Queue, data)
}
@ -180,6 +179,7 @@ func (n *Notification) makeLog(msg interface{}) {
Time: utils.Timestamp(time.Now()),
Timestamp: time.Now(),
}
utils.Log(1, fmt.Sprintf("Notifier %v has sent a message %v", n.Method, log.Message))
n.logs = append(n.logs, log)
}
@ -431,10 +431,10 @@ func (n *Notification) ResetQueue() {
}
// ResetQueue will clear the notifiers Queue for a service
func (n *Notification) ResetUniqueQueue(id int64) []*QueueData {
func (n *Notification) ResetUniqueQueue(uid string) []*QueueData {
var queue []*QueueData
for _, v := range n.Queue {
if v.Id != id {
if v.Id != uid {
queue = append(queue, v)
}
}

View File

@ -16,6 +16,7 @@
package notifier
import (
"fmt"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
@ -98,15 +99,15 @@ func TestSelectNotification(t *testing.T) {
func TestAddQueue(t *testing.T) {
msg := "this is a test in the queue!"
example.AddQueue(0, msg)
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 1, len(example.Queue))
example.AddQueue(0, msg)
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 2, len(example.Queue))
example.AddQueue(0, msg)
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 3, len(example.Queue))
example.AddQueue(0, msg)
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 4, len(example.Queue))
example.AddQueue(0, msg)
example.AddQueue(fmt.Sprintf("service_%v", 0), msg)
assert.Equal(t, 5, len(example.Queue))
}

View File

@ -19,10 +19,14 @@ import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"math/rand"
"time"
)
var (
sampleStart = time.Now().Add((-24 * 7) * time.Hour).UTC()
SampleHits = 9900.
)
// InsertSampleData will create the example/dummy services for a brand new Statping installation
func InsertSampleData() error {
utils.Log(1, "Inserting Sample Data...")
@ -103,11 +107,45 @@ func InsertSampleData() error {
insertMessages()
insertSampleIncidents()
utils.Log(1, "Sample data has finished importing")
return nil
}
func insertSampleIncidents() error {
incident1 := &Incident{&types.Incident{
Title: "Github Downtime",
Description: "This is an example of a incident for a service.",
ServiceId: 2,
}}
_, err := incident1.Create()
incidentUpdate1 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github's page for Statping seems to be sending a 501 error.",
Type: "Investigating",
}}
_, err = incidentUpdate1.Create()
incidentUpdate2 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Problem is continuing and we are looking at the issues.",
Type: "Update",
}}
_, err = incidentUpdate2.Create()
incidentUpdate3 := &IncidentUpdate{&types.IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
}}
_, err = incidentUpdate3.Create()
return err
}
func insertSampleGroups() error {
group1 := &Group{&types.Group{
Name: "Main Services",
@ -163,26 +201,31 @@ func insertSampleCheckins() error {
// InsertSampleHits will create a couple new hits for the sample services
func InsertSampleHits() error {
since := time.Now().Add((-24 * 7) * time.Hour).UTC()
for i := int64(1); i <= 5; i++ {
service := SelectService(i)
utils.Log(1, fmt.Sprintf("Adding %v sample hit records to service %v", 360, service.Name))
createdAt := since
alpha := float64(1.05)
for hi := int64(1); hi <= 168; hi++ {
alpha += 0.01
rand.Seed(time.Now().UnixNano())
latency := rand.Float64() * alpha
createdAt = createdAt.Add(1 * time.Hour)
for i := int64(1); i <= 5; i++ {
service := SelectService(i)
seed := time.Now().UnixNano()
utils.Log(1, 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,
}
service.CreateHit(hit)
}
}
return nil
}
@ -397,7 +440,7 @@ func InsertLargeSampleData() error {
var dayAgo = time.Now().Add((-24 * 90) * time.Hour)
insertHitRecords(dayAgo, 1450)
insertHitRecords(dayAgo, 5450)
insertFailureRecords(dayAgo, 730)
@ -431,10 +474,9 @@ func insertHitRecords(since time.Time, amount int64) {
service := SelectService(i)
utils.Log(1, 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++ {
rand.Seed(time.Now().UnixNano())
latency := rand.Float64()
latency := p.Noise1D(float64(hi / 10))
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{
Service: service.Id,

View File

@ -55,6 +55,21 @@ func SelectService(id int64) *Service {
return nil
}
func SelectServices(auth bool) []*Service {
var validServices []*Service
for _, sr := range CoreApp.Services {
s := sr.(*Service)
if !s.Public.Bool {
if auth {
validServices = append(validServices, s)
}
} else {
validServices = append(validServices, s)
}
}
return validServices
}
// SelectServiceLink returns a *core.Service from the service permalink
func SelectServiceLink(permalink string) *Service {
for _, s := range Services() {
@ -181,13 +196,13 @@ func (s *Service) lastFailure() *Failure {
// // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string {
last := s.LimitedFailures(1)
hits, _ := s.LimitedHits(1)
//hits, _ := s.LimitedHits(1)
zone := CoreApp.Timezone
if s.Online {
if len(last) == 0 {
return fmt.Sprintf("Online since %v", utils.Timezoner(s.CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
} else {
return fmt.Sprintf("Online, last Failure was %v", utils.Timezoner(hits[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
return fmt.Sprintf("Online, last Failure was %v", utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
}
}
if len(last) > 0 {
@ -378,7 +393,7 @@ func (s *Service) Update(restart bool) error {
if !s.AllowNotifications.Bool {
for _, n := range CoreApp.Notifications {
notif := n.(notifier.Notifier).Select()
notif.ResetUniqueQueue(s.Id)
notif.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
}
}
if restart {

View File

@ -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) {
@ -382,7 +379,7 @@ func TestSelectGroups(t *testing.T) {
groups := SelectGroups(true, false)
assert.Equal(t, int(3), len(groups))
groups = SelectGroups(true, true)
assert.Equal(t, int(4), len(groups))
assert.Equal(t, int(5), len(groups))
}
func TestService_TotalFailures(t *testing.T) {
@ -400,14 +397,15 @@ func TestService_TotalFailures24(t *testing.T) {
}
func TestService_TotalFailuresOnDate(t *testing.T) {
t.SkipNow()
ago := time.Now().UTC()
service := SelectService(8)
failures, err := service.TotalFailuresOnDate(ago)
assert.Nil(t, err)
assert.Equal(t, uint64(0), failures)
assert.Equal(t, uint64(1), failures)
}
func TestCountFailures(t *testing.T) {
failures := CountFailures()
assert.Equal(t, uint64(1463), failures)
assert.NotEqual(t, uint64(0), failures)
}

View File

@ -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, err)
utils.Log(2, fmt.Errorf("user %v not found", username))
return nil, false
}
if CheckHash(password, user.Password) {

View File

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

View File

@ -1,5 +1,5 @@
FROM alpine
MAINTAINER "Hunter Long (https://github.com/hunterlong)"
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ENV STATPING_VERSION=0.80.35

View File

@ -16,7 +16,6 @@
package handlers
import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/core"
@ -36,21 +35,12 @@ type apiResponse struct {
}
func apiIndexHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
coreClone := *core.CoreApp
coreClone.Started = utils.Timezoner(core.CoreApp.Started, core.CoreApp.Timezone)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(coreClone)
returnJson(coreClone, w, r)
}
func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var err error
core.CoreApp.ApiKey = utils.NewSHA1Hash(40)
core.CoreApp.ApiSecret = utils.NewSHA1Hash(40)
@ -63,10 +53,6 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
}
func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
CacheStorage = NewStorage()
http.Redirect(w, r, "/", http.StatusSeeOther)
}
@ -77,8 +63,7 @@ func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
Status: "error",
Error: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(output)
returnJson(output, w, r)
}
func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *http.Request) {
@ -120,6 +105,12 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
case *types.Checkin:
objName = "checkin"
objId = v.Id
case *core.Incident:
objName = "incident"
objId = v.Id
case *core.IncidentUpdate:
objName = "incident_update"
objId = v.Id
default:
objName = "missing"
}
@ -132,8 +123,7 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
Output: obj,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(output)
returnJson(output, w, r)
}
func sendUnauthorizedJson(w http.ResponseWriter, r *http.Request) {
@ -141,7 +131,6 @@ 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)
json.NewEncoder(w).Encode(output)
returnJson(output, w, r)
}

View File

@ -102,6 +102,12 @@ func TestMainApiRoutes(t *testing.T) {
URL: "/api/clear_cache",
Method: "POST",
ExpectedStatus: 303,
},
{
Name: "404 Error Page",
URL: "/api/missing_404_page",
Method: "GET",
ExpectedStatus: 404,
}}
for _, v := range tests {
@ -169,7 +175,7 @@ func TestApiServiceRoutes(t *testing.T) {
},
{
Name: "Statping Reorder Services",
URL: "/api/reorder",
URL: "/api/services/reorder",
Method: "POST",
Body: `[{"service":1,"order":1},{"service":5,"order":2},{"service":2,"order":3},{"service":3,"order":4},{"service":4,"order":5}]`,
ExpectedStatus: 200,

View File

@ -1,9 +1,6 @@
package handlers
import (
"github.com/hunterlong/statping/core"
"net/http"
"net/http/httptest"
"sync"
"time"
)
@ -74,31 +71,3 @@ func (s Storage) Set(key string, content []byte, duration time.Duration) {
Expiration: time.Now().Add(duration).UnixNano(),
}
}
func cached(duration, contentType string, handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
content := CacheStorage.Get(r.RequestURI)
w.Header().Set("Content-Type", contentType)
if core.Configs == nil {
handler(w, r)
return
}
if content != nil {
w.Write(content)
} else {
c := httptest.NewRecorder()
handler(c, r)
content := c.Body.Bytes()
result := c.Result()
if result.StatusCode != 200 {
w.WriteHeader(result.StatusCode)
w.Write(content)
return
}
w.Write(content)
if d, err := time.ParseDuration(duration); err == nil {
go CacheStorage.Set(r.RequestURI, content, d)
}
}
})
}

View File

@ -28,24 +28,15 @@ import (
)
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
checkins := core.AllCheckins()
for _, c := range checkins {
c.Hits = c.AllHits()
c.Failures = c.LimitedFailures(64)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(checkins)
returnJson(checkins, w, r)
}
func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
checkin := core.SelectCheckin(vars["api"])
if checkin == nil {
@ -54,15 +45,10 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
}
checkin.Hits = checkin.LimitedHits(32)
checkin.Failures = checkin.LimitedFailures(32)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(checkin)
returnJson(checkin, w, r)
}
func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
var checkin *core.Checkin
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&checkin)
@ -110,15 +96,10 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
}
checkin.Failing = false
checkin.LastHit = utils.Timezoner(time.Now().UTC(), core.CoreApp.Timezone)
w.Header().Set("Content-Type", "application/json")
sendJsonAction(checkinHit, "update", w, r)
}
func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
checkin := core.SelectCheckin(vars["api"])
if checkin == nil {

View File

@ -41,9 +41,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
resetCookies()
}
session, _ := sessionStore.Get(r, cookieKey)
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
form := parseForm(r)
username := form.Get("username")
password := form.Get("password")
user, auth := core.AuthUser(username, password)
if auth {
session.Values["authenticated"] = true
@ -77,10 +77,6 @@ func helpHandler(w http.ResponseWriter, r *http.Request) {
}
func logsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
utils.LockLines.Lock()
logs := make([]string, 0)
length := len(utils.LastLines)
@ -93,10 +89,6 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
}
func logsLineHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
w.WriteHeader(http.StatusInternalServerError)
return
}
if lastLine := utils.GetLastLine(); lastLine != nil {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
@ -105,11 +97,6 @@ func logsLineHandler(w http.ResponseWriter, r *http.Request) {
}
func exportHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
w.WriteHeader(http.StatusInternalServerError)
return
}
var notifiers []*notifier.Notification
for _, v := range core.CoreApp.Notifications {
notifier := v.(notifier.Notifier)

View File

@ -38,10 +38,21 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
"VisibleServices": func() []*core.Service {
auth := IsUser(r)
return core.SelectServices(auth)
},
"VisibleGroupServices": func(group *core.Group) []*core.Service {
auth := IsUser(r)
return group.VisibleServices(auth)
},
"Groups": func(includeAll bool) []*core.Group {
auth := IsUser(r)
return core.SelectGroups(includeAll, auth)
},
"Group": func(id int) *core.Group {
return core.SelectGroup(int64(id))
},
"len": func(g interface{}) int {
val := reflect.ValueOf(g)
return val.Len()
@ -52,6 +63,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)
},

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
# .gqlgen.yml example
#
# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.
schema:
- schema.graphql
exec:
filename: generated.go
model:
filename: models_gen.go
models:
Core:
model: github.com/hunterlong/statping/types.Core
Message:
model: github.com/hunterlong/statping/types.Message
Group:
model: github.com/hunterlong/statping/types.Group
Service:
model: github.com/hunterlong/statping/types.Service
User:
model: github.com/hunterlong/statping/types.User
Failure:
model: github.com/hunterlong/statping/types.Failure
Checkin:
model: github.com/hunterlong/statping/types.Checkin
CheckinHit:
model: github.com/hunterlong/statping/types.CheckinHit
ID:
model:
- github.com/99designs/gqlgen/graphql.Int64
struct_tag: json
resolver:
filename: resolver.go
type: Resolver

View File

@ -0,0 +1,188 @@
//go:generate go run github.com/99designs/gqlgen
package graphql
import (
"context"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
)
// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
type Resolver struct{}
func (r *Resolver) Checkin() CheckinResolver {
return &checkinResolver{r}
}
func (r *Resolver) Core() CoreResolver {
return &coreResolver{r}
}
func (r *Resolver) Group() GroupResolver {
return &groupResolver{r}
}
func (r *Resolver) Message() MessageResolver {
return &messageResolver{r}
}
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
func (r *Resolver) Service() ServiceResolver {
return &serviceResolver{r}
}
func (r *Resolver) User() UserResolver {
return &userResolver{r}
}
type checkinResolver struct{ *Resolver }
func (r *checkinResolver) Service(ctx context.Context, obj *types.Checkin) (*types.Service, error) {
service := core.SelectService(obj.ServiceId)
return service.Service, nil
}
func (r *checkinResolver) Failures(ctx context.Context, obj *types.Checkin) ([]*types.Failure, error) {
all := obj.Failures
var objs []*types.Failure
for _, v := range all {
objs = append(objs, v.Select())
}
return objs, nil
}
type coreResolver struct{ *Resolver }
func (r *coreResolver) Footer(ctx context.Context, obj *types.Core) (string, error) {
panic("not implemented")
}
func (r *coreResolver) Timezone(ctx context.Context, obj *types.Core) (string, error) {
panic("not implemented")
}
func (r *coreResolver) UsingCdn(ctx context.Context, obj *types.Core) (bool, error) {
panic("not implemented")
}
type messageResolver struct{ *Resolver }
func (r *messageResolver) NotifyUsers(ctx context.Context, obj *types.Message) (bool, error) {
panic("not implemented")
}
func (r *messageResolver) NotifyMethod(ctx context.Context, obj *types.Message) (bool, error) {
panic("not implemented")
}
func (r *messageResolver) NotifyBefore(ctx context.Context, obj *types.Message) (int, error) {
panic("not implemented")
}
type groupResolver struct{ *Resolver }
func (r *groupResolver) Public(ctx context.Context, obj *types.Group) (bool, error) {
return obj.Public.Bool, nil
}
type queryResolver struct{ *Resolver }
func (r *queryResolver) Core(ctx context.Context) (*types.Core, error) {
c := core.CoreApp
return c.Core, nil
}
func (r *queryResolver) Message(ctx context.Context, id int64) (*types.Message, error) {
message, err := core.SelectMessage(id)
return message.Message, err
}
func (r *queryResolver) Messages(ctx context.Context) ([]*types.Message, error) {
all, err := core.SelectMessages()
var objs []*types.Message
for _, v := range all {
objs = append(objs, v.Message)
}
return objs, err
}
func (r *queryResolver) Group(ctx context.Context, id int64) (*types.Group, error) {
group := core.SelectGroup(id)
return group.Group, nil
}
func (r *queryResolver) Groups(ctx context.Context) ([]*types.Group, error) {
all := core.SelectGroups(true, true)
var objs []*types.Group
for _, v := range all {
objs = append(objs, v.Group)
}
return objs, nil
}
func (r *queryResolver) Checkin(ctx context.Context, id int64) (*types.Checkin, error) {
panic("not implemented")
}
func (r *queryResolver) Checkins(ctx context.Context) ([]*types.Checkin, error) {
panic("not implemented")
}
func (r *queryResolver) User(ctx context.Context, id int64) (*types.User, error) {
user, err := core.SelectUser(id)
return user.User, err
}
func (r *queryResolver) Users(ctx context.Context) ([]*types.User, error) {
all, err := core.SelectAllUsers()
var objs []*types.User
for _, v := range all {
objs = append(objs, v.User)
}
return objs, err
}
type userResolver struct{ *Resolver }
func (r *userResolver) Admin(ctx context.Context, obj *types.User) (bool, error) {
return obj.Admin.Bool, nil
}
type serviceResolver struct{ *Resolver }
func (r *queryResolver) Service(ctx context.Context, id int64) (*types.Service, error) {
service := core.SelectService(id)
return service.Service, nil
}
func (r *queryResolver) Services(ctx context.Context) ([]*types.Service, error) {
all := core.Services()
var objs []*types.Service
for _, v := range all {
objs = append(objs, v.Select())
}
return objs, nil
}
func (r *serviceResolver) Expected(ctx context.Context, obj *types.Service) (string, error) {
return obj.Expected.String, nil
}
func (r *serviceResolver) PostData(ctx context.Context, obj *types.Service) (string, error) {
return obj.PostData.String, nil
}
func (r *serviceResolver) AllowNotifications(ctx context.Context, obj *types.Service) (bool, error) {
return obj.AllowNotifications.Bool, nil
}
func (r *serviceResolver) Public(ctx context.Context, obj *types.Service) (bool, error) {
return obj.Public.Bool, nil
}
func (r *serviceResolver) Headers(ctx context.Context, obj *types.Service) (string, error) {
return obj.Headers.String, nil
}
func (r *serviceResolver) Permalink(ctx context.Context, obj *types.Service) (string, error) {
return obj.Permalink.String, nil
}
func (r *serviceResolver) Online24Hours(ctx context.Context, obj *types.Service) (float64, error) {
return float64(obj.Online24Hours), nil
}
func (r *serviceResolver) Failures(ctx context.Context, obj *types.Service) ([]*types.Failure, error) {
all := obj.Failures
var objs []*types.Failure
for _, v := range all {
objs = append(objs, v.Select())
}
return objs, nil
}
func (r *serviceResolver) Group(ctx context.Context, obj *types.Service) (*types.Group, error) {
group := core.SelectGroup(int64(obj.GroupId))
return group.Group, nil
}

View File

@ -0,0 +1,127 @@
schema {
query: Query
}
type Query {
core: Core
service(id: ID!): Service
services: [Service]!
group(id: ID!): Group
groups: [Group]!
user(id: ID!): User
users: [User]!
checkin(id: ID!): Checkin
checkins: [Checkin]!
message(id: ID!): Message
messages: [Message]!
}
type Core {
name: String!
description: String!
footer: String!
domain: String!
version: String!
timezone: String!
using_cdn: Boolean!
started_on: Time!
created_at: Time!
updated_at: Time!
}
type Service {
id: ID!
name: String!
domain: String!
expected: String!
expected_status: Int!
interval: Int!
type: String!
method: String!
post_data: String!
port: Int!
timeout: Int!
order_id: Int!
allow_notifications: Boolean!
public: Boolean!
group: Group!
headers: String!
permalink: String!
online: Boolean!
latency: Float!
ping_time: Float!
online_24_hours: Float!
avg_response: String!
status_code: Int!
last_success: Time!
failures: [Failure]
created_at: Time!
updated_at: Time!
}
type Checkin {
id: ID!
service: Service!
name: String!
interval: Int!
grace: Int!
api_key: String!
failing: Boolean!
last_hit: Time!
failures: [Failure]
hits: [CheckinHit]
created_at: Time!
updated_at: Time!
}
type CheckinHit {
id: ID!
from: String!
created_at: Time!
}
type Group {
id: ID!
name: String!
public: Boolean!
order_id: Int!
created_at: Time!
updated_at: Time!
}
type User {
id: ID!
username: String!
email: String!
api_key: String!
api_secret: String!
admin: Boolean!
created_at: Time!
updated_at: Time!
}
type Failure {
id: ID!
issue: String!
method: String!
method_id: Int!
error_code: Int!
ping: Float!
created_at: Time!
}
type Message {
id: ID!
title: String!
description: String!
start_on: Time!
end_on: Time!
notify_users: Boolean!
notify_method: Boolean!
notify_before: Int!
notify_before_scale: String!
created_at: Time!
updated_at: Time!
}
scalar Time

View File

@ -24,40 +24,59 @@ import (
"net/http"
)
// apiAllGroupHandler will show all the groups
func apiAllGroupHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
func groupViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var group *core.Group
id := vars["id"]
group = core.SelectGroup(utils.ToInt(id))
if group == nil {
w.WriteHeader(http.StatusNotFound)
return
}
auth := IsUser(r)
groups := core.SelectGroups(false, auth)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(groups)
ExecuteResponse(w, r, "group.gohtml", group, nil)
}
// apiAllGroupHandler will show all the groups
func apiAllGroupHandler(w http.ResponseWriter, r *http.Request) {
auth, admin := IsUser(r), IsAdmin(r)
groups := core.SelectGroups(admin, auth)
returnJson(groups, w, r)
}
// apiGroupHandler will show a single group
func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
sendErrorJson(errors.New("group not found"), w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(group)
returnJson(group, w, r)
}
// apiGroupUpdateHandler will update a group
func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
sendErrorJson(errors.New("group not found"), w, r)
return
}
decoder := json.NewDecoder(r.Body)
decoder.Decode(&group)
_, err := group.Update()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(group, "update", w, r)
}
// apiCreateGroupHandler accepts a POST method to create new groups
func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var group *core.Group
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&group)
@ -75,10 +94,6 @@ func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) {
// apiGroupDeleteHandler accepts a DELETE method to delete groups
func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
@ -99,10 +114,6 @@ type groupOrder struct {
}
func apiGroupReorderHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
var newOrder []*groupOrder
decoder := json.NewDecoder(r.Body)
@ -112,6 +123,5 @@ func apiGroupReorderHandler(w http.ResponseWriter, r *http.Request) {
group.Order = g.Order
group.Update()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(newOrder)
returnJson(newOrder, w, r)
}

View File

@ -17,6 +17,7 @@ package handlers
import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
@ -216,13 +217,11 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
utils.Log(4, err)
}
// render the page requested
_, err = mainTemplate.Parse(render)
if err != nil {
if _, err := mainTemplate.Parse(render); err != nil {
utils.Log(4, err)
}
// execute the template
err = mainTemplate.Execute(w, data)
if err != nil {
if err := mainTemplate.Execute(w, data); err != nil {
utils.Log(4, err)
}
}
@ -245,15 +244,17 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data
return core.CoreApp.Services
},
})
_, err = t.Parse(render)
if err != nil {
if _, err := t.Parse(render); err != nil {
utils.Log(4, err)
}
if err := t.Execute(w, data); err != nil {
utils.Log(4, err)
}
}
err = t.Execute(w, data)
if err != nil {
utils.Log(4, err)
}
func returnJson(d interface{}, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(d)
}
// error404Handler is a HTTP handler for 404 error pages

11
handlers/incident.go Normal file
View File

@ -0,0 +1,11 @@
package handlers
import (
"github.com/hunterlong/statping/core"
"net/http"
)
func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
incidents := core.AllIncidents()
returnJson(incidents, w, r)
}

View File

@ -16,7 +16,6 @@
package handlers
import (
"encoding/json"
"github.com/hunterlong/statping/core"
"net/http"
)
@ -30,10 +29,10 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
health := map[string]interface{}{
"services": len(core.Services()),
"online": core.Configs != nil,
"online": true,
"setup": core.Configs != nil,
}
json.NewEncoder(w).Encode(health)
returnJson(health, w, r)
}

View File

@ -35,10 +35,6 @@ func messagesHandler(w http.ResponseWriter, r *http.Request) {
}
func viewMessageHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id := utils.ToInt(vars["id"])
message, err := core.SelectMessage(id)
@ -50,24 +46,15 @@ func viewMessageHandler(w http.ResponseWriter, r *http.Request) {
}
func apiAllMessagesHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
messages, err := core.SelectMessages()
if err != nil {
sendErrorJson(err, w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(messages)
returnJson(messages, w, r)
}
func apiMessageCreateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var message *types.Message
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&message)
@ -85,25 +72,16 @@ func apiMessageCreateHandler(w http.ResponseWriter, r *http.Request) {
}
func apiMessageGetHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
message, err := core.SelectMessage(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(err, w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(message)
returnJson(message, w, r)
}
func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
message, err := core.SelectMessage(utils.ToInt(vars["id"]))
if err != nil {
@ -119,10 +97,6 @@ func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
message, err := core.SelectMessage(utils.ToInt(vars["id"]))
if err != nil {

68
handlers/middleware.go Normal file
View File

@ -0,0 +1,68 @@
package handlers
import (
"github.com/hunterlong/statping/core"
"net/http"
"net/http/httptest"
"time"
)
// 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)
} else {
sendUnauthorizedJson(w, r)
}
return
}
handler(w, r)
})
}
// readOnly is a middleware function to check if user is a User before running original request
func readOnly(handler func(w http.ResponseWriter, r *http.Request), redirect bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
if redirect {
http.Redirect(w, r, "/", http.StatusSeeOther)
} else {
sendUnauthorizedJson(w, r)
}
return
}
handler(w, r)
})
}
// cached is a middleware function that accepts a duration and content type and will cache the response of the original request
func cached(duration, contentType string, handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
content := CacheStorage.Get(r.RequestURI)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Access-Control-Allow-Origin", "*")
if core.Configs == nil {
handler(w, r)
return
}
if content != nil {
w.Write(content)
} else {
c := httptest.NewRecorder()
handler(c, r)
content := c.Body.Bytes()
result := c.Result()
if result.StatusCode != 200 {
w.WriteHeader(result.StatusCode)
w.Write(content)
return
}
w.Write(content)
if d, err := time.ParseDuration(duration); err == nil {
go CacheStorage.Set(r.RequestURI, content, d)
}
}
})
}

View File

@ -27,39 +27,25 @@ import (
)
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var notifiers []*notifier.Notification
for _, n := range core.CoreApp.Notifications {
notif := n.(notifier.Notifier)
notifiers = append(notifiers, notif.Select())
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(notifiers)
returnJson(notifiers, w, r)
}
func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
_, notifierObj, err := notifier.SelectNotifier(vars["notifier"])
if err != nil {
sendErrorJson(err, w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(notifierObj)
returnJson(notifierObj, w, r)
}
func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
notifer, not, err := notifier.SelectNotifier(vars["notifier"])
if err != nil {
@ -83,10 +69,6 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
var err error
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
form := parseForm(r)
vars := mux.Vars(r)
method := vars["method"]

View File

@ -27,10 +27,6 @@ type PluginSelect struct {
}
func pluginSavedHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
//vars := mux.Vars(router)
//plug := SelectPlugin(vars["name"])
@ -43,11 +39,6 @@ func pluginSavedHandler(w http.ResponseWriter, r *http.Request) {
}
func pluginsDownloadHandler(w http.ResponseWriter, r *http.Request) {
auth := IsFullAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
//vars := mux.Vars(router)
//name := vars["name"]
//DownloadPlugin(name)

View File

@ -33,10 +33,6 @@ import (
//
func prometheusHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
metrics := []string{}
system := fmt.Sprintf("statping_total_failures %v\n", core.CountFailures())
system += fmt.Sprintf("statping_total_services %v", len(core.CoreApp.Services))

View File

@ -17,9 +17,11 @@ package handlers
import (
"fmt"
"github.com/99designs/gqlgen/handler"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers/graphql"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"net/http"
@ -36,7 +38,7 @@ func Router() *mux.Router {
dir := utils.Directory
CacheStorage = NewStorage()
r := mux.NewRouter()
r.Handle("/", cached("60s", "text/html", http.HandlerFunc(indexHandler)))
r.Handle("/", http.HandlerFunc(indexHandler))
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"))))
@ -59,85 +61,96 @@ func Router() *mux.Router {
r.Handle("/dashboard", http.HandlerFunc(dashboardHandler)).Methods("GET")
r.Handle("/dashboard", http.HandlerFunc(loginHandler)).Methods("POST")
r.Handle("/logout", http.HandlerFunc(logoutHandler))
r.Handle("/plugins/download/{name}", http.HandlerFunc(pluginsDownloadHandler))
r.Handle("/plugins/{name}/save", http.HandlerFunc(pluginSavedHandler)).Methods("POST")
r.Handle("/help", http.HandlerFunc(helpHandler))
r.Handle("/logs", http.HandlerFunc(logsHandler))
r.Handle("/logs/line", http.HandlerFunc(logsLineHandler))
r.Handle("/plugins/download/{name}", authenticated(pluginsDownloadHandler, true))
r.Handle("/plugins/{name}/save", authenticated(pluginSavedHandler, true)).Methods("POST")
r.Handle("/help", authenticated(helpHandler, true))
r.Handle("/logs", authenticated(logsHandler, true))
r.Handle("/logs/line", readOnly(logsLineHandler, true))
// GRAPHQL Route
r.Handle("/graphql", authenticated(handler.GraphQL(graphql.NewExecutableSchema(graphql.Config{Resolvers: &graphql.Resolver{}})), true))
// USER Routes
r.Handle("/users", http.HandlerFunc(usersHandler)).Methods("GET")
r.Handle("/user/{id}", http.HandlerFunc(usersEditHandler)).Methods("GET")
r.Handle("/users", readOnly(usersHandler, true)).Methods("GET")
r.Handle("/user/{id}", authenticated(usersEditHandler, true)).Methods("GET")
// MESSAGES Routes
r.Handle("/messages", http.HandlerFunc(messagesHandler)).Methods("GET")
r.Handle("/message/{id}", http.HandlerFunc(viewMessageHandler)).Methods("GET")
r.Handle("/messages", authenticated(messagesHandler, true)).Methods("GET")
r.Handle("/message/{id}", authenticated(viewMessageHandler, true)).Methods("GET")
// SETTINGS Routes
r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET")
r.Handle("/settings", http.HandlerFunc(saveSettingsHandler)).Methods("POST")
r.Handle("/settings/css", http.HandlerFunc(saveSASSHandler)).Methods("POST")
r.Handle("/settings/build", http.HandlerFunc(saveAssetsHandler)).Methods("GET")
r.Handle("/settings/delete_assets", http.HandlerFunc(deleteAssetsHandler)).Methods("GET")
r.Handle("/settings/export", http.HandlerFunc(exportHandler)).Methods("GET")
r.Handle("/settings", authenticated(settingsHandler, true)).Methods("GET")
r.Handle("/settings", authenticated(saveSettingsHandler, true)).Methods("POST")
r.Handle("/settings/css", authenticated(saveSASSHandler, true)).Methods("POST")
r.Handle("/settings/build", authenticated(saveAssetsHandler, true)).Methods("GET")
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")
// SERVICE Routes
r.Handle("/services", http.HandlerFunc(servicesHandler)).Methods("GET")
r.Handle("/services", authenticated(servicesHandler, true)).Methods("GET")
r.Handle("/service/{id}", http.HandlerFunc(servicesViewHandler)).Methods("GET")
r.Handle("/service/{id}/edit", http.HandlerFunc(servicesViewHandler)).Methods("GET")
r.Handle("/service/{id}/delete_failures", http.HandlerFunc(servicesDeleteFailuresHandler)).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 GROUPS Routes
r.Handle("/api/groups", http.HandlerFunc(apiAllGroupHandler)).Methods("GET")
r.Handle("/api/groups", http.HandlerFunc(apiCreateGroupHandler)).Methods("POST")
r.Handle("/api/groups/{id}", http.HandlerFunc(apiGroupHandler)).Methods("GET")
r.Handle("/api/groups/{id}", http.HandlerFunc(apiGroupDeleteHandler)).Methods("DELETE")
r.Handle("/api/groups/reorder", http.HandlerFunc(apiGroupReorderHandler)).Methods("POST")
r.Handle("/api/groups", readOnly(apiAllGroupHandler, false)).Methods("GET")
r.Handle("/api/groups", authenticated(apiCreateGroupHandler, false)).Methods("POST")
r.Handle("/api/groups/{id}", readOnly(apiGroupHandler, false)).Methods("GET")
r.Handle("/api/groups/{id}", authenticated(apiGroupUpdateHandler, false)).Methods("POST")
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", http.HandlerFunc(apiIndexHandler))
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
r.Handle("/api/clear_cache", http.HandlerFunc(apiClearCacheHandler))
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", http.HandlerFunc(apiAllServicesHandler)).Methods("GET")
r.Handle("/api/services", http.HandlerFunc(apiCreateServiceHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
r.Handle("/api/services/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST")
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}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")
r.Handle("/api/services/{id}/failures", http.HandlerFunc(apiServiceFailuresHandler)).Methods("GET")
r.Handle("/api/services/{id}/failures", http.HandlerFunc(servicesDeleteFailuresHandler)).Methods("DELETE")
r.Handle("/api/services/{id}/hits", http.HandlerFunc(apiServiceHitsHandler)).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")
r.Handle("/api/services/{id}/failures", authenticated(servicesDeleteFailuresHandler, false)).Methods("DELETE")
r.Handle("/api/services/{id}/hits", authenticated(apiServiceHitsHandler, false)).Methods("GET")
// API INCIDENTS Routes
r.Handle("/api/incidents", readOnly(apiAllIncidentsHandler, false)).Methods("GET")
// API USER Routes
r.Handle("/api/users", http.HandlerFunc(apiAllUsersHandler)).Methods("GET")
r.Handle("/api/users", http.HandlerFunc(apiCreateUsersHandler)).Methods("POST")
r.Handle("/api/users/{id}", http.HandlerFunc(apiUserHandler)).Methods("GET")
r.Handle("/api/users/{id}", http.HandlerFunc(apiUserUpdateHandler)).Methods("POST")
r.Handle("/api/users/{id}", http.HandlerFunc(apiUserDeleteHandler)).Methods("DELETE")
r.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")
r.Handle("/api/users", authenticated(apiCreateUsersHandler, false)).Methods("POST")
r.Handle("/api/users/{id}", authenticated(apiUserHandler, false)).Methods("GET")
r.Handle("/api/users/{id}", authenticated(apiUserUpdateHandler, false)).Methods("POST")
r.Handle("/api/users/{id}", authenticated(apiUserDeleteHandler, false)).Methods("DELETE")
// API NOTIFIER Routes
r.Handle("/api/notifiers", http.HandlerFunc(apiNotifiersHandler)).Methods("GET")
r.Handle("/api/notifier/{notifier}", http.HandlerFunc(apiNotifierGetHandler)).Methods("GET")
r.Handle("/api/notifier/{notifier}", http.HandlerFunc(apiNotifierUpdateHandler)).Methods("POST")
r.Handle("/api/notifier/{method}/test", http.HandlerFunc(testNotificationHandler)).Methods("POST")
r.Handle("/api/notifiers", authenticated(apiNotifiersHandler, false)).Methods("GET")
r.Handle("/api/notifier/{notifier}", authenticated(apiNotifierGetHandler, false)).Methods("GET")
r.Handle("/api/notifier/{notifier}", authenticated(apiNotifierUpdateHandler, false)).Methods("POST")
r.Handle("/api/notifier/{method}/test", authenticated(testNotificationHandler, false)).Methods("POST")
// API MESSAGES Routes
r.Handle("/api/messages", http.HandlerFunc(apiAllMessagesHandler)).Methods("GET")
r.Handle("/api/messages", http.HandlerFunc(apiMessageCreateHandler)).Methods("POST")
r.Handle("/api/messages/{id}", http.HandlerFunc(apiMessageGetHandler)).Methods("GET")
r.Handle("/api/messages/{id}", http.HandlerFunc(apiMessageUpdateHandler)).Methods("POST")
r.Handle("/api/messages/{id}", http.HandlerFunc(apiMessageDeleteHandler)).Methods("DELETE")
r.Handle("/api/messages", readOnly(apiAllMessagesHandler, false)).Methods("GET")
r.Handle("/api/messages", authenticated(apiMessageCreateHandler, false)).Methods("POST")
r.Handle("/api/messages/{id}", readOnly(apiMessageGetHandler, false)).Methods("GET")
r.Handle("/api/messages/{id}", authenticated(apiMessageUpdateHandler, false)).Methods("POST")
r.Handle("/api/messages/{id}", authenticated(apiMessageDeleteHandler, false)).Methods("DELETE")
// API CHECKIN Routes
r.Handle("/api/checkins", http.HandlerFunc(apiAllCheckinsHandler)).Methods("GET")
r.Handle("/api/checkin/{api}", http.HandlerFunc(apiCheckinHandler)).Methods("GET")
r.Handle("/api/checkin", http.HandlerFunc(checkinCreateHandler)).Methods("POST")
r.Handle("/api/checkin/{api}", http.HandlerFunc(checkinDeleteHandler)).Methods("DELETE")
r.Handle("/api/checkins", authenticated(apiAllCheckinsHandler, false)).Methods("GET")
r.Handle("/api/checkin/{api}", authenticated(apiCheckinHandler, false)).Methods("GET")
r.Handle("/api/checkin", authenticated(checkinCreateHandler, false)).Methods("POST")
r.Handle("/api/checkin/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE")
r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
// Static Files Routes
@ -146,7 +159,7 @@ func Router() *mux.Router {
r.PathPrefix("/files/grafana.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))
// API Generic Routes
r.Handle("/metrics", http.HandlerFunc(prometheusHandler))
r.Handle("/metrics", readOnly(prometheusHandler, false))
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))

View File

@ -49,10 +49,6 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
}
func servicesHandler(w http.ResponseWriter, r *http.Request) {
if !IsUser(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
data := map[string]interface{}{
"Services": core.CoreApp.Services,
}
@ -65,10 +61,6 @@ type serviceOrder struct {
}
func reorderServiceHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
var newOrder []*serviceOrder
decoder := json.NewDecoder(r.Body)
@ -78,8 +70,7 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) {
service.Order = s.Order
service.Update(false)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(newOrder)
returnJson(newOrder, w, r)
}
func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
@ -131,25 +122,16 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
sendErrorJson(errors.New("service not found"), w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(servicer)
returnJson(servicer, w, r)
}
func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var service *types.Service
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&service)
@ -167,10 +149,6 @@ func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
@ -188,6 +166,21 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
sendJsonAction(service, "update", w, r)
}
func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
sendErrorJson(errors.New("service not found"), w, r)
return
}
if service.IsRunning() {
service.Close()
} else {
service.Start()
}
sendJsonAction(service, "running", w, r)
}
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
@ -207,8 +200,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
end := time.Unix(endField, 0)
obj := core.GraphDataRaw(service, start, end, grouping, "latency")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(obj)
returnJson(obj, w, r)
}
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
@ -227,9 +219,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
end := time.Unix(endField, 0)
obj := core.GraphDataRaw(service, start, end, grouping, "ping_time")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(obj)
returnJson(obj, w, r)
}
type dataXy struct {
@ -285,16 +275,10 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
month = 1
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(monthOutput)
returnJson(monthOutput, w, r)
}
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
@ -310,20 +294,11 @@ func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func apiAllServicesHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
services := core.Services()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(services)
returnJson(services, w, r)
}
func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
@ -335,25 +310,16 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceFailuresHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
sendErrorJson(errors.New("service not found"), w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(servicer.AllFailures())
returnJson(servicer.AllFailures(), w, r)
}
func apiServiceHitsHandler(w http.ResponseWriter, r *http.Request) {
if !IsReadAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
@ -367,6 +333,5 @@ func apiServiceHitsHandler(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(hits)
returnJson(hits, w, r)
}

View File

@ -16,73 +16,67 @@
package handlers
import (
"bytes"
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
func settingsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, nil)
}
func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
var err error
r.ParseForm()
form := parseForm(r)
app := core.CoreApp
name := r.PostForm.Get("project")
name := form.Get("project")
if name != "" {
app.Name = name
}
description := r.PostForm.Get("description")
description := form.Get("description")
if description != app.Description {
app.Description = description
}
style := r.PostForm.Get("style")
style := form.Get("style")
if style != app.Style {
app.Style = style
}
footer := r.PostForm.Get("footer")
footer := form.Get("footer")
if footer != app.Footer.String {
app.Footer = types.NewNullString(footer)
}
domain := r.PostForm.Get("domain")
domain := form.Get("domain")
if domain != app.Domain {
app.Domain = domain
}
timezone := r.PostForm.Get("timezone")
timezone := form.Get("timezone")
timeFloat, _ := strconv.ParseFloat(timezone, 10)
app.Timezone = float32(timeFloat)
app.UseCdn = types.NewNullBool(r.PostForm.Get("enable_cdn") == "on")
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()))
}
//notifiers.OnSettingsSaved(core.CoreApp.ToCore())
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
}
func saveSASSHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
form := r.PostForm
form := parseForm(r)
theme := form.Get("theme")
variables := form.Get("variables")
mobile := form.Get("mobile")
@ -95,19 +89,13 @@ func saveSASSHandler(w http.ResponseWriter, r *http.Request) {
}
func saveAssetsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
dir := utils.Directory
err := source.CreateAllAssets(dir)
if err != nil {
if err := source.CreateAllAssets(dir); err != nil {
utils.Log(3, err)
sendErrorJson(err, w, r)
return
}
err = source.CompileSASS(dir)
if err != nil {
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.")
}
@ -116,18 +104,101 @@ func saveAssetsHandler(w http.ResponseWriter, r *http.Request) {
}
func deleteAssetsHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
if err := source.DeleteAllAssets(utils.Directory); err != nil {
utils.Log(3, fmt.Errorf("error deleting all assets %v", err))
}
source.DeleteAllAssets(utils.Directory)
resetRouter()
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
}
func parseId(r *http.Request) int64 {
vars := mux.Vars(r)
return utils.ToInt(vars["id"])
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))
w.Write([]byte(err.Error()))
return
}
defer file.Close()
io.Copy(&fileData, file)
data := fileData.String()
for i, line := range strings.Split(strings.TrimSuffix(data, "\n"), "\n")[1:] {
col := strings.Split(line, ",")
newService, err := commaToService(col)
if err != nil {
utils.Log(3, 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))
continue
}
utils.Log(1, fmt.Sprintf("Created new service %v", service.Name))
}
ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "/settings")
}
// 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) != 17 {
err := fmt.Errorf("does not have the expected amount of %v columns for a service", 16)
return nil, err
}
interval, err := time.ParseDuration(s[4])
if err != nil {
return nil, err
}
timeout, err := time.ParseDuration(s[9])
if err != nil {
return nil, err
}
allowNotifications, err := strconv.ParseBool(s[11])
if err != nil {
return nil, err
}
public, err := strconv.ParseBool(s[12])
if err != nil {
return nil, err
}
verifySsl, err := strconv.ParseBool(s[16])
if err != nil {
return nil, err
}
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
}
func parseForm(r *http.Request) url.Values {

View File

@ -53,10 +53,11 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
project := r.PostForm.Get("project")
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
//sample := r.PostForm.Get("sample_data")
description := r.PostForm.Get("description")
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{
@ -117,7 +118,9 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
})
admin.Create()
core.SampleData()
if sample {
core.SampleData()
}
core.InitApp()
CacheStorage.Delete("/")
resetCookies()

View File

@ -28,19 +28,11 @@ import (
)
func usersHandler(w http.ResponseWriter, r *http.Request) {
if !IsUser(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
users, _ := core.SelectAllUsers()
ExecuteResponse(w, r, "users.gohtml", users, nil)
}
func usersEditHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
user, _ := core.SelectUser(int64(id))
@ -48,10 +40,6 @@ func usersEditHandler(w http.ResponseWriter, r *http.Request) {
}
func apiUserHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
user, err := core.SelectUser(utils.ToInt(vars["id"]))
if err != nil {
@ -59,15 +47,10 @@ func apiUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
user.Password = ""
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
returnJson(user, w, r)
}
func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
user, err := core.SelectUser(utils.ToInt(vars["id"]))
if err != nil {
@ -88,10 +71,6 @@ func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
vars := mux.Vars(r)
users := core.CountUsers()
if users == 1 {
@ -112,24 +91,15 @@ func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
users, err := core.SelectAllUsers()
if err != nil {
sendErrorJson(err, w, r)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
returnJson(users, w, r)
}
func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
var user *types.User
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&user)

View File

@ -16,6 +16,7 @@
package notifiers
import (
"fmt"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
@ -75,23 +76,21 @@ 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(s.Id, u.Var2)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), u.Var2)
}
// OnSuccess for commandLine will trigger successful service
func (u *commandLine) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
u.AddQueue(s.Id, u.Var1)
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
func (u *commandLine) OnSave() error {
u.AddQueue(0, u.Var1)
u.AddQueue(0, u.Var2)
u.AddQueue("saved", u.Var1)
u.AddQueue("saved", u.Var2)
return nil
}

View File

@ -66,33 +66,16 @@ func TestCommandNotifier(t *testing.T) {
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 Check Offline", func(t *testing.T) {
assert.False(t, command.Online)
})
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)
time.Sleep(20 * time.Second)
assert.Equal(t, 0, len(command.Queue))
})

View File

@ -59,7 +59,7 @@ func init() {
// 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
}
@ -70,24 +70,27 @@ func (u *discord) Select() *notifier.Notification {
// OnFailure will trigger failing service
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(s.Id, msg)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (u *discord) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
msg := fmt.Sprintf(`{"content": "Your service '%v' is back online!"}`, s.Name)
u.AddQueue(s.Id, msg)
if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
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
func (u *discord) OnSave() error {
msg := fmt.Sprintf(`{"content": "The discord notifier on Statping was just updated."}`)
u.AddQueue(0, msg)
u.AddQueue("saved", msg)
return nil
}
@ -95,7 +98,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

@ -69,17 +69,13 @@ func TestDiscordNotifier(t *testing.T) {
assert.Equal(t, 1, len(discorder.Queue))
})
t.Run("discord Check Offline", func(t *testing.T) {
assert.False(t, discorder.Online)
})
t.Run("discord OnSuccess", func(t *testing.T) {
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) {

View File

@ -24,7 +24,6 @@ import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
"net/smtp"
"time"
)
@ -149,6 +148,12 @@ var emailer = &email{&notifier.Notification{
Title: "Send Alerts To",
Placeholder: "sendto@email.com",
DbField: "Var2",
}, {
Type: "text",
Title: "Disable TLS/SSL",
Placeholder: "",
SmallText: "To Disable TLS/SSL insert 'true'",
DbField: "api_key",
}},
}}
@ -188,24 +193,28 @@ func (u *email) OnFailure(s *types.Service, f *types.Failure) {
Data: interface{}(s),
From: u.Var1,
}
u.AddQueue(s.Id, email)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), email)
}
// OnSuccess will trigger successful service
func (u *email) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
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(s.Id, email)
u.AddQueue(fmt.Sprintf("service_%v", s.Id), email)
}
u.Online = true
}
func (u *email) Select() *notifier.Notification {
@ -221,20 +230,6 @@ func (u *email) OnSave() error {
// 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
}
err = dial.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return err
}
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",
@ -253,18 +248,22 @@ func (u *email) OnTest() error {
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.TLSConfig = &tls.Config{InsecureSkipVerify: true}
emailSource(email)
m := mail.NewMessage()
// if email setting TLS is Disabled
if u.ApiKey == "true" {
mailer.SSL = false
} else {
mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
m.SetHeader("From", email.From)
m.SetHeader("To", email.To)
m.SetHeader("Subject", email.Subject)

View File

@ -105,17 +105,13 @@ func TestEmailNotifier(t *testing.T) {
assert.Equal(t, 1, len(emailer.Queue))
})
t.Run("email Check Offline", func(t *testing.T) {
assert.False(t, emailer.Online)
})
t.Run("email OnSuccess", func(t *testing.T) {
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) {
@ -136,7 +132,7 @@ func TestEmailNotifier(t *testing.T) {
t.Run("email Run Queue", func(t *testing.T) {
go notifier.Queue(emailer)
time.Sleep(5 * time.Second)
time.Sleep(6 * time.Second)
assert.Equal(t, EMAIL_HOST, emailer.Host)
assert.Equal(t, 0, len(emailer.Queue))
})

View File

@ -62,7 +62,7 @@ func (u *lineNotifier) Send(msg interface{}) error {
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
}
@ -73,23 +73,27 @@ func (u *lineNotifier) Select() *notifier.Notification {
// OnFailure will trigger failing service
func (u *lineNotifier) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(s.Id, msg)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (u *lineNotifier) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
msg := fmt.Sprintf("Your service '%v' is back online!", s.Name)
u.AddQueue(s.Id, msg)
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))
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(1, msg)
u.AddQueue("saved", message)
return nil
}

View File

@ -34,7 +34,7 @@ type mobilePush struct {
var mobile = &mobilePush{&notifier.Notification{
Method: "mobile",
Title: "Mobile Notifications",
Description: `Receive push notifications on your Android or iPhone devices 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",
@ -99,24 +99,28 @@ func (u *mobilePush) OnFailure(s *types.Service, f *types.Failure) {
Topic: mobileIdentifier,
Data: data,
}
u.AddQueue(s.Id, msg)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (u *mobilePush) OnSuccess(s *types.Service) {
data := dataJson(s, nil)
if !u.Online {
u.ResetUniqueQueue(s.Id)
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(s.Id, msg)
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved
@ -126,7 +130,7 @@ func (u *mobilePush) OnSave() error {
Title: "Notification Saved",
Topic: mobileIdentifier,
}
u.AddQueue(0, msg)
u.AddQueue("saved", msg)
return nil
}
@ -178,7 +182,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

@ -76,29 +76,20 @@ func TestMobileNotifier(t *testing.T) {
})
t.Run("mobile OnFailure multiple times", func(t *testing.T) {
for i := 0; i <= 50; i++ {
for i := 0; i <= 5; i++ {
mobile.OnFailure(TestService, TestFailure)
}
assert.Equal(t, 52, len(mobile.Queue))
})
t.Run("mobile Check Offline", func(t *testing.T) {
assert.False(t, mobile.Online)
assert.Equal(t, 7, len(mobile.Queue))
})
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))
assert.Equal(t, 7, len(mobile.Queue))
})
t.Run("mobile OnSuccess Again", func(t *testing.T) {
t.SkipNow()
assert.True(t, mobile.Online)
assert.True(t, TestService.Online)
mobile.OnSuccess(TestService)
assert.Equal(t, 1, len(mobile.Queue))
go notifier.Queue(mobile)

View File

@ -42,6 +42,7 @@ 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),
}

View File

@ -29,7 +29,7 @@ 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":"{{.}}"}`
)
@ -71,7 +71,7 @@ func parseSlackMessage(id int64, temp string, data interface{}) error {
if err != nil {
return err
}
slacker.AddQueue(id, buf.String())
slacker.AddQueue(fmt.Sprintf("service_%v", id), buf.String())
return nil
}
@ -79,12 +79,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 +94,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")
}
@ -106,15 +107,15 @@ func (u *slack) OnFailure(s *types.Service, f *types.Failure) {
Service: s,
Template: failingTemplate,
Time: time.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 {
u.ResetUniqueQueue(s.Id)
if !s.Online {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
message := slackMessage{
Service: s,
Template: successTemplate,
@ -122,12 +123,11 @@ func (u *slack) OnSuccess(s *types.Service) {
}
parseSlackMessage(s.Id, successTemplate, message)
}
u.Online = true
}
// OnSave triggers when this notifier has been saved
func (u *slack) OnSave() error {
message := fmt.Sprintf("Notification %v is receiving updated information.", u.Method)
u.AddQueue(0, message)
u.AddQueue("saved", message)
return nil
}

View File

@ -78,33 +78,17 @@ func TestSlackNotifier(t *testing.T) {
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)
})
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))
})
t.Run("slack OnSuccess Again", func(t *testing.T) {
assert.True(t, slacker.Online)
assert.True(t, TestService.Online)
slacker.OnSuccess(TestService)
assert.Equal(t, 1, len(slacker.Queue))
go notifier.Queue(slacker)
time.Sleep(6 * time.Second)
time.Sleep(15 * time.Second)
assert.Equal(t, 0, len(slacker.Queue))
})
@ -127,7 +111,7 @@ func TestSlackNotifier(t *testing.T) {
t.Run("slack Queue", func(t *testing.T) {
go notifier.Queue(slacker)
time.Sleep(5 * time.Second)
time.Sleep(10 * time.Second)
assert.Equal(t, SLACK_URL, slacker.Host)
assert.Equal(t, 0, len(slacker.Queue))
})

View File

@ -78,7 +78,7 @@ func (u *telegram) Send(msg interface{}) error {
v.Set("text", message)
rb := *strings.NewReader(v.Encode())
contents, _, err := utils.HttpRequest(apiEndpoint, "GET", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second))
contents, _, err := utils.HttpRequest(apiEndpoint, "GET", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second), true)
success, _ := telegramSuccess(contents)
if !success {
@ -92,18 +92,21 @@ func (u *telegram) Send(msg interface{}) error {
// OnFailure will trigger failing service
func (u *telegram) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(s.Id, msg)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (u *telegram) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
msg := fmt.Sprintf("Your service '%v' is back online!", s.Name)
u.AddQueue(s.Id, msg)
if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
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

View File

@ -37,6 +37,7 @@ func init() {
}
func TestTelegramNotifier(t *testing.T) {
t.SkipNow()
t.Parallel()
if telegramToken == "" || telegramChannel == "" {
t.Log("Telegram notifier testing skipped, missing TELEGRAM_TOKEN and TELEGRAM_CHANNEL environment variable")
@ -71,7 +72,7 @@ func TestTelegramNotifier(t *testing.T) {
})
t.Run("Telegram Check Offline", func(t *testing.T) {
assert.False(t, telegramNotifier.Online)
assert.False(t, TestService.Online)
})
t.Run("Telegram OnSuccess", func(t *testing.T) {
@ -80,7 +81,7 @@ func TestTelegramNotifier(t *testing.T) {
})
t.Run("Telegram Check Back Online", func(t *testing.T) {
assert.True(t, telegramNotifier.Online)
assert.True(t, TestService.Online)
})
t.Run("Telegram OnSuccess Again", func(t *testing.T) {

View File

@ -89,7 +89,7 @@ func (u *twilio) Send(msg interface{}) error {
v.Set("Body", message)
rb := *strings.NewReader(v.Encode())
contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second))
contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second), true)
success, _ := twilioSuccess(contents)
if !success {
errorOut := twilioError(contents)
@ -102,18 +102,21 @@ func (u *twilio) Send(msg interface{}) error {
// OnFailure will trigger failing service
func (u *twilio) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(s.Id, msg)
u.Online = false
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (u *twilio) OnSuccess(s *types.Service) {
if !u.Online {
u.ResetUniqueQueue(s.Id)
msg := fmt.Sprintf("Your service '%v' is back online!", s.Name)
u.AddQueue(s.Id, msg)
if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg string
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

View File

@ -45,7 +45,6 @@ func init() {
func TestTwilioNotifier(t *testing.T) {
t.SkipNow()
t.Parallel()
if TWILIO_SID == "" || TWILIO_SECRET == "" || TWILIO_FROM == "" {
t.Log("twilio notifier testing skipped, missing TWILIO_SID environment variable")
t.SkipNow()
@ -77,7 +76,7 @@ func TestTwilioNotifier(t *testing.T) {
})
t.Run("Twilio Check Offline", func(t *testing.T) {
assert.False(t, twilioNotifier.Online)
assert.False(t, TestService.Online)
})
t.Run("Twilio OnSuccess", func(t *testing.T) {
@ -86,7 +85,7 @@ func TestTwilioNotifier(t *testing.T) {
})
t.Run("Twilio Check Back Online", func(t *testing.T) {
assert.True(t, twilioNotifier.Online)
assert.True(t, TestService.Online)
})
t.Run("Twilio OnSuccess Again", func(t *testing.T) {

View File

@ -100,14 +100,8 @@ func (w *webhooker) Select() *notifier.Notification {
}
func replaceBodyText(body string, s *types.Service, f *types.Failure) string {
if s != nil {
body = strings.Replace(body, "%service.Name", s.Name, -1)
body = strings.Replace(body, "%service.Id", utils.ToString(s.Id), -1)
body = strings.Replace(body, "%service.Online", utils.ToString(s.Online), -1)
}
if strings.Contains(body, "%failure.Issue") && f != nil {
body = strings.Replace(body, "%failure.Issue", f.Issue, -1)
}
body = utils.ConvertInterface(body, s)
body = utils.ConvertInterface(body, f)
return body
}
@ -171,18 +165,16 @@ func (w *webhooker) OnTest() error {
// OnFailure will trigger failing service
func (w *webhooker) OnFailure(s *types.Service, f *types.Failure) {
msg := replaceBodyText(w.Var2, s, f)
w.AddQueue(s.Id, msg)
w.Online = false
w.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
// OnSuccess will trigger successful service
func (w *webhooker) OnSuccess(s *types.Service) {
if !w.Online {
w.ResetUniqueQueue(s.Id)
if !s.Online {
w.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
msg := replaceBodyText(w.Var2, s, nil)
w.AddQueue(s.Id, msg)
w.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)
}
w.Online = true
}
// OnSave triggers when this notifier has been saved

View File

@ -65,7 +65,7 @@ func TestWebhookNotifier(t *testing.T) {
t.Run("webhooker Replace Body Text", func(t *testing.T) {
fullMsg = replaceBodyText(webhookMessage, TestService, TestFailure)
assert.Equal(t, `{"id": "1","name": "Interpol - All The Rage Back Home","online": "false","issue": "testing"}`, fullMsg)
assert.Equal(t, `{"id": "1","name": "Interpol - All The Rage Back Home","online": "true","issue": "testing"}`, fullMsg)
})
t.Run("webhooker Within Limits", func(t *testing.T) {
@ -79,17 +79,13 @@ func TestWebhookNotifier(t *testing.T) {
assert.Len(t, webhook.Queue, 1)
})
t.Run("webhooker Check Offline", func(t *testing.T) {
assert.False(t, webhook.Online)
})
t.Run("webhooker OnSuccess", func(t *testing.T) {
webhook.OnSuccess(TestService)
assert.Equal(t, len(webhook.Queue), 1)
})
t.Run("webhooker Check Back Online", func(t *testing.T) {
assert.True(t, webhook.Online)
assert.True(t, TestService.Online)
})
t.Run("webhooker OnSuccess Again", func(t *testing.T) {

View File

@ -125,6 +125,43 @@ HTML, BODY {
height: 300px;
width: 100%; }
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem; }
input.inputTags-field:focus {
outline-width: 0; }
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; }
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px; }
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer; }
.btn-primary {
background-color: #3e9bff;
border-color: #006fe6;
@ -361,13 +398,13 @@ HTML, BODY {
.pulse-glow:before,
.pulse-glow:after {
position: absolute;
content: '';
height: 0.5rem;
width: 1.75rem;
top: 1.2rem;
content: "";
height: 0.4rem;
width: 1.7rem;
top: 1.3rem;
right: 2.15rem;
border-radius: 0;
box-shadow: 0 0 7px #47d337;
box-shadow: 0 0 6px #47d337;
animation: glow-grow 2s ease-out infinite; }
.sortable_drag {
@ -415,6 +452,12 @@ HTML, BODY {
.jumbotron {
background-color: white; }
.toggle-service {
font-size: 18pt;
float: left;
margin: 2px 3px 0 0;
cursor: pointer; }
@media (max-width: 767px) {
HTML, BODY {
background-color: #fcfcfc; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -111,32 +111,7 @@ let options = {
series: [
{
name: "Response Time",
data: [
{
x: "02-10-2017 GMT",
y: 34
},
{
x: "02-11-2017 GMT",
y: 43
},
{
x: "02-12-2017 GMT",
y: 31
},
{
x: "02-13-2017 GMT",
y: 43
},
{
x: "02-14-2017 GMT",
y: 33
},
{
x: "02-15-2017 GMT",
y: 52
}
]
data: [],
}
],
xaxis: {
@ -152,10 +127,12 @@ const startOn = UTCTime() - (86400 * 14);
async function RenderCharts() {
{{ range .Services }}
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);{{end}}
options.fill.colors = {{if .Online}}["#48d338"]{{else}}["#dd3545"]{{end}};
options.stroke.colors = {{if .Online}}["#3aa82d"]{{else}}["#c23342"]{{end}};
{{ range .Services }}
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn);{{end}}
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn);{{end}}
}
$( document ).ready(function() {

1
source/js/inputTags.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -86,6 +86,29 @@ $('.scrollclick').on('click',function(e) {
e.preventDefault();
});
$('.toggle-service').on('click',function(e) {
let obj = $(this);
let serviceId = obj.attr("data-id");
let online = obj.attr("data-online");
let d = confirm("Do you want to "+(eval(online) ? "stop" : "start")+" checking this service?");
if (d) {
$.ajax({
url: "/api/services/" + serviceId + "/running",
type: 'POST',
success: function (data) {
if (online === "true") {
obj.removeClass("fa-toggle-on text-success");
obj.addClass("fa-toggle-off text-black-50");
} else {
obj.removeClass("fa-toggle-off text-black-50");
obj.addClass("fa-toggle-on text-success");
}
obj.attr("data-online", online !== "true");
}
});
}
});
$('select#service_type').on('change', function() {
var selected = $('#service_type option:selected').val();
var typeLabel = $('#service_type_label');
@ -100,6 +123,15 @@ $('select#service_type').on('change', function() {
$('#service_url').attr('placeholder', '192.168.1.1');
$('#post_data').parent().parent().addClass('d-none');
$('#service_response').parent().parent().addClass('d-none');
$('#service_response_code').parent().parent().addClass('d-none');
$('#headers').parent().parent().addClass('d-none');
} else if (selected === 'icmp') {
$('#service_port').parent().parent().removeClass('d-none');
$('#headers').parent().parent().addClass('d-none');
$('#service_check_type').parent().parent().addClass('d-none');
$('#service_url').attr('placeholder', '192.168.1.1');
$('#post_data').parent().parent().addClass('d-none');
$('#service_response').parent().parent().addClass('d-none');
$('#service_response_code').parent().parent().addClass('d-none');
} else {
$('#post_data').parent().parent().removeClass('d-none');
@ -113,6 +145,9 @@ $('select#service_type').on('change', function() {
async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
if (!chart.el) {
return
}
let chartData = await ChartLatency(service, start, end, group, retry);
if (!chartData) {
chartData = await ChartLatency(service, start, end, "minute", retry);
@ -279,7 +314,6 @@ $('form.ajax_form').on('submit', function() {
arrayData.push(newArr)
});
let sendData = JSON.stringify(newArr);
// console.log('sending '+method.toUpperCase()+' '+action+':', sendData);
$.ajax({
url: action,
type: method,
@ -371,8 +405,24 @@ $(function() {
$('.confirm-btn').on('click', function() {
var r = confirm('Are you sure you want to delete?');
let obj = $(this);
let redirect = obj.attr('data-redirect');
let href = obj.attr('href');
let method = obj.attr('data-method');
let data = obj.attr('data-object');
if (r === true) {
return true;
$.ajax({
url: href,
type: method,
data: data ? data : null,
success: function (data) {
console.log("send to url: ", href);
if (redirect) {
window.location.href = redirect;
}
return false;
}
});
} else {
return false;
}

View File

@ -151,6 +151,48 @@ HTML,BODY {
width: 100%;
}
.inputTags-field {
border: 0;
background-color: transparent;
padding-top: .13rem;
}
input.inputTags-field:focus {
outline-width: 0;
}
.inputTags-list {
display: block;
width: 100%;
min-height: calc(2.25rem + 2px);
padding: .2rem .35rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.inputTags-item {
background-color: #3aba39;
margin-right: 5px;
padding: 5px 8px;
font-size: 10pt;
color: white;
border-radius: 4px;
}
.inputTags-item .close-item {
margin-left: 6px;
font-size: 13pt;
font-weight: bold;
cursor: pointer;
}
@mixin dynamic-color-hov($color) {
&.dyn-dark {
background-color: darken($color, 12%) !important;
@ -417,15 +459,15 @@ HTML,BODY {
.pulse-glow:before,
.pulse-glow:after {
position: absolute;
content: '';
height: 0.5rem;
width: 1.75rem;
top: 1.2rem;
right: 2.15rem;
border-radius: 0;
box-shadow: 0 0 7px #47d337;
animation: glow-grow 2s ease-out infinite;
position: absolute;
content: "";
height: 0.4rem;
width: 1.7rem;
top: 1.3rem;
right: 2.15rem;
border-radius: 0;
box-shadow: 0 0 6px #47d337;
animation: glow-grow 2s ease-out infinite;
}
.sortable_drag {
@ -480,4 +522,11 @@ HTML,BODY {
background-color: white;
}
.toggle-service {
font-size: 18pt;
float: left;
margin: 2px 3px 0 0;
cursor: pointer;
}
@import 'mobile';

View File

@ -0,0 +1,5 @@
name,domain,expected,expected_status,interval,type,method,post_data,port,timeout,order,allow_notifications,public,group_id,headers,permalink
Bulk Upload,http://google.com,,200,60s,http,get,,,60s,1,TRUE,TRUE,,Authorization=example,bulk_example
JSON Post,https://jsonplaceholder.typicode.com/posts,,200,1m,http,post,"{""id"": 1, ""title"": 'foo', ""body"": 'bar', ""userId"": 1}",,15s,2,TRUE,TRUE,,Content-Type=application/json,json_post_example
Google DNS,8.8.8.8,,,,tcp,,,53,10s,3,TRUE,TRUE,,,google_dns_example
Google DNS UDP,8.8.8.8,,,,udp,,,53,10s,4,TRUE,TRUE,,,google_dns_udp_example
1 name domain expected expected_status interval type method post_data port timeout order allow_notifications public group_id headers permalink
2 Bulk Upload http://google.com 200 60s http get 60s 1 TRUE TRUE Authorization=example bulk_example
3 JSON Post https://jsonplaceholder.typicode.com/posts 200 1m http post {"id": 1, "title": 'foo', "body": 'bar', "userId": 1} 15s 2 TRUE TRUE Content-Type=application/json json_post_example
4 Google DNS 8.8.8.8 tcp 53 10s 3 TRUE TRUE google_dns_example
5 Google DNS UDP 8.8.8.8 udp 53 10s 4 TRUE TRUE google_dns_udp_example

View File

@ -17,9 +17,9 @@
<label for="order" class="col-sm-4 col-form-label">Public Group</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="public" class="switch" id="switch-group-public" checked>
<input type="checkbox" name="public" class="switch" id="switch-group-public" {{if .Public.Bool}}checked{{end}}>
<label for="switch-group-public">Show group services to the public</label>
<input type="hidden" name="public" id="switch-group-public-value" value="true">
<input type="hidden" name="public" id="switch-group-public-value" value="{{if .Public.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>

View File

@ -0,0 +1,88 @@
{{define "form_incident"}}
<div class="card">
<div class="card-body">
{{$message := .}}
{{if ne .Id 0}}
<form class="ajax_form" action="/api/messages/{{.Id}}" data-redirect="/messages" method="POST">
{{else}}
<form class="ajax_form" action="/api/messages" data-redirect="/messages" method="POST">
{{end}}
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input type="text" name="title" class="form-control" value="{{.Title}}" id="title" placeholder="Message Title" required>
</div>
</div>
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea rows="5" name="description" class="form-control" id="description" required>{{.Description}}</textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4">
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="{{ParseTime .StartOn "2006-01-02T15:04:05Z"}}" required>
</div>
<div class="col-sm-4">
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="{{ParseTime .EndOn "2006-01-02T15:04:05Z"}}" required>
</div>
</div>
<div class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select class="form-control" name="service" id="service_id">
<option value="0" {{if eq (ToString .ServiceId) "0"}}selected{{end}}>Global Message</option>
{{range Services}}
{{$s := .Select}}
<option value="{{$s.Id}}" {{if eq $message.ServiceId $s.Id}}selected{{end}}>{{$s.Name}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<div class="col-sm-8">
<input type="text" name="notify_method" class="form-control" id="notify_method" value="{{.NotifyMethod}}" placeholder="email">
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notify Users</label>
<div class="col-sm-8">
<span class="switch">
<input type="checkbox" name="notify_users-value" class="switch" id="switch-normal"{{if .NotifyUsers.Bool}} checked{{end}}>
<label for="switch-normal">Notify Users Before Scheduled Time</label>
<input type="hidden" name="notify_users" id="switch-normal-value" value="{{if .NotifyUsers.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8">
<div class="form-inline">
<input type="number" name="notify_before" class="col-4 form-control" id="notify_before" value="{{.NotifyBefore.Int64}}">
<select class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute"{{if ne .Id 0}} selected{{else}}{{if eq .NotifyBeforeScale "minute"}}selected{{end}}{{end}}>Minutes</option>
<option value="hour"{{if eq .NotifyBeforeScale "hour"}} selected{{end}}>Hours</option>
<option value="day"{{if eq .NotifyBeforeScale "day"}} selected{{end}}>Days</option>
</select>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Message{{else}}Create Message{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -21,6 +21,7 @@
<option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option>
<option value="udp" {{if eq .Type "udp"}}selected{{end}}>UDP Service</option>
<option value="icmp" {{if eq .Type "icmp"}}selected{{end}}>ICMP Ping</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div>
@ -52,6 +53,13 @@
<small class="form-text text-muted">Insert a JSON string to send data to the endpoint.</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="headers" class="col-sm-4 col-form-label">HTTP Headers</label>
<div class="col-sm-8">
<input name="headers" class="form-control" id="headers" autocapitalize="none" spellcheck="false" placeholder='Authorization=1010101,Content-Type=application/json' value="{{.Headers.String}}">
<small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
@ -66,7 +74,7 @@
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
</div>
</div>
<div class="form-group row{{if (ne .Type "tcp") and (ne .Type "udp")}} d-none{{end}}">
<div class="form-group row{{if (ne .Type "tcp") and (ne .Type "udp") and (ne .Type "icmp")}} d-none{{end}}">
<label for="port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{if ne .Port 0}}{{.Port}}{{end}}" id="service_port" placeholder="8080">
@ -100,13 +108,23 @@
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" {{if eq .Id 0}}checked{{end}}{{if .VerifySSL.Bool}}checked{{end}}>
<label for="switch-verify-ssl">Verify SSL Certificate for this service</label>
<input type="hidden" name="verify_ssl" id="switch-verify-ssl-value" value="{{if eq .Id 0}}true{{else}}{{if .VerifySSL.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" {{if eq .Id 0}}checked{{end}}{{if .AllowNotifications.Bool}}checked{{end}}>
<label for="switch-notifications">Allow notifications to be sent for this service</label>
<input type="hidden" name="allow_notifications" id="switch-notifications-value" value="{{if .AllowNotifications.Bool}}true{{else}}false{{end}}">
<input type="hidden" name="allow_notifications" id="switch-notifications-value" value="{{if eq .Id 0}}true{{else}}{{if .AllowNotifications.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
@ -116,7 +134,7 @@
<span class="switch float-left">
<input type="checkbox" name="public-option" class="switch" id="switch-public" {{if eq .Id 0}}checked{{else}}{{if .Public.Bool}}checked{{end}}{{end}}>
<label for="switch-public">Show service details to the public</label>
<input type="hidden" name="public" id="switch-public-value" value="{{if .Public.Bool}}true{{else}}false{{end}}">
<input type="hidden" name="public" id="switch-public-value" value="{{if eq .Id 0}}true{{else}}{{if .Public.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
@ -138,7 +156,7 @@
</div>
{{if ne .Id 0}}
<div class="col-6">
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
<a href="/service/{{ .Id }}/delete_failures" data-method="GET" data-redirect="/service/{{ .Id }}" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div>
{{end}}
</div>

17
source/tmpl/group.gohtml Normal file
View File

@ -0,0 +1,17 @@
{{define "title"}}{{.Name}} Status{{end}}
{{define "description"}}Group {{.Name}}{{end}}
{{ define "content" }}
{{$isAdmin := Auth}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if IsUser}}
{{template "nav"}}
{{end}}
<div class="col-12 mb-4">
{{template "form_group" .Group}}
</div>
</div>
{{end}}

View File

@ -13,7 +13,7 @@
<div class="col-12 full-col-12">
<h4 class="group_header">{{.Name}}</h4>
<div class="list-group online_list mb-3">
{{ range .Services }}
{{ range VisibleGroupServices . }}
<a href="#" class="service_li list-group-item list-group-item-action {{if not .Online}}bg-danger text-white{{ end }}" data-id="{{.Id}}">
{{ .Name }}
{{if .Online}}
@ -53,7 +53,7 @@
</div>
{{end}}
{{ range .Services }}
{{ range VisibleServices }}
{{$avgTime := .AvgTime}}
<div class="mb-4" id="service_id_{{.Id}}">
<div class="card">

View File

@ -1081,7 +1081,7 @@
"exec": [
"pm.test(\"View All Groups\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.length).to.eql(2);",
" pm.expect(jsonData.length).to.eql(3);",
"});"
],
"type": "text/javascript"
@ -1365,6 +1365,57 @@
}
]
},
{
"name": "Reorder Groups",
"event": [
{
"listen": "test",
"script": {
"id": "b5a67a19-fd08-40b0-a961-3e9474ab78c6",
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "[{\"group\":1,\"order\":1},{\"group\":2,\"order\":2}]"
},
"url": {
"raw": "{{endpoint}}/api/groups/reorder",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"groups",
"reorder"
]
},
"description": "Reorder services in a specific order for the index page."
},
"response": []
},
{
"name": "Delete Group",
"event": [

View File

@ -3,6 +3,7 @@
{{ define "content" }}
{{$s := .Service}}
{{$failures := $s.LimitedFailures 16}}
{{$incidents := $s.Incidents}}
{{$checkinFailures := $s.LimitedCheckinFailures 16}}
{{$isAdmin := Auth}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
@ -83,6 +84,7 @@
<nav class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs" role="serviceLists">
{{if $isAdmin}}<a class="flex-sm-fill text-sm-center nav-link active" id="edit-tab" data-toggle="tab" href="#edit" role="tab" aria-controls="edit" aria-selected="false">Edit Service</a>{{end}}
<a class="flex-sm-fill text-sm-center nav-link{{ if not $failures }} disabled{{end}}" id="failures-tab" data-toggle="tab" href="#failures" role="tab" aria-controls="failures" aria-selected="true">Failures</a>
<a class="flex-sm-fill text-sm-center nav-link{{ if not $incidents }} disabled{{end}}" id="incidents-tab" data-toggle="tab" href="#incidents" role="tab" aria-controls="incidents" aria-selected="true">Incidents</a>
{{if $isAdmin}}<a class="flex-sm-fill text-sm-center nav-link" id="checkins-tab" data-toggle="tab" href="#checkins" role="tab" aria-controls="checkins" aria-selected="false">Checkins</a>{{end}}
<a class="flex-sm-fill text-sm-center nav-link{{if not $isAdmin}} active{{end}}" id="response-tab" data-toggle="tab" href="#response" role="tab" aria-controls="response" aria-selected="false">Response</a>
</nav>
@ -104,6 +106,36 @@
{{ end }}
</div>
{{end}}
<div class="tab-pane fade" id="incidents" role="serviceLists" aria-labelledby="incidents-tab">
{{ if $incidents }}
<div class="list-group mt-3 mb-4">
{{ range $incidents }}
<div class="list-group-item flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.Title}}</h5>
<small>{{.CreatedAt}}</small>
</div>
<p class="mb-1">{{.Description}}</p>
<ul class="list-group mt-3">
{{ range .AllUpdates }}
<li class="list-group-item">
<p>
<span class="badge badge-primary">{{.Type}}</span>
<span class="float-right">
{{.Message}}
<p class="text-muted text-right small">{{.CreatedAt}}</p>
</span>
</p>
</li>
{{end}}
</ul>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{if $isAdmin}}
<div class="tab-pane fade" id="checkins" role="serviceLists" aria-labelledby="checkins-tab">
{{if $s.AllCheckins}}
@ -303,11 +335,14 @@ async function RenderHeatmap() {
}
async function RenderChartLatency() {
options.fill.colors = {{if $s.Online}}["#48d338"]{{else}}["#dd3545"]{{end}};
options.stroke.colors = {{if $s.Online}}["#3aa82d"]{{else}}["#c23342"]{{end}};
let chart = new ApexCharts(document.querySelector("#service"), options);
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
}
$(document).ready(async function() {
let startDate = $("#service_start").flatpickr({
enableTime: false,
static: true,

View File

@ -11,7 +11,8 @@
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-md-table-cell">Status</th>
<th scope="col">Visibility</th>
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
<th scope="col" class="d-none d-md-table-cell">Group</th>
<th scope="col"></th>
</tr>
</thead>
@ -19,8 +20,11 @@
{{range .Services}}
<tr id="service_{{.Id}}" data-id="{{.Id}}">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span> {{.Name}}</td>
<td class="d-none d-md-table-cell">{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}}</td>
<td class="d-none d-md-table-cell">{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}}
<i class="toggle-service fas {{if .IsRunning}}fa-toggle-on text-success{{else}}fa-toggle-off text-muted{{end}}" data-online="{{if .IsRunning}}true{{else}}false{{end}}" data-id="{{.Id}}"></i>
</td>
<td class="d-none d-md-table-cell">{{if .Public.Bool}}<span class="badge badge-primary">PUBLIC</span>{{else}}<span class="badge badge-secondary">PRIVATE</span>{{end}}</td>
<td class="d-none d-md-table-cell">{{if ne .GroupId 0}}<span class="badge badge-secondary">{{(Group .GroupId).Name}}</span>{{end}}</td>
<td class="text-right">
<div class="btn-group">
<a href="/service/{{ServiceLink .}}" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> View</a>
@ -57,6 +61,7 @@
<td>{{if .Public.Bool}}<span class="badge badge-primary">PUBLIC</span>{{else}}<span class="badge badge-secondary">PRIVATE</span>{{end}}</td>
<td class="text-right">
<div class="btn-group">
<a href="/group/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> Edit</a>
{{if Auth}}<a href="/api/groups/{{.Id}}" class="ajax_delete btn btn-danger" data-method="DELETE" data-obj="group_{{.Id}}" data-id="{{.Id}}"><i class="fas fa-times"></i></a>{{end}}
</div>
</td>
@ -79,6 +84,7 @@
<script src="/js/sortable.min.js"></script>
{{end}}
<script>
// drag and drop sorting for Services
sortable('.sortable', {
forcePlaceholderSize: true,
hoverClass: 'sortable_drag',
@ -95,14 +101,16 @@
newOrder.push(o);
});
$.ajax({
url: "/api/services/reorder",
type: 'POST',
url: "/api/reorder/services",
type: "POST",
data: JSON.stringify(newOrder),
contentType: "application/json",
dataType: "json",
success: function(data) { }
});
});
// drag and drop sorting for Groups
sortable('.sortable_groups', {
forcePlaceholderSize: true,
hoverClass: 'sortable_drag',
@ -119,8 +127,8 @@
newOrder.push(o);
});
$.ajax({
url: "/api/groups/reorder",
type: 'POST',
url: "/api/reorder/groups",
type: "POST",
data: JSON.stringify(newOrder),
contentType: "application/json",
dataType: "json",

View File

@ -37,6 +37,20 @@
<input type="text" name="description" class="form-control" value="{{ .Description }}" id="description" placeholder="Great Uptime">
</div>
<div class="form-group">
<div class="col-4 col-sm-4 mt-sm-1 mt-0">
<label for="update_notify" class="d-inline d-sm-none">Send Updates only</label>
<label for="update_notify" class="d-none d-sm-block">Send Updates only</label>
<span class="switch">
<input type="checkbox" name="update_notify-option" class="switch" id="switch-update_notify"{{if UPDATENOTIFY}} checked{{end}}>
<label for="switch-update_notify" class="mt-2 mt-sm-0"></label>
</span>
<input type="hidden" name="update_notify" id="switch-update_notify-value" value="{{if UPDATENOTIFY}}true{{else}}false{{end}}">
</div>
</div>
<div class="form-group row">
<div class="col-8 col-sm-9">
<label for="domain">Domain</label>
@ -55,7 +69,7 @@
{{if not .Domain}}
<div class="alert alert-danger" role="alert">
Your Statup server does not have a dedicated URL!
Your Statping server does not have a dedicated URL!
</div>
{{end}}
@ -123,14 +137,31 @@
</form>
<h3>Additional Settings</h3>
<h3 class="mt-4">Bulk Import Services</h3>
You can import multiple services based on a CSV file with the format shown on the <a href="https://github.com/hunterlong/statping/wiki/Bulk-Import-Services" target="_blank">Bulk Import Wiki</a>.
<div class="card">
<div class="card-body">
<form action="/settings/bulk_import" method="POST" enctype="multipart/form-data" class="form-inline">
<div class="form-group col-10">
<input type="file" name="file" class="form-control-file" accept=".csv">
</div>
<div class="form-group">
<button type="submit" class="btn btn-outline-success right">Import</button>
</div>
</form>
</div>
</div>
<h3 class="mt-4">Additional Settings</h3>
<div class="row">
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a>
<div class="col-12">
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a>
{{if .Domain}}
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
{{end}}
</div>
{{if .Domain}}
<div class="row align-content-center">
@ -139,6 +170,8 @@
<a class="btn btn-sm btn-primary" href={{safeURL QrAuth}}>Open in Statping App</a>
{{end}}
</div>
</div>
</div>
<div class="tab-pane" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,7 @@ type Core struct {
Version string `gorm:"column:version" json:"version"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
UpdateNotify NullBool `gorm:"column:update_notify;default:false" json:"update_notify,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

24
types/incident.go Normal file
View File

@ -0,0 +1,24 @@
package types
import "time"
// Incident is the main struct for Incidents
type Incident struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Title string `gorm:"column:title" json:"title,omitempty"`
Description string `gorm:"column:description" json:"description,omitempty"`
ServiceId int64 `gorm:"index;column:service" json:"service"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"`
Updates []*IncidentUpdate `gorm:"-" json:"updates,omitempty"`
}
// IncidentUpdate contains updates based on a Incident
type IncidentUpdate struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
IncidentId int64 `gorm:"index;column:incident" json:"-"`
Message string `gorm:"column:message" json:"message,omitempty"`
Type string `gorm:"column:type" json:"type,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"`
}

View File

@ -34,8 +34,10 @@ type Service struct {
Timeout int `gorm:"default:30;column:timeout" json:"timeout"`
Order int `gorm:"default:0;column:order_id" json:"order_id"`
AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications"`
VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl"`
Public NullBool `gorm:"default:true;column:public" json:"public"`
GroupId int `gorm:"default:0;column:group_id" json:"group_id"`
Headers NullString `gorm:"column:headers" json:"headers"`
Permalink NullString `gorm:"column:permalink" json:"permalink"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
@ -48,6 +50,10 @@ type Service struct {
Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"`
LastResponse string `gorm:"-" json:"-"`
UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime
UpdateNotify bool `gorm:"-" json:"-"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool`
DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_success"`
Failures []FailureInterface `gorm:"-" json:"failures,omitempty"`

View File

@ -20,11 +20,10 @@ import (
)
const (
TIME_NANO = "2006-01-02T15:04:05Z"
TIME = "2006-01-02 15:04:05"
POSTGRES_TIME = "2006-01-02 15:04"
CHART_TIME = "2006-01-02T15:04:05.999999-07:00"
TIME_DAY = "2006-01-02"
TIME_NANO = "2006-01-02T15:04:05Z"
TIME = "2006-01-02 15:04:05"
CHART_TIME = "2006-01-02T15:04:05.999999-07:00"
TIME_DAY = "2006-01-02"
)
var (

View File

@ -91,6 +91,9 @@ func rotate() {
// Log creates a new entry in the Logger. Log has 1-5 levels depending on how critical the log/error is
func Log(level int, err interface{}) error {
if disableLogs {
return nil
}
pushLastLine(err)
var outErr error
switch level {

View File

@ -16,15 +16,20 @@
package utils
import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/ararog/timeago"
"io"
"io/ioutil"
"math"
"math/rand"
"net"
"net/http"
"os"
"os/exec"
"reflect"
"regexp"
"strconv"
"strings"
@ -33,7 +38,8 @@ import (
var (
// Directory returns the current path or the STATPING_DIR environment variable
Directory string
Directory string
disableLogs bool
)
// init will set the utils.Directory to the current running directory, or STATPING_DIR if it is set
@ -48,6 +54,8 @@ func init() {
}
Directory = dir
}
logger := os.Getenv("DISABLE_LOGS")
disableLogs, _ = strconv.ParseBool(logger)
}
// ToInt converts a int to a string
@ -78,6 +86,25 @@ func ToInt(s interface{}) int64 {
}
}
// ConvertInterface will take all the keys/values from an interface and replace all %type.Key from a string
// Input: {"name": "%service.Name", "domain": "%service.Domain"}
// Output: {"name": "Google DNS", "domain": "8.8.8.8"}
func ConvertInterface(in string, obj interface{}) string {
if reflect.ValueOf(obj).IsNil() {
return in
}
s := reflect.ValueOf(obj).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
find := strings.Split(fmt.Sprintf("%s.%v", typeOfT, typeOfT.Field(i).Name), ".")
find[1] = strings.ToLower(find[1])
key := strings.Join(find[1:], ".")
in = strings.ReplaceAll(in, fmt.Sprintf("%%%v", key), fmt.Sprintf("%v", f.Interface()))
}
return in
}
// ToString converts a int to a string
func ToString(s interface{}) string {
switch v := s.(type) {
@ -249,21 +276,8 @@ func SaveFile(filename string, data []byte) error {
// // body - The body or form data to send with HTTP request
// // timeout - Specific duration to timeout on. time.Duration(30 * time.Seconds)
// // You can use a HTTP Proxy if you HTTP_PROXY environment variable
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration) ([]byte, *http.Response, error) {
func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration, verifySSL bool) ([]byte, *http.Response, error) {
var err error
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
ResponseHeaderTimeout: timeout,
TLSHandshakeTimeout: timeout,
Proxy: http.ProxyFromEnvironment,
}
client := &http.Client{
Transport: transport,
Timeout: timeout,
}
var req *http.Request
if req, err = http.NewRequest(method, url, body); err != nil {
return nil, nil, err
@ -276,11 +290,41 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
keyVal := strings.Split(h, "=")
if len(keyVal) == 2 {
if keyVal[0] != "" && keyVal[1] != "" {
req.Header.Set(keyVal[0], keyVal[1])
if strings.ToLower(keyVal[0]) == "host" {
req.Host = strings.TrimSpace(keyVal[1])
} else {
req.Header.Set(keyVal[0], keyVal[1])
}
}
}
}
var resp *http.Response
dialer := &net.Dialer{
Timeout: timeout,
KeepAlive: timeout,
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !verifySSL,
ServerName: req.Host,
},
DisableKeepAlives: true,
ResponseHeaderTimeout: timeout,
TLSHandshakeTimeout: timeout,
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// redirect all connections to host specified in url
addr = strings.Split(req.URL.Host, ":")[0] + addr[strings.LastIndex(addr, ":"):]
return dialer.DialContext(ctx, network, addr)
},
}
client := &http.Client{
Transport: transport,
Timeout: timeout,
}
if resp, err = client.Do(req); err != nil {
return nil, resp, err
}
@ -288,3 +332,115 @@ func HttpRequest(url, method string, content interface{}, headers []string, body
contents, err := ioutil.ReadAll(resp.Body)
return contents, resp, err
}
const (
B = 0x100
N = 0x1000
BM = 0xff
)
func NewPerlin(alpha, beta float64, n int, seed int64) *Perlin {
return NewPerlinRandSource(alpha, beta, n, rand.NewSource(seed))
}
// Perlin is the noise generator
type Perlin struct {
alpha float64
beta float64
n int
p [B + B + 2]int
g3 [B + B + 2][3]float64
g2 [B + B + 2][2]float64
g1 [B + B + 2]float64
}
func NewPerlinRandSource(alpha, beta float64, n int, source rand.Source) *Perlin {
var p Perlin
var i int
p.alpha = alpha
p.beta = beta
p.n = n
r := rand.New(source)
for i = 0; i < B; i++ {
p.p[i] = i
p.g1[i] = float64((r.Int()%(B+B))-B) / B
for j := 0; j < 2; j++ {
p.g2[i][j] = float64((r.Int()%(B+B))-B) / B
}
normalize2(&p.g2[i])
}
for ; i > 0; i-- {
k := p.p[i]
j := r.Int() % B
p.p[i] = p.p[j]
p.p[j] = k
}
for i := 0; i < B+2; i++ {
p.p[B+i] = p.p[i]
p.g1[B+i] = p.g1[i]
for j := 0; j < 2; j++ {
p.g2[B+i][j] = p.g2[i][j]
}
for j := 0; j < 3; j++ {
p.g3[B+i][j] = p.g3[i][j]
}
}
return &p
}
func normalize2(v *[2]float64) {
s := math.Sqrt(v[0]*v[0] + v[1]*v[1])
v[0] = v[0] / s
v[1] = v[1] / s
}
func (p *Perlin) Noise1D(x float64) float64 {
var scale float64 = 1
var sum float64
px := x
for i := 0; i < p.n; i++ {
val := p.noise1(px)
sum += val / scale
scale *= p.alpha
px *= p.beta
}
if sum < 0 {
sum = sum * -1
}
return sum
}
func (p *Perlin) noise1(arg float64) float64 {
var vec [1]float64
vec[0] = arg
t := vec[0] + N
bx0 := int(t) & BM
bx1 := (bx0 + 1) & BM
rx0 := t - float64(int(t))
rx1 := rx0 - 1.
sx := sCurve(rx0)
u := rx0 * p.g1[p.p[bx0]]
v := rx1 * p.g1[p.p[bx1]]
return lerp(sx, u, v)
}
func sCurve(t float64) float64 {
return t * t * (3. - 2.*t)
}
func lerp(t, a, b float64) float64 {
return a + t*(b-a)
}

View File

@ -24,6 +24,17 @@ import (
"time"
)
func TestConvertInterface(t *testing.T) {
type Service struct {
Name string
Domain string
}
sample := `{"name": "%service.Name", "domain": "%service.Domain"}`
input := &Service{"Test Name", "statping.com"}
out := ConvertInterface(sample, input)
assert.Equal(t, `{"name": "Test Name", "domain": "statping.com"}`, out)
}
func TestCreateLog(t *testing.T) {
err := createLog(Directory)
assert.Nil(t, err)

View File

@ -1 +1 @@
0.80.49
0.80.64