diff --git a/api.go b/api.go index 9fedb9a..a8da867 100644 --- a/api.go +++ b/api.go @@ -3,9 +3,13 @@ package cachet import ( "bytes" "crypto/tls" + "encoding/json" "errors" - "io/ioutil" "net/http" + "strconv" + "time" + + "github.com/Sirupsen/logrus" ) type CachetAPI struct { @@ -14,6 +18,10 @@ type CachetAPI struct { Insecure bool `json:"insecure"` } +type CachetResponse struct { + Data json.RawMessage `json:"data"` +} + func (api CachetAPI) Ping() error { resp, _, err := api.NewRequest("GET", "/ping", nil) if err != nil { @@ -27,30 +35,43 @@ func (api CachetAPI) Ping() error { return nil } -func (api CachetAPI) NewRequest(requestType, url string, reqBody []byte) (*http.Response, []byte, error) { +// SendMetric adds a data point to a cachet monitor +func (api CachetAPI) SendMetric(id int, lag int64) { + logrus.Debugf("Sending lag metric ID:%d %vms", id, lag) + + jsonBytes, _ := json.Marshal(map[string]interface{}{ + "value": lag, + "timestamp": time.Now().Unix(), + }) + + resp, _, err := api.NewRequest("POST", "/metrics/"+strconv.Itoa(id)+"/points", jsonBytes) + if err != nil || resp.StatusCode != 200 { + logrus.Warnf("Could not log metric! ID: %d, err: %v", id, err) + } +} + +// NewRequest wraps http.NewRequest +func (api CachetAPI) NewRequest(requestType, url string, reqBody []byte) (*http.Response, CachetResponse, error) { req, err := http.NewRequest(requestType, api.URL+url, bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Cachet-Token", api.Token) - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - } - if api.Insecure { - transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - + 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 nil, []byte{}, err + return nil, CachetResponse{}, err } - defer res.Body.Close() - body, _ := ioutil.ReadAll(res.Body) + var body struct { + Data json.RawMessage `json:"data"` + } + err = json.NewDecoder(res.Body).Decode(&body) - return res, body, nil + return res, body, err } diff --git a/cli/main.go b/cli/main.go index 775a315..4385031 100644 --- a/cli/main.go +++ b/cli/main.go @@ -92,10 +92,10 @@ func main() { wg := &sync.WaitGroup{} for index, monitor := range cfg.Monitors { - logrus.Infof("Starting Monitor #%d:", index) + logrus.Infof("Starting Monitor #%d: ", index) logrus.Infof("Features: \n - %v", strings.Join(monitor.Describe(), "\n - ")) - go monitor.ClockStart(cfg, wg) + go monitor.ClockStart(cfg, monitor, wg) } signals := make(chan os.Signal, 1) @@ -164,6 +164,7 @@ func getConfiguration(path string) (*cachet.CachetMonitor, error) { var t cachet.MonitorInterface var err error + // get default type monType := cachet.GetMonitorType("") if t, ok := rawMonitor["type"].(string); ok { monType = cachet.GetMonitorType(t) @@ -175,11 +176,17 @@ func getConfiguration(path string) (*cachet.CachetMonitor, error) { err = mapstructure.Decode(rawMonitor, &s) t = &s case "dns": - // t = cachet.DNSMonitor + var s cachet.DNSMonitor + err = mapstructure.Decode(rawMonitor, &s) + t = &s case "icmp": - // t = cachet.ICMPMonitor + var s cachet.ICMPMonitor + err = mapstructure.Decode(rawMonitor, &s) + t = &s case "tcp": - // t = cachet.TCPMonitor + var s cachet.TCPMonitor + err = mapstructure.Decode(rawMonitor, &s) + t = &s default: logrus.Errorf("Invalid monitor type (index: %d) %v", index, monType) continue diff --git a/dns.go b/dns.go index 40825b1..8f303d6 100644 --- a/dns.go +++ b/dns.go @@ -1,3 +1,5 @@ package cachet -type DNSMonitor struct{} +type DNSMonitor struct { + AbstractMonitor `mapstructure:",squash"` +} diff --git a/http.go b/http.go index 1d4df2a..746693b 100644 --- a/http.go +++ b/http.go @@ -10,23 +10,23 @@ import ( "time" ) -// // Investigating template -// var HTTPTemplate = MessageTemplate{ -// Subject: `{{ .Name }} - {{ .config.SystemName }}`, -// Message: `{{ .Name }} check **failed** - {{ .now }} +// Investigating template +var defaultHTTPInvestigatingTpl = MessageTemplate{ + Subject: `{{ .Name }} - {{ .config.SystemName }}`, + Message: `{{ .Name }} check **failed** - {{ .now }} -// {{ .lastFailReason }}`, -// } +{{ .lastFailReason }}`, +} -// // Fixed template -// var HTTPTemplate = MessageTemplate{ -// Subject: `{{ .Name }} - {{ .config.SystemName }}`, -// Message: `**Resolved** - {{ .now }} +// Fixed template +var defaultHTTPFixedTpl = MessageTemplate{ + Subject: `{{ .Name }} - {{ .config.SystemName }}`, + Message: `**Resolved** - {{ .now }} -// - - - +- - - -// {{ .incident.Message }}`, -// } +{{ .incident.Message }}`, +} type HTTPMonitor struct { AbstractMonitor `mapstructure:",squash"` @@ -41,24 +41,21 @@ type HTTPMonitor struct { } func (monitor *HTTPMonitor) test() bool { - client := &http.Client{ - Timeout: time.Duration(monitor.Timeout * time.Second), - } - if monitor.Strict == false { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - req, err := http.NewRequest(monitor.Method, monitor.Target, nil) for k, v := range monitor.Headers { req.Header.Add(k, v) } + transport := http.DefaultTransport.(*http.Transport) + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: monitor.Strict == false} + client := &http.Client{ + Timeout: time.Duration(monitor.Timeout * time.Second), + Transport: transport, + } + resp, err := client.Do(req) if err != nil { monitor.lastFailReason = err.Error() - return false } @@ -66,7 +63,6 @@ func (monitor *HTTPMonitor) test() bool { if monitor.ExpectedStatusCode > 0 && resp.StatusCode != monitor.ExpectedStatusCode { monitor.lastFailReason = "Unexpected response code: " + strconv.Itoa(resp.StatusCode) + ". Expected " + strconv.Itoa(monitor.ExpectedStatusCode) - return false } @@ -75,7 +71,6 @@ func (monitor *HTTPMonitor) test() bool { responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { monitor.lastFailReason = err.Error() - return false } @@ -91,6 +86,9 @@ func (monitor *HTTPMonitor) test() bool { } func (mon *HTTPMonitor) Validate() []string { + mon.Template.Investigating.SetDefault(defaultHTTPInvestigatingTpl) + mon.Template.Fixed.SetDefault(defaultHTTPFixedTpl) + errs := mon.AbstractMonitor.Validate() if len(mon.ExpectedBody) > 0 { @@ -125,22 +123,3 @@ func (mon *HTTPMonitor) Describe() []string { return features } - -// SendMetric sends lag metric point -/*func (monitor *Monitor) SendMetric(delay int64) error { - if monitor.MetricID == 0 { - return nil - } - - jsonBytes, _ := json.Marshal(&map[string]interface{}{ - "value": delay, - }) - - resp, _, err := monitor.config.makeRequest("POST", "/metrics/"+strconv.Itoa(monitor.MetricID)+"/points", jsonBytes) - if err != nil || resp.StatusCode != 200 { - return fmt.Errorf("Could not log data point!\n%v\n", err) - } - - return nil -} -*/ diff --git a/icmp.go b/icmp.go index de12ea3..a2a9a41 100644 --- a/icmp.go +++ b/icmp.go @@ -1,3 +1,5 @@ package cachet -type ICMPMonitor struct{} +type ICMPMonitor struct { + AbstractMonitor `mapstructure:",squash"` +} diff --git a/incident.go b/incident.go index fd22b67..1949bf5 100644 --- a/incident.go +++ b/incident.go @@ -57,15 +57,13 @@ func (incident *Incident) Send(cfg *CachetMonitor) error { } var data struct { - Incident struct { - ID int `json:"id"` - } `json:"data"` + ID int `json:"id"` } - if err := json.Unmarshal(body, &data); err != nil { - return fmt.Errorf("Cannot parse incident body: %v, %v", err, string(body)) + if err := json.Unmarshal(body.Data, &data); err != nil { + return fmt.Errorf("Cannot parse incident body: %v, %v", err, string(body.Data)) } - incident.ID = data.Incident.ID + incident.ID = data.ID if resp.StatusCode != 200 { return fmt.Errorf("Could not create/update incident!") } @@ -84,15 +82,13 @@ func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) { } var data struct { - Component struct { - Status int `json:"status"` - } `json:"data"` + Status int `json:"status"` } - if err := json.Unmarshal(body, &data); err != nil { - return 0, fmt.Errorf("Cannot parse component body: %v. Err = %v", string(body), err) + if err := json.Unmarshal(body.Data, &data); err != nil { + return 0, fmt.Errorf("Cannot parse component body: %v. Err = %v", string(body.Data), err) } - return data.Component.Status, nil + return data.Status, nil } // SetInvestigating sets status to Investigating diff --git a/monitor.go b/monitor.go index 4853bcc..f969302 100644 --- a/monitor.go +++ b/monitor.go @@ -13,9 +13,9 @@ const DefaultTimeFormat = "15:04:05 Jan 2 MST" const HistorySize = 10 type MonitorInterface interface { - ClockStart(*CachetMonitor, *sync.WaitGroup) + ClockStart(*CachetMonitor, MonitorInterface, *sync.WaitGroup) ClockStop() - tick() + tick(MonitorInterface) test() bool Validate() []string @@ -70,6 +70,10 @@ func (mon *AbstractMonitor) Validate() []string { mon.Timeout = DefaultTimeout } + if mon.Timeout > mon.Interval { + errs = append(errs, "Timeout greater than interval") + } + if mon.ComponentID == 0 && mon.MetricID == 0 { errs = append(errs, "component_id & metric_id are unset") } @@ -78,6 +82,10 @@ func (mon *AbstractMonitor) Validate() []string { mon.Threshold = 100 } + if err := mon.Template.Fixed.Compile(); err != nil { + errs = append(errs, "Could not compile template: "+err.Error()) + } + return errs } func (mon *AbstractMonitor) GetMonitor() *AbstractMonitor { @@ -93,19 +101,19 @@ func (mon *AbstractMonitor) Describe() []string { return features } -func (mon *AbstractMonitor) ClockStart(cfg *CachetMonitor, wg *sync.WaitGroup) { +func (mon *AbstractMonitor) ClockStart(cfg *CachetMonitor, iface MonitorInterface, wg *sync.WaitGroup) { wg.Add(1) mon.config = cfg mon.stopC = make(chan bool) if cfg.Immediate { - mon.tick() + mon.tick(iface) } ticker := time.NewTicker(mon.Interval * time.Second) for { select { case <-ticker.C: - mon.tick() + mon.tick(iface) case <-mon.stopC: wg.Done() return @@ -124,9 +132,9 @@ func (mon *AbstractMonitor) ClockStop() { func (mon *AbstractMonitor) test() bool { return false } -func (mon *AbstractMonitor) tick() { +func (mon *AbstractMonitor) tick(iface MonitorInterface) { reqStart := getMs() - up := mon.test() + up := iface.test() lag := getMs() - reqStart if len(mon.history) == HistorySize-1 { @@ -139,9 +147,8 @@ func (mon *AbstractMonitor) tick() { mon.AnalyseData() // report lag - if up && mon.MetricID > 0 { - logrus.Infof("%v", lag) - // mon.SendMetric(lag) + if mon.MetricID > 0 { + go mon.config.API.SendMetric(mon.MetricID, lag) } } @@ -158,7 +165,7 @@ func (monitor *AbstractMonitor) AnalyseData() { t := (float32(numDown) / float32(len(monitor.history))) * 100 logrus.Printf("%s %.2f%%/%.2f%% down at %v\n", monitor.Name, t, monitor.Threshold, time.Now().UnixNano()/int64(time.Second)) - if len(monitor.history) != 10 { + if len(monitor.history) != HistorySize { // not saturated return } diff --git a/tcp.go b/tcp.go index 7be5db0..fa34f70 100644 --- a/tcp.go +++ b/tcp.go @@ -1,6 +1,8 @@ package cachet type TCPMonitor struct { + AbstractMonitor `mapstructure:",squash"` + // same as output from net.JoinHostPort // defaults to parsed config from /etc/resolv.conf when empty DNSServer string diff --git a/template.go b/template.go index 11ef04c..25f5131 100644 --- a/template.go +++ b/template.go @@ -1,6 +1,38 @@ package cachet +import "text/template" + type MessageTemplate struct { Subject string `json:"subject"` Message string `json:"message"` + + subjectTpl *template.Template + messageTpl *template.Template +} + +func (t *MessageTemplate) SetDefault(d MessageTemplate) { + if len(t.Subject) == 0 { + t.Subject = d.Subject + } + if len(t.Message) == 0 { + t.Message = d.Message + } +} + +func (t *MessageTemplate) Compile() error { + var err error + + if len(t.Subject) > 0 { + t.subjectTpl, err = compileTemplate(t.Subject) + } + + if err != nil && len(t.Message) > 0 { + t.messageTpl, err = compileTemplate(t.Message) + } + + return err +} + +func compileTemplate(text string) (*template.Template, error) { + return template.New("").Parse(text) }