Skip to content
Snippets Groups Projects
Commit 23260b5c authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Initial Commit

parents
No related branches found
No related tags found
No related merge requests found
/config.yml
/config.json
examples/
/.idea/
*.iml
/cachet-monitor
/.old
\ No newline at end of file
FROM golang:alpine as builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/cachet-monitor /cachet-monitor
ENTRYPOINT ["/cachet-monitor", "-config", "/config/monitoring.yaml"]
This diff is collapsed.
api.go 0 → 100644
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
)
type CachetResponse struct {
Meta MetaResponse `json:"meta,omitempty"`
Data json.RawMessage `json:"data"`
}
type MetaResponse struct {
}
func (api ConfigApi) Request(target interface{}, method string, url string, body interface{}) (MetaResponse, error) {
var err error
var encodedBody []byte
if body != nil {
encodedBody, err = json.Marshal(body)
if err != nil {
return MetaResponse{}, err
}
}
targetUri := api.Url+url
req, err := http.NewRequest(method, targetUri, bytes.NewBuffer(encodedBody))
if err != nil {
return MetaResponse{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Cachet-Token", api.Token)
transport := http.DefaultTransport.(*http.Transport)
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: api.Insecure}
client := &http.Client{
Transport: transport,
}
res, err := client.Do(req)
if err != nil {
return MetaResponse{}, err
}
var data CachetResponse
err = json.NewDecoder(res.Body).Decode(&data)
defer req.Body.Close()
if err != nil {
return MetaResponse{}, err
}
if res.StatusCode != 200 {
return MetaResponse{}, errors.New(fmt.Sprintf("API Responded with non-200 status code: %d", res.StatusCode))
}
if target != nil {
err = json.Unmarshal(data.Data, &target)
if err != nil {
return MetaResponse{}, err
}
}
return data.Meta, nil
}
package main
import "fmt"
import "encoding/json"
const (
ComponentStatusUnknown = 0
ComponentStatusOperational = 1
ComponentStatusPerformanceIssues = 2
ComponentStatusPartialOutage = 3
ComponentStatusMajorOutage = 4
)
type ApiComponent struct {
Id int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Link string `json:"link,omitempty"`
Status int `json:"status,omitempty"`
Order int `json:"order,omitempty"`
GroupId int `json:"group_id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
DeletedAt string `json:"deleted_at,omitempty"`
StatusName string `json:"status_name,omitempty"`
Tags json.RawMessage `json:"tags,omitempty"`
}
type ApiComponentBody struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Link string `json:"link,omitempty"`
Status int `json:"status,omitempty"`
Order int `json:"order,omitempty"`
GroupId int `json:"group_id,omitempty"`
StatusName string `json:"status_name,omitempty"`
Tags json.RawMessage `json:"tags,omitempty"`
}
type ApiComponentRepo struct {
parent ConfigApi
}
func (api ConfigApi) Components() ApiComponentRepo {
return ApiComponentRepo{
api,
}
}
func (repo ApiComponentRepo) Get(id int) (component ApiComponent, err error) {
_, err = repo.parent.Request(
&component,
"GET",
fmt.Sprintf("/components/%d", id),
nil,
)
return
}
func (repo ApiComponentRepo) Create(body ApiComponentBody) (component ApiComponent, err error) {
_, err = repo.parent.Request(
&component,
"POST",
"/components",
body,
)
return
}
func (repo ApiComponentRepo) Save(id int, body ApiComponentBody) (component ApiComponent, err error) {
_, err = repo.parent.Request(
&component,
"PUT",
fmt.Sprintf("/components/%d", id),
body,
)
return
}
func (repo ApiComponentRepo) Delete(id int) (err error) {
_, err = repo.parent.Request(
nil,
"DELETE",
fmt.Sprintf("/components/%d", id),
nil,
)
return
}
package main
type ApiGroup struct {
Id int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Order int `json:"order,omitempty"`
Collapsed int `json:"collapsed,omitempty"`
}
type ApiGroupBody struct {
Name string `json:"name,omitempty"`
Order int `json:"order,omitempty"`
Collapsed int `json:"collapsed,omitempty"`
}
type ApiGroupRepo struct {
parent ConfigApi
}
func (api ConfigApi) Groups() ApiGroupRepo {
return ApiGroupRepo{
api,
}
}
build.sh 0 → 100755
#!/bin/sh
IMAGE=k8r.eu/justjanne/cachet-monitor
TAGS=$(git describe --always --tags HEAD)
docker build -t $IMAGE:$TAGS .
docker tag $IMAGE:$TAGS $IMAGE:latest
echo Successfully tagged $IMAGE:latest
docker push $IMAGE:$TAGS
docker push $IMAGE:latest
\ No newline at end of file
package main
type Config struct {
Api ConfigApi `yaml:"api"`
Immediate bool `yaml:"immediate"`
Components []ConfigComponent `yaml:"components"`
}
type ConfigApi struct {
Url string `yaml:"url"`
Token string `yaml:"token"`
Insecure bool `yaml:"insecure,omitempty"`
}
type ConfigComponent struct {
Name string `yaml:"name"`
ComponentId int `yaml:"component,omitempty"`
MetricId int `yaml:"metric,omitempty"`
Interval string `yaml:"interval"`
LatencyThresholds struct {
Slow int `yaml:"slow,omitempty"`
Down int `yaml:"down,omitempty"`
} `yaml:"latency_thresholds,omitempty"`
OutageThresholds struct {
Performance int `yaml:"performance,omitempty"`
Partial int `yaml:"partial,omitempty"`
Major int `yaml:"major,omitempty"`
} `yaml:"outage_thresholds,omitempty"`
ThresholdDuration string `yaml:"threshold_duration"`
Type string `yaml:"type"`
Http struct {
Url string `yaml:"url,omitempty"`
Method string `yaml:"method,omitempty"`
Insecure bool `yaml:"insecure,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
Expected struct {
StatusCode int `yaml:"status_code,omitempty"`
Body string `yaml:"body,omitempty"`
} `yaml:"expected,omitempty"`
} `yaml:"http,omitempty"`
}
go.mod 0 → 100644
go.sum 0 → 100644
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20160216232012-784ddc588536/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
main.go 0 → 100644
package main
import (
"flag"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"os/signal"
"sync"
)
func setDefaultInt(target *int, defValue int) {
if *target == 0 {
*target = defValue
}
}
func setDefaultString(target *string, defValue string) {
if *target == "" {
*target = defValue
}
}
func main() {
configPath := flag.String("config", "config.yaml", "Path to config file")
flag.Parse()
var config Config
file, err := ioutil.ReadFile(*configPath)
if err != nil {
panic(err.Error())
}
err = yaml.Unmarshal(file, &config)
if err != nil {
panic(err.Error())
}
for index := range config.Components {
setDefaultString(&config.Components[index].Interval, "10s")
setDefaultInt(&config.Components[index].LatencyThresholds.Slow, 500)
setDefaultInt(&config.Components[index].LatencyThresholds.Down, 1000)
setDefaultInt(&config.Components[index].OutageThresholds.Performance, 50)
setDefaultInt(&config.Components[index].OutageThresholds.Partial, 65)
setDefaultInt(&config.Components[index].OutageThresholds.Major, 90)
setDefaultString(&config.Components[index].ThresholdDuration, "5m")
setDefaultString(&config.Components[index].Http.Method, "GET")
setDefaultInt(&config.Components[index].Http.Expected.StatusCode, 200)
}
storage := NewStorage()
wg := &sync.WaitGroup{}
var monitors []*MonitorWrapper
for index, component := range config.Components {
logrus.Infof("Starting Monitor #%d: %s", index, component.Name)
var monitor Monitor
if component.Type == "http" {
monitor = NewHttpMonitor(component)
}
wrapper := NewMonitorWrapper(&config.Api, &storage, monitor)
monitors = append(monitors, &wrapper)
go wrapper.ClockStart(config.Immediate, wg)
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, os.Kill)
<-signals
logrus.Warnf("Abort: Waiting monitors to finish")
for _, mon := range monitors {
mon.ClockStop()
}
wg.Wait()
}
package main
import (
"fmt"
"github.com/sirupsen/logrus"
"sync"
"time"
)
type Datapoint struct {
Time time.Time
Up bool
Latency int
}
func (d Datapoint) String() string {
return fmt.Sprintf("Datapoint{time=%s, up=%t, latency=%d}", d.Time.Format(time.RFC3339), d.Up, d.Latency)
}
type MonitorWrapper struct {
api *ConfigApi
storage *Storage
monitor Monitor
config *ConfigComponent
thresholdDuration time.Duration
// Closed when mon.Stop() is called
stopC chan bool
}
func NewMonitorWrapper(api *ConfigApi, storage *Storage, monitor Monitor) MonitorWrapper {
config := monitor.Config()
threshold, err := time.ParseDuration(config.ThresholdDuration)
if err != nil {
panic(err)
}
return MonitorWrapper{
api: api,
storage: storage,
monitor: monitor,
config: config,
thresholdDuration: threshold,
}
}
func (w *MonitorWrapper) Name() string {
return w.config.Name
}
func (w *MonitorWrapper) ClockStart(immediate bool, wg *sync.WaitGroup) {
wg.Add(1)
w.stopC = make(chan bool)
if immediate {
w.tick()
}
ticker := time.NewTicker(Interval(w.config))
for {
select {
case <-ticker.C:
w.tick()
case <-w.stopC:
wg.Done()
return
}
}
}
func (w *MonitorWrapper) tick() {
datapoint, err := w.monitor.Measure()
if err != nil {
logrus.Errorf("Monitor %s encountered error: %s\n", w.config.Name, err.Error())
return
}
status := DetermineStatus(w.config, w.storage.Store(w.config.ComponentId, datapoint, w.thresholdDuration))
_, err = w.api.Components().Save(w.config.ComponentId, ApiComponentBody{
Status: status,
})
if err != nil {
logrus.Error(err.Error())
}
}
func (w *MonitorWrapper) ClockStop() {
select {
case <-w.stopC:
return
default:
close(w.stopC)
}
}
type Monitor interface {
Config() *ConfigComponent
Measure() (Datapoint, error)
}
func Interval(component *ConfigComponent) time.Duration {
interval, err := time.ParseDuration(component.Interval)
if err != nil {
panic(err)
}
return interval
}
func DetermineStatus(component *ConfigComponent, datapoints []Datapoint) int {
countSlow := 0
countDown := 0
for _, datapoint := range datapoints {
if !datapoint.Up {
countDown++
} else if datapoint.Latency >= component.LatencyThresholds.Down {
countDown++
} else if datapoint.Latency >= component.LatencyThresholds.Slow {
countSlow++
}
}
percentageSlow := countSlow * 100 / len(datapoints)
percentageDown := countDown * 100 / len(datapoints)
if percentageDown > component.OutageThresholds.Major {
return ComponentStatusMajorOutage
} else if percentageDown > component.OutageThresholds.Partial {
return ComponentStatusPartialOutage
} else if percentageSlow > component.OutageThresholds.Performance {
return ComponentStatusPerformanceIssues
} else {
return ComponentStatusOperational
}
}
package main
import (
"crypto/tls"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"regexp"
"time"
)
type HttpMonitor struct {
config ConfigComponent
bodyRegexp *regexp.Regexp
}
func NewHttpMonitor(config ConfigComponent) *HttpMonitor {
bodyRegexp, _ := regexp.Compile(config.Http.Expected.Body)
return &HttpMonitor{
config: config,
bodyRegexp: bodyRegexp,
}
}
func (m *HttpMonitor) Config() *ConfigComponent {
return &m.config
}
func (m *HttpMonitor) Measure() (Datapoint, error) {
req, err := http.NewRequest(m.config.Http.Method, m.config.Http.Url, nil)
if err != nil {
return Datapoint{}, err
}
for k, v := range m.config.Http.Headers {
req.Header.Add(k, v)
}
transport := http.DefaultTransport.(*http.Transport)
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: m.config.Http.Insecure}
client := &http.Client{
Timeout: time.Duration(m.config.LatencyThresholds.Down) * time.Millisecond,
Transport: transport,
}
before := time.Now()
resp, err := client.Do(req)
after := time.Now()
latency := int(after.Sub(before).Nanoseconds() / 1000000)
if err != nil {
return Datapoint{}, err
}
defer resp.Body.Close()
if m.config.Http.Expected.StatusCode > 0 && resp.StatusCode != m.config.Http.Expected.StatusCode {
logrus.Infof("Monitor %s: Expected HTTP response status: %d, got: %d", m.config.Name, m.config.Http.Expected.StatusCode, resp.StatusCode)
return Datapoint{
Time: after,
Up: false,
Latency: latency,
}, nil
}
if m.bodyRegexp != nil {
// check response body
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Datapoint{}, err
}
if !m.bodyRegexp.Match(responseBody) {
logrus.Infof("Monitor %s: Unexpected body: %\nExpected to match: %s", m.config.Name, string(responseBody), m.config.Http.Expected.Body)
return Datapoint{
Time: after,
Up: false,
Latency: latency,
}, nil
}
}
return Datapoint{
Time: after,
Up: true,
Latency: latency,
}, nil
}
package main
import "time"
type Storage struct {
datapoints map[int][]Datapoint
}
func NewStorage() Storage {
return Storage{
datapoints: make(map[int][]Datapoint),
}
}
func (s Storage) Store(id int, datapoint Datapoint, duration time.Duration) []Datapoint {
// Find time of first datapoint we want to keep
now := time.Now()
timeThreshold := now.Add(-duration)
// Append datapoint
data := append(s.datapoints[id], datapoint)
// Find first datapoint that should be kept
minIdx := 0
for idx, elem := range data {
if elem.Time.After(timeThreshold) {
minIdx = idx
break
}
}
// Remove unwanted datapoints
data = data[minIdx:]
// Store datapoints
s.datapoints[id] = data
return data
}
util.go 0 → 100644
package main
import (
"os"
"strconv"
)
func EnvString(key string, defValue string) string {
data, valid := os.LookupEnv(key)
if valid {
return data
}
return defValue
}
func EnvFloat(key string, defValue float64) float64 {
data, err := strconv.ParseFloat(EnvString(key, ""), 64)
if err != nil {
return defValue
}
return data
}
func EnvInt(key string, defValue int64) int64 {
data, err := strconv.ParseInt(EnvString(key, ""), 10, 64)
if err != nil {
return defValue
}
return data
}
func EnvUInt(key string, defValue uint64) uint64 {
data, err := strconv.ParseUint(EnvString(key, ""), 10, 64)
if err != nil {
return defValue
}
return data
}
func EnvBool(key string, defValue bool) bool {
data, err := strconv.ParseBool(EnvString(key, ""))
if err != nil {
return defValue
}
return data
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment