diff --git a/cli/main.go b/cli/main.go index fe92dc8..c394621 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,31 +1,105 @@ package main import ( - "time" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" cachet "github.com/castawaylabs/cachet-monitor" ) +var configPath string +var systemName string +var logPath string + func main() { - cachet.New() - config := cachet.Config - log := cachet.Logger + 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() - log.Printf("System: %s, Interval: %d second(s), API: %s\n", config.SystemName, config.Interval, config.APIUrl) - log.Printf("Starting %d monitors:\n", len(config.Monitors)) - for _, mon := range config.Monitors { - 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) - } + cfg, err := getConfiguration(configPath) + if err != nil { + panic(err) } - log.Println() - - ticker := time.NewTicker(time.Duration(config.Interval) * time.Second) - for range ticker.C { - for _, mon := range config.Monitors { - go mon.Run() - } + if len(systemName) > 0 { + cfg.SystemName = systemName } + 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 } diff --git a/component.go b/component.go deleted file mode 100644 index 1e0a95c..0000000 --- a/component.go +++ /dev/null @@ -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"` -} diff --git a/config.go b/config.go index 68e181c..b0dc9fa 100644 --- a/config.go +++ b/config.go @@ -1,117 +1,47 @@ package cachet import ( - "encoding/json" "errors" - "flag" - "fmt" - "io" - "io/ioutil" "log" "net" - "net/http" - "net/url" "os" ) -// Static config -var Config CachetConfig +type CachetMonitor struct { + Logger *log.Logger `json:"-"` -// Central logger -var Logger *log.Logger + APIUrl string `json:"api_url"` + 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 -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"` + Monitors []*Monitor `json:"monitors"` } -func New() error { - var configPath string - var systemName string - 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!") - } +func (mon *CachetMonitor) ValidateConfiguration() error { + if mon.Logger == nil { + mon.Logger = log.New(os.Stdout, "", log.Llongfile|log.Ldate|log.Ltime) } - if err := json.Unmarshal(data, &Config); err != nil { - return errors.New("Cannot parse config!") - } - - if len(systemName) > 0 { - Config.SystemName = systemName - } - if len(Config.SystemName) == 0 { + if len(mon.SystemName) == 0 { // get hostname - Config.SystemName = getHostname() - } - if Config.Interval <= 0 { - Config.Interval = 60 + mon.SystemName = getHostname() } - if len(os.Getenv("CACHET_API")) > 0 { - Config.APIUrl = os.Getenv("CACHET_API") - } - if len(os.Getenv("CACHET_TOKEN")) > 0 { - Config.APIToken = os.Getenv("CACHET_TOKEN") + if mon.Interval <= 0 { + mon.Interval = 60 } - if len(Config.APIToken) == 0 || len(Config.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") + 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") } - if len(Config.Monitors) == 0 { - return errors.New("No monitors defined!\nSee sample configuration: https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json\n") + 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") } - 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 } diff --git a/http.go b/http.go new file mode 100644 index 0000000..f6b5b01 --- /dev/null +++ b/http.go @@ -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 +} diff --git a/incident.go b/incident.go index f02bab1..00077ce 100644 --- a/incident.go +++ b/incident.go @@ -2,6 +2,8 @@ package cachet import ( "encoding/json" + "errors" + "fmt" "strconv" ) @@ -18,36 +20,26 @@ type Incident struct { 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 -func GetIncidents() []Incident { - _, body, err := makeRequest("GET", "/incidents", nil) +func (monitor *CachetMonitor) GetIncidents() ([]Incident, error) { + _, body, err := monitor.makeRequest("GET", "/incidents", nil) if err != nil { - Logger.Printf("Cannot get incidents: %v\n", err) - return []Incident{} + return []Incident{}, fmt.Errorf("Cannot get incidents: %v\n", err) } - var data IncidentList + var data struct { + Incidents []Incident `json:"data"` + } err = json.Unmarshal(body, &data) if err != nil { - Logger.Printf("Cannot parse incidents: %v\n", err) - panic(err) + return []Incident{}, fmt.Errorf("Cannot parse incidents: %v\n", err) } - return data.Incidents + return data.Incidents, nil } // Send - Create or Update incident -func (incident *Incident) Send() { +func (monitor *CachetMonitor) SendIncident(incident *Incident) error { jsonBytes, _ := json.Marshal(map[string]interface{}{ "name": incident.Name, "message": incident.Message, @@ -63,58 +55,57 @@ func (incident *Incident) Send() { requestURL += "/" + string(incident.ID) } - resp, body, err := makeRequest(requestType, requestURL, jsonBytes) + resp, body, err := monitor.makeRequest(requestType, requestURL, jsonBytes) if err != nil { - Logger.Printf("Cannot create/update incident: %v\n", err) - return + return err } - Logger.Println(strconv.Itoa(resp.StatusCode) + " " + string(body)) - - var data IncidentData - err = json.Unmarshal(body, &data) - if err != nil { - Logger.Println("Cannot parse incident body.", string(body)) - panic(err) + var data struct { + Incident Incident `json:"data"` + } + if err := json.Unmarshal(body, &data); err != nil { + return errors.New("Cannot parse incident body." + string(body)) } else { incident.ID = data.Incident.ID incident.Component = data.Incident.Component } 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 } -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 { - return + return nil } if incident.Component == nil { // fetch component - if err := incident.fetchComponent(); err != nil { - Logger.Printf("Cannot fetch component for incident. %v\n", err) - return + component, err := monitor.fetchComponent(string(*incident.ComponentID)) + if err != nil { + return fmt.Errorf("Cannot fetch component for incident. %v\n", err) } + + incident.Component = component } status, _ := strconv.Atoi(string(incident.Status)) @@ -133,11 +124,12 @@ func (incident *Incident) UpdateComponent() { "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 { - Logger.Printf("Could not update component: (resp code %d) %v", resp.StatusCode, err) - return + return fmt.Errorf("Could not update component: (resp code %d) %v", resp.StatusCode, err) } + + return nil } // SetInvestigating sets status to Investigating diff --git a/metrics.go b/metrics.go index ff21d58..25e4973 100644 --- a/metrics.go +++ b/metrics.go @@ -2,22 +2,24 @@ package cachet import ( "encoding/json" + "fmt" "strconv" ) // SendMetric sends lag metric point -func SendMetric(metricID int, delay int64) { +func (monitor *CachetMonitor) SendMetric(metricID int, delay int64) error { if metricID <= 0 { - return + return nil } jsonBytes, _ := json.Marshal(&map[string]interface{}{ "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 { - Logger.Printf("Could not log data point!\n%v\n", err) - return + return fmt.Errorf("Could not log data point!\n%v\n", err) } + + return nil } diff --git a/monitor.go b/monitor.go index 8f735c1..368dd57 100644 --- a/monitor.go +++ b/monitor.go @@ -23,6 +23,28 @@ type Monitor struct { History []bool `json:"-"` LastFailReason *string `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 @@ -38,7 +60,7 @@ func (monitor *Monitor) Run() { monitor.AnalyseData() 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 - 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 { // not enough data @@ -90,11 +112,11 @@ func (monitor *Monitor) AnalyseData() { if t > monitor.Threshold && monitor.Incident == nil { // is down, create an incident - Logger.Println("Creating incident...") + monitor.config.Logger.Println("Creating incident...") component_id := json.Number(strconv.Itoa(*monitor.ComponentID)) monitor.Incident = &Incident{ - Name: monitor.Name + " - " + Config.SystemName, + Name: monitor.Name + " - " + monitor.config.SystemName, Message: monitor.Name + " check failed", ComponentID: &component_id, } @@ -107,11 +129,11 @@ func (monitor *Monitor) AnalyseData() { monitor.Incident.SetInvestigating() // create/update incident - monitor.Incident.Send() - monitor.Incident.UpdateComponent() + monitor.config.SendIncident(monitor.Incident) + monitor.config.UpdateComponent(monitor.Incident) } else if t < monitor.Threshold && monitor.Incident != nil { // 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)) monitor.Incident = &Incident{ @@ -121,8 +143,8 @@ func (monitor *Monitor) AnalyseData() { } monitor.Incident.SetFixed() - monitor.Incident.Send() - monitor.Incident.UpdateComponent() + monitor.config.SendIncident(monitor.Incident) + monitor.config.UpdateComponent(monitor.Incident) monitor.Incident = nil } diff --git a/readme.md b/readme.md index f49ab61..e306379 100644 --- a/readme.md +++ b/readme.md @@ -70,7 +70,7 @@ Environment variables | ------------ | --------------------------- | --------------------------- | | CACHET_API | http://demo.cachethq.io/api | URL endpoint for cachet api | | CACHET_TOKEN | randomvalue | API Authentication token | -| DEVELOPMENT | 1 | Strips logging | +| CACHET_DEV | 1 | Strips logging | Vision and goals ---------------- diff --git a/request.go b/request.go deleted file mode 100644 index 40f92b6..0000000 --- a/request.go +++ /dev/null @@ -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 -}