refactoring

This commit is contained in:
Matej Kramny
2016-05-19 12:20:56 +01:00
parent dfad6f0906
commit 2d62fc7443
9 changed files with 242 additions and 229 deletions

View File

@@ -1,31 +1,105 @@
package main package main
import ( import (
"time" "encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
cachet "github.com/castawaylabs/cachet-monitor" cachet "github.com/castawaylabs/cachet-monitor"
) )
var configPath string
var systemName string
var logPath string
func main() { func main() {
cachet.New() flag.StringVar(&configPath, "c", "/etc/cachet-monitor.config.json", "Config path")
config := cachet.Config flag.StringVar(&systemName, "name", "", "System Name")
log := cachet.Logger flag.StringVar(&logPath, "log", "", "Log path")
flag.Parse()
log.Printf("System: %s, Interval: %d second(s), API: %s\n", config.SystemName, config.Interval, config.APIUrl) cfg, err := getConfiguration(configPath)
log.Printf("Starting %d monitors:\n", len(config.Monitors)) if err != nil {
for _, mon := range config.Monitors { panic(err)
log.Printf(" %s: GET %s & Expect HTTP %d\n", mon.Name, mon.URL, mon.ExpectedStatusCode)
if mon.MetricID > 0 {
log.Printf(" - Logs lag to metric id: %d\n", mon.MetricID)
}
} }
log.Println() if len(systemName) > 0 {
cfg.SystemName = systemName
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
for range ticker.C {
for _, mon := range config.Monitors {
go mon.Run()
}
} }
if len(logPath) > 0 {
cfg.LogPath = logPath
}
if len(os.Getenv("CACHET_API")) > 0 {
cfg.APIUrl = os.Getenv("CACHET_API")
}
if len(os.Getenv("CACHET_TOKEN")) > 0 {
cfg.APIToken = os.Getenv("CACHET_TOKEN")
}
if err := cfg.ValidateConfiguration(); err != nil {
panic(err)
}
cfg.Run()
}
func getLogger(logPath string) *log.Logger {
var logWriter = os.Stdout
var err error
if len(logPath) > 0 {
logWriter, err = os.Create(logPath)
if err != nil {
fmt.Printf("Unable to open file '%v' for logging\n", logPath)
os.Exit(1)
}
}
flags := log.Llongfile | log.Ldate | log.Ltime
if len(os.Getenv("CACHET_DEV")) > 0 {
flags = 0
}
return log.New(logWriter, "", flags)
}
func getConfiguration(path string) (*cachet.CachetMonitor, error) {
var cfg cachet.CachetMonitor
var data []byte
// test if its a url
url, err := url.ParseRequestURI(path)
if err == nil && len(url.Scheme) > 0 {
// download config
response, err := http.Get(path)
if err != nil {
return nil, errors.New("Cannot download network config: " + err.Error())
}
defer response.Body.Close()
data, _ = ioutil.ReadAll(response.Body)
fmt.Println("Downloaded network configuration.")
} else {
data, err = ioutil.ReadFile(path)
if err != nil {
return nil, errors.New("Config file '" + path + "' missing!")
}
}
if err := json.Unmarshal(data, &cfg); err != nil {
fmt.Println(err)
return nil, errors.New("Cannot parse config!")
}
cfg.Logger = getLogger(cfg.LogPath)
return &cfg, nil
} }

View File

@@ -1,20 +0,0 @@
package cachet
import "encoding/json"
// Component Cachet model
type Component struct {
ID json.Number `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status json.Number `json:"status_id"`
HumanStatus string `json:"-"`
IncidentCount int `json:"-"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
}
// ComponentData json response model
type ComponentData struct {
Component Component `json:"data"`
}

110
config.go
View File

