Compare commits

...

16 Commits

Author SHA1 Message Date
Luther Wen Xu 99232eb17f
server: Fail early for non-existent service name
continuous-integration/drone/push Build is failing Details
2020-05-16 20:04:47 +07:00
Luther Wen Xu 2d158a5bd0
drone: Add caching on Build, Test, Lint pipeline
continuous-integration/drone/push Build is passing Details
2020-04-24 15:51:25 +07:00
Luther Wen Xu 3bb5187e39
server: Prevent nil map access crash
continuous-integration/drone/push Build is failing Details
2020-04-24 15:33:57 +07:00
Luther Wen Xu 62e4645b63
server: Implement service_name=all
continuous-integration/drone/push Build is passing Details
2020-04-23 23:32:10 +07:00
Luther Wen Xu 2dd0ad173b
server: Actually use caching
continuous-integration/drone/push Build is passing Details
2020-04-23 22:51:45 +07:00
Luther Wen Xu ed0e61cf79
server: Implement `interval` on /api/history/day
The default value is 5 for interval.
2020-04-23 22:32:56 +07:00
Luther Wen Xu 2560637c21
server: Implement /api/history/month and /api/history/year
continuous-integration/drone/push Build is passing Details
2020-04-23 21:53:08 +07:00
Luther Wen Xu 84501beb23
server: Implement /api/history/week 2020-04-23 21:44:23 +07:00
Luther Wen Xu 6d5d07e2e5
server: Add /api/history/day 2020-04-23 20:55:30 +07:00
Luther Wen Xu 237ce4ee2a
Drone: Add linter
continuous-integration/drone/push Build is passing Details
2020-04-23 16:31:34 +07:00
Luther Wen Xu 05395032be
config+server: Check for errors and handle them
continuous-integration/drone/push Build is passing Details
2020-04-23 16:28:31 +07:00
Luther Wen Xu 81e74807f4
server: Add HTTP server
continuous-integration/drone/push Build is passing Details
2020-04-23 15:42:16 +07:00
Luther Wen Xu 5f14a5b943
db: Allow empty service name in query 2020-04-23 15:41:57 +07:00
Luther Wen Xu f6a8187f4e
check: Add JSON marshalling 2020-04-23 15:41:02 +07:00
Luther Wen Xu 3e40b6ecdb
Dockerfile: Fix copy paste mistake
continuous-integration/drone/push Build is passing Details
2020-04-21 22:27:27 +07:00
Luther Wen Xu 64c096860a
Add CI and Dockerfile
continuous-integration/drone/push Build is passing Details
2020-04-21 21:54:30 +07:00
10 changed files with 344 additions and 13 deletions

@ -0,0 +1,68 @@
kind: pipeline
name: Build Application
steps:
- name: Build
image: golang:1.14
commands:
- go build ./...
volumes:
- name: deps
path: /go
- name: Test
image: golang:1.14
commands:
- go test ./...
volumes:
- name: deps
path: /go
- name: Lint
image: golangci/golangci-lint:v1.25.0
commands:
- golangci-lint run -v
volumes:
- name: deps
path: /go
volumes:
- name: deps
temp: {}
---
kind: pipeline
name: Build Docker Image
steps:
- name: Build Docker Image
image: plugins/docker
settings:
registry: docker.teamortix.com
username: droneci
password:
from_secret: docker_password
repo: docker.teamortix.com/teamortix/status
tags: test
purge: true
trigger:
event:
exclude:
- tag
depends_on:
- Build Application
---
kind: pipeline
name: Continuous Deployment
steps:
- name: Deploy Docker Image
image: plugins/docker
settings:
registry: docker.teamortix.com
username: droneci
password:
from_secret: docker_password
repo: docker.teamortix.com/teamortix/status
auto_tag: true
trigger:
event:
- tag
depends_on:
- Build Application

@ -0,0 +1,10 @@
FROM golang:1.14.2 AS build
WORKDIR /root/
COPY . .
RUN go build -o Status .
FROM golang:1.14.2
WORKDIR /root/
COPY --from=build /root/Status .
EXPOSE 8080
CMD ["./Status"]

@ -1,6 +1,7 @@
package check
import (
"encoding/json"
"time"
)
@ -32,3 +33,19 @@ func statusFromSuccessCount(count int) Result {
func divide(t time.Duration, dividend int) time.Duration {
return time.Duration(int(t) / dividend)
}
func (r Result) String() string {
switch r {
case Online:
return "online"
case Unstable:
return "unstable"
case Offline:
return "offline"
}
panic("Unexpected value")
}
func (r Result) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}

@ -7,6 +7,7 @@ import (
type Config struct {
Targets []checkTarget
Port int
}
func ReadConfig(file string) (Config, error) {
@ -15,6 +16,6 @@ func ReadConfig(file string) (Config, error) {
return Config{}, err
}
var result Config
json.Unmarshal(bytes, &result)
return result, nil
err = json.Unmarshal(bytes, &result)
return result, err
}

@ -3,18 +3,15 @@ package db
import (
"time"
"github.com/jinzhu/gorm"
"status/check"
)
//PingEntry represents an entry of Ping that is persisted in the DB.
type PingEntry struct {
gorm.Model
ServiceName string
Time time.Time
Ping int64
Status check.Result
ServiceName string `json:"service_name"`
Time time.Time `json:"query_time"`
Ping int64 `json:"ping"`
Status check.Result `json:"status"`
}
func (p PingEntry) AddToDB() {
@ -24,8 +21,12 @@ func (p PingEntry) AddToDB() {
db.Create(&p)
}
func GetFromDB(serviceName string, from, to time.Time) []*PingEntry {
entries := make([]*PingEntry, 0)
db.Where("service_name = ? AND time BETWEEN ? AND ?", serviceName, from, to).Find(&entries)
func GetFromDB(serviceName string, from, to time.Time) []PingEntry {
entries := make([]PingEntry, 0)
if serviceName == "" {
db.Where("time BETWEEN ? AND ?", from, to).Find(&entries)
} else {
db.Where("service_name = ? AND time BETWEEN ? AND ?", serviceName, from, to).Find(&entries)
}
return entries
}

@ -1,9 +1,18 @@
package main
import (
"status/server"
)
func main() {
cfg, err := ReadConfig("config.json")
if err != nil {
panic(err)
}
checkLoop(cfg.Targets)
go checkLoop(cfg.Targets)
validServices := make([]string, 0, len(cfg.Targets))
for _, v := range cfg.Targets {
validServices = append(validServices, v.ServiceName)
}
server.StartServer(cfg.Port, validServices)
}

@ -0,0 +1,102 @@
package server
import (
"encoding/json"
"net/http"
"strconv"
"time"
"status/db"
)
func writeJSON(w http.ResponseWriter, a interface{}) {
bytes, err := json.Marshal(a)
if err != nil {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(bytes)
if err != nil {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
}
}
func allStatus(w http.ResponseWriter, req *http.Request) {
status := getStatus()
writeJSON(w, status)
}
func dayHistory(w http.ResponseWriter, req *http.Request) {
keys, ok := req.URL.Query()["service_name"]
if !ok || len(keys) == 0 || len(keys[0]) < 1 {
http.Error(w, "`service_name` not provided", http.StatusBadRequest)
return
}
if len(keys) >= 2 {
http.Error(w, "Only one `service_name` allowed", http.StatusBadRequest)
return
}
service_name := keys[0]
if !validTarget[service_name] {
http.Error(w, "Invalid `service_name` requested", http.StatusBadRequest)
return
}
keys, ok = req.URL.Query()["interval"]
var interval = 5
if len(keys) >= 2 {
http.Error(w, "Only one `interval` allowed", http.StatusBadRequest)
return
}
if ok && len(keys) == 1 && len(keys[0]) >= 1 {
var err error
interval, err = strconv.Atoi(keys[0])
if err != nil || interval < 1 || interval > 1440 {
http.Error(w, "Invalid `interval` provided. Must be integer in [1, 1440]", http.StatusBadRequest)
return
}
}
if service_name == "all" {
service_name = ""
}
entries := db.GetFromDB(service_name, time.Now().Add(time.Duration(-24)*time.Hour), time.Now())
if interval == 1 {
writeJSON(w, entries)
return
}
filteredEntries := make([]db.PingEntry, 0, len(entries)/interval)
for i := 0; i < len(entries); i += interval {
filteredEntries = append(filteredEntries, entries[i])
}
writeJSON(w, filteredEntries)
}
func weekHistory(w http.ResponseWriter, req *http.Request) {
history(w, req, 7)
}
func monthHistory(w http.ResponseWriter, req *http.Request) {
history(w, req, 31)
}
func yearHistory(w http.ResponseWriter, req *http.Request) {
history(w, req, 366)
}
func history(w http.ResponseWriter, req *http.Request, dayCount int) {
keys, ok := req.URL.Query()["service_name"]
if !ok || len(keys) == 0 || len(keys[0]) < 1 {
http.Error(w, "`service_name` not provided", http.StatusBadRequest)
return
}
if len(keys) >= 2 {
http.Error(w, "Only one `service_name` allowed", http.StatusBadRequest)
return
}
if !validTarget[keys[0]] {
http.Error(w, "Invalid `service_name` requested", http.StatusBadRequest)
return
}
entries := getHistoryDay(keys[0], dayCount)
writeJSON(w, entries)
}

@ -0,0 +1,60 @@
package server
import (
"time"
"status/check"
"status/db"
)
type stat struct {
StartTime time.Time `json:"start_time"`
TotalPing int64 `json:"total_ping"`
SampleCount int `json:"sample_count"`
SuccessCount int `json:"success_count"`
FailCount int `json:"fail_count"`
Summary check.Result `json:"summary"`
}
var dayAggregation = make(map[string]map[time.Time]stat)
func convertToResult(successCount, totalCount int) check.Result {
if successCount >= totalCount*99/100 {
return check.Online
}
if successCount >= totalCount*9/10 {
return check.Unstable
}
return check.Offline
}
func getDay(serviceName string, day time.Time) stat {
if _, ok := dayAggregation[serviceName]; !ok {
dayAggregation[serviceName] = make(map[time.Time]stat)
}
if cached, ok := dayAggregation[serviceName][day]; ok {
return cached
}
entries := db.GetFromDB(serviceName, day.AddDate(0, 0, -1), day)
result := stat{
StartTime: day,
}
for _, v := range entries {
result.SampleCount++
result.TotalPing += v.Ping
switch v.Status {
case check.Online:
result.SuccessCount++
case check.Offline:
result.FailCount++
}
}
result.Summary = convertToResult(result.SuccessCount, result.SampleCount)
dayAggregation[serviceName][day] = result
return result
}
func today() time.Time {
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
}

@ -0,0 +1,33 @@
package server
import (
"time"
"status/db"
)
func getStatus() map[string]db.PingEntry {
entries := db.GetFromDB("", time.Now().Add(time.Duration(-2)*time.Minute), time.Now())
result := make(map[string]db.PingEntry)
for _, v := range entries {
if v.Time.Before(result[v.ServiceName].Time) {
continue
}
result[v.ServiceName] = v
}
return result
}
func getHistoryDay(serviceName string, dayCount int) []stat {
currentDay := today()
result := make([]stat, 0)
for i := 0; i >= -dayCount+1; i-- {
dayData := getDay(serviceName, currentDay.AddDate(0, 0, i))
if dayData.SampleCount > 0 {
result = append(result, dayData)
} else {
break
}
}
return result
}

@ -0,0 +1,30 @@
package server
import (
"fmt"
"net/http"
"strconv"
)
var (
validTarget = make(map[string]bool)
)
func apiEndpoints() {
http.HandleFunc("/api/latest", allStatus)
http.HandleFunc("/api/history/day", dayHistory)
http.HandleFunc("/api/history/week", weekHistory)
http.HandleFunc("/api/history/month", monthHistory)
http.HandleFunc("/api/history/year", yearHistory)
}
func StartServer(port int, validTargets []string) {
for _, v := range validTargets {
validTarget[v] = true
}
apiEndpoints()
err := http.ListenAndServe(":"+strconv.Itoa(port), nil)
if err != nil {
fmt.Printf("ListenAndServe returned an error: %v", err)
}
}