@@ -1,117 +1,47 @@
package cachet package cachet
import ( import (
"encoding/json"
"errors" "errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http"
"net/url"
"os" "os"
) )
// Static config type CachetMonitor struct {
var Config CachetConfig Logger *log.Logger `json:"-"`
// Central logger APIUrl string `json:"api_url"`
var Logger *log.Logger APIToken string `json:"api_token"`
Interval int64 `json:"interval"`
SystemName string `json:"system_name"`
LogPath string `json:"log_path"`
InsecureAPI bool `json:"insecure_api"`
// CachetConfig is the monitoring tool configuration Monitors []*Monitor `json:"monitors"`
type CachetConfig struct {
APIUrl string `json:"api_url"`
APIToken string `json:"api_token"`
Interval int64 `json:"interval"`
Monitors []*Monitor `json:"monitors"`
SystemName string `json:"system_name"`
LogPath string `json:"log_path"`
InsecureAPI bool `json:"insecure_api"`
} }
func New() error { func (mon *CachetMonitor) ValidateConfiguration() error {
var configPath string if mon.Logger == nil {
var systemName string mon.Logger = log.New(os.Stdout, "", log.Llongfile|log.Ldate|log.Ltime)
var logPath string
flag.StringVar(&configPath, "c", "/etc/cachet-monitor.config.json", "Config path")
flag.StringVar(&systemName, "name", "", "System Name")
flag.StringVar(&logPath, "log", "", "Log path")
flag.Parse()
var data []byte
// test if its a url
url, err := url.ParseRequestURI(configPath)
if err == nil && len(url.Scheme) > 0 {
// download config
response, err := http.Get(configPath)
if err != nil {
return errors.New("Cannot download network config: " + err.Error())
}
defer response.Body.Close()
data, _ = ioutil.ReadAll(response.Body)
fmt.Println("Downloaded network configuration.")
} else {
data, err = ioutil.ReadFile(configPath)
if err != nil {
return errors.New("Config file '" + configPath + "' missing!")
}
} }
if err := json.Unmarshal(data, &Config); err != nil { if len(mon.SystemName) == 0 {
return errors.New("Cannot parse config!")
}
if len(systemName) > 0 {
Config.SystemName = systemName
}
if len(Config.SystemName) == 0 {
// get hostname // get hostname
Config.SystemName = getHostname() mon.SystemName = getHostname()
}
if Config.Interval <= 0 {
Config.Interval = 60
} }
if len(os.Getenv("CACHET_API")) > 0 { if mon.Interval <= 0 {
Config.APIUrl = os.Getenv("CACHET_API") mon.Interval = 60
}
if len(os.Getenv("CACHET_TOKEN")) > 0 {
Config.APIToken = os.Getenv("CACHET_TOKEN")
} }
if len(Config.APIToken) == 0 || len(Config.APIUrl) == 0 { if len(mon.APIToken) == 0 || len(mon.APIUrl) == 0 {
return errors.New("API URL or API Token not set. cachet-monitor won't be able to report incidents.\n\nPlease set:\n CACHET_API and CACHET_TOKEN environment variable to override settings.\n\nGet help at https://github.com/CastawayLabs/cachet-monitor\n") return errors.New("API URL or API Token not set. cachet-monitor won't be able to report incidents.\n\nPlease set:\n CACHET_API and CACHET_TOKEN environment variable to override settings.\n\nGet help at https://github.com/castawaylabs/cachet-monitor\n")
} }
if len(Config.Monitors) == 0 { if len(mon.Monitors) == 0 {
return errors.New("No monitors defined!\nSee sample configuration: https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json\n") return errors.New("No monitors defined!\nSee sample configuration: https://github.com/castawaylabs/cachet-monitor/blob/master/example.config.json\n")
} }
if len(logPath) > 0 {
Config.LogPath = logPath
}
var logWriter io.Writer
logWriter = os.Stdout
if len(Config.LogPath) > 0 {
logWriter, err = os.Create(Config.LogPath)
if err != nil {
return errors.New("Unable to open file '" + Config.LogPath + "' for logging\n")
}
}
flags := log.Llongfile | log.Ldate | log.Ltime
if len(os.Getenv("DEVELOPMENT")) > 0 {
flags = 0
}
Logger = log.New(logWriter, "", flags)
return nil return nil
} }

45
http.go Normal file
View File

@@ -0,0 +1,45 @@
package cachet
import (
"bytes"
"crypto/tls"
"encoding/json"
"io/ioutil"
"net/http"
)
// Component Cachet model
type Component struct {
ID json.Number `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status json.Number `json:"status_id"`
HumanStatus string `json:"-"`
IncidentCount int `json:"-"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
}
func (monitor *CachetMonitor) makeRequest(requestType string, url string, reqBody []byte) (*http.Response, []byte, error) {
req, err := http.NewRequest(requestType, monitor.APIUrl+url, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Cachet-Token", monitor.APIToken)
client := &http.Client{}
if monitor.InsecureAPI == true {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
res, err := client.Do(req)
if err != nil {
return nil, []byte{}, err
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
return res, body, nil
}

View File

@@ -2,6 +2,8 @@ package cachet
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"strconv" "strconv"
) )
@@ -18,36 +20,26 @@ type Incident struct {
UpdatedAt *string `json:"updated_at"` UpdatedAt *string `json:"updated_at"`
} }
// IncidentData is a response when creating/updating an incident
type IncidentData struct {
Incident Incident `json:"data"`
}
// IncidentList - from API /incidents
type IncidentList struct {
Incidents []Incident `json:"data"`
}
// GetIncidents - Get list of incidents // GetIncidents - Get list of incidents
func GetIncidents() []Incident { func (monitor *CachetMonitor) GetIncidents() ([]Incident, error) {
_, body, err := makeRequest("GET", "/incidents", nil) _, body, err := monitor.makeRequest("GET", "/incidents", nil)
if err != nil { if err != nil {
Logger.Printf("Cannot get incidents: %v\n", err) return []Incident{}, fmt.Errorf("Cannot get incidents: %v\n", err)
return []Incident{}
} }
var data IncidentList var data struct {
Incidents []Incident `json:"data"`
}
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
if err != nil { if err != nil {
Logger.Printf("Cannot parse incidents: %v\n", err) return []Incident{}, fmt.Errorf("Cannot parse incidents: %v\n", err)
panic(err)
} }
return data.Incidents return data.Incidents, nil
} }
// Send - Create or Update incident // Send - Create or Update incident
func (incident *Incident) Send() { func (monitor *CachetMonitor) SendIncident(incident *Incident) error {
jsonBytes, _ := json.Marshal(map[string]interface{}{ jsonBytes, _ := json.Marshal(map[string]interface{}{
"name": incident.Name, "name": incident.Name,
"message": incident.Message, "message": incident.Message,
@@ -63,58 +55,57 @@ func (incident *Incident) Send() {
requestURL += "/" + string(incident.ID) requestURL += "/" + string(incident.ID)
} }
resp, body, err := makeRequest(requestType, requestURL, jsonBytes) resp, body, err := monitor.makeRequest(requestType, requestURL, jsonBytes)
if err != nil { if err != nil {
Logger.Printf("Cannot create/update incident: %v\n", err) return err
return
} }
Logger.Println(strconv.Itoa(resp.StatusCode) + " " + string(body)) var data struct {
Incident Incident `json:"data"`
var data IncidentData }
err = json.Unmarshal(body, &data) if err := json.Unmarshal(body, &data); err != nil {
if err != nil { return errors.New("Cannot parse incident body." + string(body))
Logger.Println("Cannot parse incident body.", string(body))
panic(err)
} else { } else {
incident.ID = data.Incident.ID incident.ID = data.Incident.ID
incident.Component = data.Incident.Component incident.Component = data.Incident.Component
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
Logger.Println("Could not create/update incident!") return errors.New("Could not create/update incident!")
} }
}
func (incident *Incident) fetchComponent() error {
_, body, err := makeRequest("GET", "/components/"+string(*incident.ComponentID), nil)
if err != nil {
return err
}
var data ComponentData
err = json.Unmarshal(body, &data)
if err != nil {
Logger.Println("Cannot parse component body. %v", string(body))
panic(err)
}
incident.Component = &data.Component
return nil return nil
} }
func (incident *Incident) UpdateComponent() { func (monitor *CachetMonitor) fetchComponent(componentID string) (*Component, error) {
_, body, err := monitor.makeRequest("GET", "/components/"+componentID, nil)
if err != nil {
return nil, err
}
var data struct {
Component Component `json:"data"`
}
if err := json.Unmarshal(body, &data); err != nil {
return nil, errors.New("Cannot parse component body. " + string(body))
}
return &data.Component, nil
}
func (monitor *CachetMonitor) UpdateComponent(incident *Incident) error {
if incident.ComponentID == nil || len(*incident.ComponentID) == 0 { if incident.ComponentID == nil || len(*incident.ComponentID) == 0 {
return return nil
} }
if incident.Component == nil { if incident.Component == nil {
// fetch component // fetch component
if err := incident.fetchComponent(); err != nil { component, err := monitor.fetchComponent(string(*incident.ComponentID))
Logger.Printf("Cannot fetch component for incident. %v\n", err) if err != nil {
return return fmt.Errorf("Cannot fetch component for incident. %v\n", err)
} }
incident.Component = component
} }
status, _ := strconv.Atoi(string(incident.Status)) status, _ := strconv.Atoi(string(incident.Status))
@@ -133,11 +124,12 @@ func (incident *Incident) UpdateComponent() {
"status": incident.Component.Status, "status": incident.Component.Status,
}) })
resp, _, err := makeRequest("PUT", "/components/"+string(incident.Component.ID), jsonBytes) resp, _, err := monitor.makeRequest("PUT", "/components/"+string(incident.Component.ID), jsonBytes)
if err != nil || resp.StatusCode != 200 { if err != nil || resp.StatusCode != 200 {
Logger.Printf("Could not update component: (resp code %d) %v", resp.StatusCode, err) return fmt.Errorf("Could not update component: (resp code %d) %v", resp.StatusCode, err)
return
} }
return nil
} }
// SetInvestigating sets status to Investigating // SetInvestigating sets status to Investigating

View File

@@ -2,22 +2,24 @@ package cachet
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv" "strconv"
) )
// SendMetric sends lag metric point // SendMetric sends lag metric point
func SendMetric(metricID int, delay int64) { func (monitor *CachetMonitor) SendMetric(metricID int, delay int64) error {
if metricID <= 0 { if metricID <= 0 {
return return nil
} }
jsonBytes, _ := json.Marshal(&map[string]interface{}{ jsonBytes, _ := json.Marshal(&map[string]interface{}{
"value": delay, "value": delay,
}) })
resp, _, err := makeRequest("POST", "/metrics/"+strconv.Itoa(metricID)+"/points", jsonBytes) resp, _, err := monitor.makeRequest("POST", "/metrics/"+strconv.Itoa(metricID)+"/points", jsonBytes)
if err != nil || resp.StatusCode != 200 { if err != nil || resp.StatusCode != 200 {
Logger.Printf("Could not log data point!\n%v\n", err) return fmt.Errorf("Could not log data point!\n%v\n", err)
return
} }
return nil
} }

View File

@@ -23,6 +23,28 @@ type Monitor struct {
History []bool `json:"-"` History []bool `json:"-"`
LastFailReason *string `json:"-"` LastFailReason *string `json:"-"`
Incident *Incident `json:"-"` Incident *Incident `json:"-"`
config *CachetMonitor
}
func (cfg *CachetMonitor) Run() {
cfg.Logger.Printf("System: %s\nInterval: %d second(s)\nAPI: %s\n\n", cfg.SystemName, cfg.Interval, cfg.APIUrl)
cfg.Logger.Printf("Starting %d monitors:\n", len(cfg.Monitors))
for _, mon := range cfg.Monitors {
cfg.Logger.Printf(" %s: GET %s & Expect HTTP %d\n", mon.Name, mon.URL, mon.ExpectedStatusCode)
if mon.MetricID > 0 {
cfg.Logger.Printf(" - Logs lag to metric id: %d\n", mon.MetricID)
}
}
cfg.Logger.Println()
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
for range ticker.C {
for _, mon := range cfg.Monitors {
mon.config = cfg
go mon.Run()
}
}
} }
// Run loop // Run loop
@@ -38,7 +60,7 @@ func (monitor *Monitor) Run() {
monitor.AnalyseData() monitor.AnalyseData()
if isUp == true && monitor.MetricID > 0 { if isUp == true && monitor.MetricID > 0 {
SendMetric(monitor.MetricID, lag) monitor.config.SendMetric(monitor.MetricID, lag)
} }
} }
@@ -81,7 +103,7 @@ func (monitor *Monitor) AnalyseData() {
} }
t := (float32(numDown) / float32(len(monitor.History))) * 100 t := (float32(numDown) / float32(len(monitor.History))) * 100
Logger.Printf("%s %.2f%% Down at %v. Threshold: %.2f%%\n", monitor.URL, t, time.Now().UnixNano()/int64(time.Second), monitor.Threshold) monitor.config.Logger.Printf("%s %.2f%% Down at %v. Threshold: %.2f%%\n", monitor.URL, t, time.Now().UnixNano()/int64(time.Second), monitor.Threshold)
if len(monitor.History) != 10 { if len(monitor.History) != 10 {
// not enough data // not enough data
@@ -90,11 +112,11 @@ func (monitor *Monitor) AnalyseData() {
if t > monitor.Threshold && monitor.Incident == nil { if t > monitor.Threshold && monitor.Incident == nil {
// is down, create an incident // is down, create an incident
Logger.Println("Creating incident...") monitor.config.Logger.Println("Creating incident...")
component_id := json.Number(strconv.Itoa(*monitor.ComponentID)) component_id := json.Number(strconv.Itoa(*monitor.ComponentID))
monitor.Incident = &Incident{ monitor.Incident = &Incident{
Name: monitor.Name + " - " + Config.SystemName, Name: monitor.Name + " - " + monitor.config.SystemName,
Message: monitor.Name + " check failed", Message: monitor.Name + " check failed",
ComponentID: &component_id, ComponentID: &component_id,
} }
@@ -107,11 +129,11 @@ func (monitor *Monitor) AnalyseData() {
monitor.Incident.SetInvestigating() monitor.Incident.SetInvestigating()
// create/update incident // create/update incident
monitor.Incident.Send() monitor.config.SendIncident(monitor.Incident)
monitor.Incident.UpdateComponent() monitor.config.UpdateComponent(monitor.Incident)
} else if t < monitor.Threshold && monitor.Incident != nil { } else if t < monitor.Threshold && monitor.Incident != nil {
// was down, created an incident, its now ok, make it resolved. // was down, created an incident, its now ok, make it resolved.
Logger.Println("Updating incident to resolved...") monitor.config.Logger.Println("Updating incident to resolved...")
component_id := json.Number(strconv.Itoa(*monitor.ComponentID)) component_id := json.Number(strconv.Itoa(*monitor.ComponentID))
monitor.Incident = &Incident{ monitor.Incident = &Incident{
@@ -121,8 +143,8 @@ func (monitor *Monitor) AnalyseData() {
} }
monitor.Incident.SetFixed() monitor.Incident.SetFixed()
monitor.Incident.Send() monitor.config.SendIncident(monitor.Incident)
monitor.Incident.UpdateComponent() monitor.config.UpdateComponent(monitor.Incident)
monitor.Incident = nil monitor.Incident = nil
} }

View File

@@ -70,7 +70,7 @@ Environment variables
| ------------ | --------------------------- | --------------------------- | | ------------ | --------------------------- | --------------------------- |
| CACHET_API | http://demo.cachethq.io/api | URL endpoint for cachet api | | CACHET_API | http://demo.cachethq.io/api | URL endpoint for cachet api |
| CACHET_TOKEN | randomvalue | API Authentication token | | CACHET_TOKEN | randomvalue | API Authentication token |
| DEVELOPMENT | 1 | Strips logging | | CACHET_DEV | 1 | Strips logging |
Vision and goals Vision and goals
---------------- ----------------

View File

@@ -1,32 +0,0 @@
package cachet
import (
"bytes"
"crypto/tls"
"io/ioutil"
"net/http"
)
func makeRequest(requestType string, url string, reqBody []byte) (*http.Response, []byte, error) {
req, err := http.NewRequest(requestType, Config.APIUrl+url, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Cachet-Token", Config.APIToken)
client := &http.Client{}
if Config.InsecureAPI == true {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
res, err := client.Do(req)
if err != nil {
return nil, []byte{}, err
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
return res, body, nil
}