Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab2264c7a | ||
|
|
021871b763 | ||
|
|
698781afec | ||
|
|
e6d8d31fa5 | ||
|
|
6a51993296 | ||
|
|
8aae002623 | ||
|
|
c04128ce36 | ||
|
|
1b93730121 | ||
|
|
85d92bcb07 | ||
|
|
0dc54e4e6e | ||
|
|
b3bc1d4405 | ||
|
|
b4fa33b8ad | ||
|
|
edfd4a51e6 | ||
|
|
a2d8128109 | ||
|
|
d43eca4b7d | ||
|
|
36bf228599 | ||
|
|
0cd6fa13a7 | ||
|
|
e910807973 | ||
|
|
9b29a0450c | ||
|
|
aaecc1669a | ||
|
|
48586eb0aa | ||
|
|
2c364f3d2f | ||
|
|
0de0baf5f9 | ||
|
|
3f4b9ced77 | ||
|
|
20e4dd1414 | ||
|
|
29b02fd164 | ||
|
|
5c4f0c2e69 | ||
|
|
9d0e176695 | ||
|
|
6e9f5440ba | ||
|
|
0ceccccd45 | ||
|
|
c729fbdf41 | ||
|
|
30eea191d3 | ||
|
|
e0556b56b4 | ||
|
|
2d62fc7443 | ||
|
|
dfad6f0906 | ||
|
|
267a6cb6b3 | ||
|
|
025d0c5822 | ||
|
|
5793df7353 | ||
|
|
fae03e7561 | ||
|
|
bcf53f0afc | ||
|
|
e4a586b92a | ||
|
|
76b897eb05 | ||
|
|
3c1e2cd452 | ||
|
|
270dbd361b | ||
|
|
a83cf43e60 | ||
|
|
8b0bc42d50 | ||
|
|
b609679993 | ||
|
|
850f4d237b | ||
|
|
019bc8c057 | ||
|
|
a710944218 | ||
|
|
2b4097e90a | ||
|
|
7a5ad278bb | ||
|
|
f918ea38cd |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
gin-bin
|
||||
example.config.local.json
|
||||
/config.yml
|
||||
/config.json
|
||||
examples/
|
||||
74
CODE_OF_CONDUCT.md
Normal file
74
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at management@castawaylabs.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM golang
|
||||
|
||||
ADD . /go/src/github.com/castawaylabs/cachet-monitor
|
||||
RUN go install github.com/castawaylabs/cachet-monitor
|
||||
|
||||
ENTRYPOINT /go/bin/cachet-monitor
|
||||
79
api.go
Normal file
79
api.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CachetAPI struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}
|
||||
|
||||
type CachetResponse struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
func (api CachetAPI) Ping() error {
|
||||
resp, _, err := api.NewRequest("GET", "/ping", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("API Responded with non-200 status code")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMetric adds a data point to a cachet monitor
|
||||
func (api CachetAPI) SendMetric(id int, lag int64) {
|
||||
logrus.Debugf("Sending lag metric ID:%d RTT %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)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
// 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.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, CachetResponse{}, err
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&body)
|
||||
|
||||
return res, body, err
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package cachet
|
||||
|
||||
// Component Cachet model
|
||||
type Component struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status int `json:"status_id"`
|
||||
HumanStatus string `json:"-"`
|
||||
IncidentCount int `json:"-"`
|
||||
CreatedAt int `json:"created_at"`
|
||||
UpdatedAt int `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ComponentData json response model
|
||||
type ComponentData struct {
|
||||
Component Component `json:"data"`
|
||||
}
|
||||
116
cachet/config.go
116
cachet/config.go
@@ -1,116 +0,0 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/castawaylabs/cachet-monitor/system"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Static config
|
||||
var Config CachetConfig
|
||||
|
||||
// Central logger
|
||||
var Logger *log.Logger
|
||||
|
||||
// CachetConfig is the monitoring tool configuration
|
||||
type CachetConfig struct {
|
||||
APIUrl string `json:"api_url"`
|
||||
APIToken string `json:"api_token"`
|
||||
Monitors []*Monitor `json:"monitors"`
|
||||
SystemName string `json:"system_name"`
|
||||
LogPath string `json:"log_path"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
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 {
|
||||
fmt.Printf("Cannot download network config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
data, _ = ioutil.ReadAll(response.Body)
|
||||
|
||||
fmt.Println("Downloaded network configuration.")
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Config file '" + configPath + "' missing!")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &Config)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Cannot parse config!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(systemName) > 0 {
|
||||
Config.SystemName = systemName
|
||||
}
|
||||
if len(Config.SystemName) == 0 {
|
||||
// get hostname
|
||||
Config.SystemName = system.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 len(Config.APIToken) == 0 || len(Config.APIUrl) == 0 {
|
||||
fmt.Printf("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")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(Config.Monitors) == 0 {
|
||||
fmt.Printf("No monitors defined!\nSee sample configuration: https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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 {
|
||||
fmt.Printf("Unable to open file '%v' for logging\n", Config.LogPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
flags := log.Llongfile|log.Ldate|log.Ltime
|
||||
if len(os.Getenv("DEVELOPMENT")) > 0 {
|
||||
flags = 0
|
||||
}
|
||||
|
||||
Logger = log.New(logWriter, "", flags)
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Incident Cachet data model
|
||||
type Incident struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"` // 4?
|
||||
HumanStatus string `json:"human_status"`
|
||||
Component *Component `json:"-"`
|
||||
ComponentID *int `json:"component_id"`
|
||||
CreatedAt int `json:"created_at"`
|
||||
UpdatedAt int `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)
|
||||
if err != nil {
|
||||
Logger.Printf("Cannot get incidents: %v\n", err)
|
||||
return []Incident{}
|
||||
}
|
||||
|
||||
var data IncidentList
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
Logger.Printf("Cannot parse incidents: %v\n", err)
|
||||
}
|
||||
|
||||
return data.Incidents
|
||||
}
|
||||
|
||||
// Send - Create or Update incident
|
||||
func (incident *Incident) Send() {
|
||||
jsonBytes, err := json.Marshal(incident)
|
||||
if err != nil {
|
||||
Logger.Printf("Cannot encode incident: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
requestType := "POST"
|
||||
requestURL := "/incidents"
|
||||
if incident.ID > 0 {
|
||||
requestType = "PUT"
|
||||
requestURL += "/" + strconv.Itoa(incident.ID)
|
||||
}
|
||||
|
||||
resp, body, err := makeRequest(requestType, requestURL, jsonBytes)
|
||||
if err != nil {
|
||||
Logger.Printf("Cannot create/update incident: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
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.")
|
||||
panic(err)
|
||||
} else {
|
||||
incident.ID = data.Incident.ID
|
||||
incident.Component = data.Incident.Component
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
Logger.Println("Could not create/update incident!")
|
||||
}
|
||||
}
|
||||
|
||||
// GetSimilarIncidentID gets the same incident.
|
||||
// Updates incident.ID
|
||||
func (incident *Incident) GetSimilarIncidentID() {
|
||||
incidents := GetIncidents()
|
||||
|
||||
for _, inc := range incidents {
|
||||
if incident.Name == inc.Name && incident.Message == inc.Message && incident.Status == inc.Status && incident.HumanStatus == inc.HumanStatus {
|
||||
incident.ID = inc.ID
|
||||
Logger.Printf("Updated incident id to %v\n", inc.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (incident *Incident) fetchComponent() error {
|
||||
_, body, err := makeRequest("GET", "/components/" + strconv.Itoa(*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.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
incident.Component = &data.Component
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (incident *Incident) UpdateComponent() {
|
||||
if incident.ComponentID == nil || *incident.ComponentID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if incident.Component == nil {
|
||||
// fetch component
|
||||
if err := incident.fetchComponent(); err != nil {
|
||||
Logger.Printf("Cannot fetch component for incident. %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch incident.Status {
|
||||
case 1, 2, 3:
|
||||
if incident.Component.Status == 3 {
|
||||
incident.Component.Status = 4
|
||||
} else {
|
||||
incident.Component.Status = 3
|
||||
}
|
||||
case 4:
|
||||
incident.Component.Status = 1
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(map[string]interface{}{
|
||||
"status": incident.Component.Status,
|
||||
})
|
||||
|
||||
resp, _, err := makeRequest("PUT", "/components/" + strconv.Itoa(incident.Component.ID), jsonBytes)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
Logger.Printf("Could not update component: (resp code %d) %v", resp.StatusCode, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SetInvestigating sets status to Investigating
|
||||
func (incident *Incident) SetInvestigating() {
|
||||
incident.Status = 1
|
||||
incident.HumanStatus = "Investigating"
|
||||
}
|
||||
|
||||
// SetIdentified sets status to Identified
|
||||
func (incident *Incident) SetIdentified() {
|
||||
incident.Status = 2
|
||||
incident.HumanStatus = "Identified"
|
||||
}
|
||||
|
||||
// SetWatching sets status to Watching
|
||||
func (incident *Incident) SetWatching() {
|
||||
incident.Status = 3
|
||||
incident.HumanStatus = "Watching"
|
||||
}
|
||||
|
||||
// SetFixed sets status to Fixed
|
||||
func (incident *Incident) SetFixed() {
|
||||
incident.Status = 4
|
||||
incident.HumanStatus = "Fixed"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SendMetric sends lag metric point
|
||||
func SendMetric(metricID int, delay int64) {
|
||||
if metricID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(&map[string]interface{}{
|
||||
"value": delay,
|
||||
})
|
||||
|
||||
resp, _, err := 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
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const timeout = time.Duration(time.Second)
|
||||
|
||||
// Monitor data model
|
||||
type Monitor struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
MetricID int `json:"metric_id"`
|
||||
Threshold float32 `json:"threshold"`
|
||||
ComponentID *int `json:"component_id"`
|
||||
ExpectedStatusCode int `json:"expected_status_code"`
|
||||
|
||||
History []bool `json:"-"`
|
||||
LastFailReason *string `json:"-"`
|
||||
Incident *Incident `json:"-"`
|
||||
}
|
||||
|
||||
// Run loop
|
||||
func (monitor *Monitor) Run() {
|
||||
reqStart := getMs()
|
||||
isUp := monitor.doRequest()
|
||||
lag := getMs() - reqStart
|
||||
|
||||
if len(monitor.History) >= 10 {
|
||||
monitor.History = monitor.History[len(monitor.History)-9:]
|
||||
}
|
||||
monitor.History = append(monitor.History, isUp)
|
||||
monitor.AnalyseData()
|
||||
|
||||
if isUp == true && monitor.MetricID > 0 {
|
||||
SendMetric(monitor.MetricID, lag)
|
||||
}
|
||||
}
|
||||
|
||||
func (monitor *Monitor) doRequest() bool {
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
resp, err := client.Get(monitor.URL)
|
||||
if err != nil {
|
||||
errString := err.Error()
|
||||
monitor.LastFailReason = &errString
|
||||
return false
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.StatusCode == monitor.ExpectedStatusCode
|
||||
}
|
||||
|
||||
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
||||
func (monitor *Monitor) AnalyseData() {
|
||||
// look at the past few incidents
|
||||
numDown := 0
|
||||
for _, wasUp := range monitor.History {
|
||||
if wasUp == false {
|
||||
numDown++
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if len(monitor.History) != 10 {
|
||||
// not enough data
|
||||
return
|
||||
}
|
||||
|
||||
if t > monitor.Threshold && monitor.Incident == nil {
|
||||
// is down, create an incident
|
||||
Logger.Println("Creating incident...")
|
||||
|
||||
monitor.Incident = &Incident{
|
||||
Name: monitor.Name + " - " + Config.SystemName,
|
||||
Message: monitor.Name + " failed",
|
||||
ComponentID: monitor.ComponentID,
|
||||
}
|
||||
|
||||
if monitor.LastFailReason != nil {
|
||||
monitor.Incident.Message += "\n\n" + *monitor.LastFailReason
|
||||
}
|
||||
|
||||
// set investigating status
|
||||
monitor.Incident.SetInvestigating()
|
||||
|
||||
// lookup relevant incident
|
||||
monitor.Incident.GetSimilarIncidentID()
|
||||
|
||||
// create/update incident
|
||||
monitor.Incident.Send()
|
||||
monitor.Incident.UpdateComponent()
|
||||
} 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...")
|
||||
|
||||
// Add resolved message
|
||||
monitor.Incident.Message += "\n\n-\n\nResolved at " + time.Now().String()
|
||||
|
||||
monitor.Incident.SetFixed()
|
||||
monitor.Incident.Send()
|
||||
monitor.Incident.UpdateComponent()
|
||||
|
||||
monitor.Incident = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getMs() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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{}
|
||||
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
|
||||
}
|
||||
196
cli/main.go
Normal file
196
cli/main.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
cachet "github.com/castawaylabs/cachet-monitor"
|
||||
docopt "github.com/docopt/docopt-go"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const usage = `cachet-monitor
|
||||
|
||||
Usage:
|
||||
cachet-monitor (-c PATH | --config PATH) [--log=LOGPATH] [--name=NAME] [--immediate]
|
||||
cachet-monitor -h | --help | --version
|
||||
|
||||
Arguments:
|
||||
PATH path to config.json
|
||||
LOGPATH path to log output (defaults to STDOUT)
|
||||
NAME name of this logger
|
||||
|
||||
Examples:
|
||||
cachet-monitor -c /root/cachet-monitor.json
|
||||
cachet-monitor -c /root/cachet-monitor.json --log=/var/log/cachet-monitor.log --name="development machine"
|
||||
|
||||
Options:
|
||||
-c PATH.json --config PATH Path to configuration file
|
||||
-h --help Show this screen.
|
||||
--version Show version
|
||||
--immediate Tick immediately (by default waits for first defined interval)
|
||||
|
||||
Environment varaibles:
|
||||
CACHET_API override API url from configuration
|
||||
CACHET_TOKEN override API token from configuration
|
||||
CACHET_DEV set to enable dev logging`
|
||||
|
||||
func main() {
|
||||
arguments, _ := docopt.Parse(usage, nil, true, "cachet-monitor", false)
|
||||
|
||||
cfg, err := getConfiguration(arguments["--config"].(string))
|
||||
if err != nil {
|
||||
logrus.Panicf("Unable to start (reading config): %v", err)
|
||||
}
|
||||
|
||||
if immediate, ok := arguments["--immediate"]; ok {
|
||||
cfg.Immediate = immediate.(bool)
|
||||
}
|
||||
|
||||
if name := arguments["--name"]; name != nil {
|
||||
cfg.SystemName = name.(string)
|
||||
}
|
||||
logrus.SetOutput(getLogger(arguments["--log"]))
|
||||
|
||||
if len(os.Getenv("CACHET_API")) > 0 {
|
||||
cfg.API.URL = os.Getenv("CACHET_API")
|
||||
}
|
||||
if len(os.Getenv("CACHET_TOKEN")) > 0 {
|
||||
cfg.API.Token = os.Getenv("CACHET_TOKEN")
|
||||
}
|
||||
if len(os.Getenv("CACHET_DEV")) > 0 {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
if valid := cfg.Validate(); !valid {
|
||||
logrus.Errorf("Invalid configuration")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Debug("Configuration valid")
|
||||
logrus.Infof("System: %s", cfg.SystemName)
|
||||
logrus.Infof("API: %s", cfg.API.URL)
|
||||
logrus.Infof("Monitors: %d\n", len(cfg.Monitors))
|
||||
|
||||
logrus.Infof("Pinging cachet")
|
||||
if err := cfg.API.Ping(); err != nil {
|
||||
logrus.Errorf("Cannot ping cachet!\n%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logrus.Infof("Ping OK")
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
for index, monitor := range cfg.Monitors {
|
||||
logrus.Infof("Starting Monitor #%d: ", index)
|
||||
logrus.Infof("Features: \n - %v", strings.Join(monitor.Describe(), "\n - "))
|
||||
|
||||
go monitor.ClockStart(cfg, monitor, 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 cfg.Monitors {
|
||||
mon.GetMonitor().ClockStop()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func getLogger(logPath interface{}) *os.File {
|
||||
if logPath == nil || len(logPath.(string)) == 0 {
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
file, err := os.Create(logPath.(string))
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to open file '%v' for logging: \n%v", logPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
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 {
|
||||
logrus.Warn("Unable to download network configuration")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
data, _ = ioutil.ReadAll(response.Body)
|
||||
|
||||
logrus.Info("Downloaded network configuration.")
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to open file: '" + path + "'")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
|
||||
err = yaml.Unmarshal(data, &cfg)
|
||||
} else {
|
||||
err = json.Unmarshal(data, &cfg)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.Warnf("Unable to parse configuration file")
|
||||
}
|
||||
|
||||
cfg.Monitors = make([]cachet.MonitorInterface, len(cfg.RawMonitors))
|
||||
for index, rawMonitor := range cfg.RawMonitors {
|
||||
var t cachet.MonitorInterface
|
||||
var err error
|
||||
|
||||
// get default type
|
||||
monType := cachet.GetMonitorType("")
|
||||
if t, ok := rawMonitor["type"].(string); ok {
|
||||
monType = cachet.GetMonitorType(t)
|
||||
}
|
||||
|
||||
switch monType {
|
||||
case "http":
|
||||
var s cachet.HTTPMonitor
|
||||
err = mapstructure.Decode(rawMonitor, &s)
|
||||
t = &s
|
||||
case "dns":
|
||||
var s cachet.DNSMonitor
|
||||
err = mapstructure.Decode(rawMonitor, &s)
|
||||
t = &s
|
||||
default:
|
||||
logrus.Errorf("Invalid monitor type (index: %d) %v", index, monType)
|
||||
continue
|
||||
}
|
||||
|
||||
t.GetMonitor().Type = monType
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to unmarshal monitor to type (index: %d): %v", index, err)
|
||||
continue
|
||||
}
|
||||
|
||||
cfg.Monitors[index] = t
|
||||
}
|
||||
|
||||
return &cfg, err
|
||||
}
|
||||
89
config.go
Normal file
89
config.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CachetMonitor struct {
|
||||
SystemName string `json:"system_name" yaml:"system_name"`
|
||||
DateFormat string `json:"date_format" yaml:"date_format"`
|
||||
API CachetAPI `json:"api"`
|
||||
RawMonitors []map[string]interface{} `json:"monitors" yaml:"monitors"`
|
||||
|
||||
Monitors []MonitorInterface `json:"-" yaml:"-"`
|
||||
Immediate bool `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
func (cfg *CachetMonitor) Validate() bool {
|
||||
valid := true
|
||||
|
||||
if len(cfg.SystemName) == 0 {
|
||||
// get hostname
|
||||
cfg.SystemName = getHostname()
|
||||
}
|
||||
|
||||
if len(cfg.DateFormat) == 0 {
|
||||
cfg.DateFormat = DefaultTimeFormat
|
||||
}
|
||||
|
||||
if len(cfg.API.Token) == 0 || len(cfg.API.URL) == 0 {
|
||||
logrus.Warnf("API URL or API Token missing.\nGet help at https://github.com/castawaylabs/cachet-monitor")
|
||||
valid = false
|
||||
}
|
||||
|
||||
if len(cfg.Monitors) == 0 {
|
||||
logrus.Warnf("No monitors defined!\nSee help for example configuration")
|
||||
valid = false
|
||||
}
|
||||
|
||||
for index, monitor := range cfg.Monitors {
|
||||
if errs := monitor.Validate(); len(errs) > 0 {
|
||||
logrus.Warnf("Monitor validation errors (index %d): %v", index, "\n - "+strings.Join(errs, "\n - "))
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// getHostname returns id of the current system
|
||||
func getHostname() string {
|
||||
hostname, err := os.Hostname()
|
||||
if err == nil && len(hostname) > 0 {
|
||||
return hostname
|
||||
}
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil || len(addrs) == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return addrs[0].String()
|
||||
}
|
||||
|
||||
func getMs() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func GetMonitorType(t string) string {
|
||||
if len(t) == 0 {
|
||||
return "http"
|
||||
}
|
||||
|
||||
return strings.ToLower(t)
|
||||
}
|
||||
|
||||
func getTemplateData(monitor *AbstractMonitor) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"SystemName": monitor.config.SystemName,
|
||||
"API": monitor.config.API,
|
||||
"Monitor": monitor,
|
||||
"now": time.Now().Format(monitor.config.DateFormat),
|
||||
}
|
||||
}
|
||||
15
config_test.go
Normal file
15
config_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetMonitorType(t *testing.T) {
|
||||
if monType := GetMonitorType(""); monType != "http" {
|
||||
t.Error("monitor type `` should default to http")
|
||||
}
|
||||
|
||||
if mt := GetMonitorType("HTTP"); mt != "http" {
|
||||
t.Error("does not return correct monitor type")
|
||||
}
|
||||
}
|
||||
121
dns.go
Normal file
121
dns.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DNSAnswer struct {
|
||||
Regex string
|
||||
regexp *regexp.Regexp
|
||||
Exact string
|
||||
}
|
||||
|
||||
type DNSMonitor struct {
|
||||
AbstractMonitor `mapstructure:",squash"`
|
||||
|
||||
// IP:port format or blank to use system defined DNS
|
||||
DNS string
|
||||
|
||||
// A(default), AAAA, MX, ...
|
||||
Question string
|
||||
question uint16
|
||||
|
||||
Answers []DNSAnswer
|
||||
}
|
||||
|
||||
func (monitor *DNSMonitor) Validate() []string {
|
||||
errs := monitor.AbstractMonitor.Validate()
|
||||
|
||||
if len(monitor.DNS) == 0 {
|
||||
config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||
if len(config.Servers) > 0 {
|
||||
monitor.DNS = net.JoinHostPort(config.Servers[0], config.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(monitor.DNS) == 0 {
|
||||
monitor.DNS = "8.8.8.8:53"
|
||||
}
|
||||
|
||||
if len(monitor.Question) == 0 {
|
||||
monitor.Question = "A"
|
||||
}
|
||||
monitor.Question = strings.ToUpper(monitor.Question)
|
||||
|
||||
monitor.question = findDNSType(monitor.Question)
|
||||
if monitor.question == 0 {
|
||||
errs = append(errs, "Could not look up DNS question type")
|
||||
}
|
||||
|
||||
for i, a := range monitor.Answers {
|
||||
if len(a.Regex) > 0 {
|
||||
monitor.Answers[i].regexp, _ = regexp.Compile(a.Regex)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (monitor *DNSMonitor) test() bool {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(monitor.Target), monitor.question)
|
||||
m.RecursionDesired = true
|
||||
|
||||
c := new(dns.Client)
|
||||
r, _, err := c.Exchange(m, monitor.DNS)
|
||||
if err != nil {
|
||||
logrus.Warnf("DNS error: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, check := range monitor.Answers {
|
||||
found := false
|
||||
for _, answer := range r.Answer {
|
||||
found = matchAnswer(answer, check)
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
logrus.Warnf("DNS check failed: %v. Not found in any of %v", check, r.Answer)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func findDNSType(t string) uint16 {
|
||||
for rr, strType := range dns.TypeToString {
|
||||
if t == strType {
|
||||
return rr
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func matchAnswer(answer dns.RR, check DNSAnswer) bool {
|
||||
fields := []string{}
|
||||
for i := 0; i < dns.NumField(answer); i++ {
|
||||
fields = append(fields, dns.Field(answer, i+1))
|
||||
}
|
||||
|
||||
str := strings.Join(fields, " ")
|
||||
|
||||
if check.regexp != nil {
|
||||
return check.regexp.Match([]byte(str))
|
||||
}
|
||||
|
||||
return str == check.Exact
|
||||
}
|
||||
@@ -1,14 +1,59 @@
|
||||
{
|
||||
"api_url": "https://demo.cachethq.io/api",
|
||||
"api_token": "9yMHsdioQosnyVK4iCVR",
|
||||
"api": {
|
||||
"url": "https://demo.cachethq.io/api/v1",
|
||||
"token": "9yMHsdioQosnyVK4iCVR",
|
||||
"insecure": false
|
||||
},
|
||||
"date_format": "02/01/2006 15:04:05 MST",
|
||||
"monitors": [
|
||||
{
|
||||
"name": "nodegear frontend",
|
||||
"url": "https://nodegear.io/ping",
|
||||
"metric_id": 1,
|
||||
"name": "google",
|
||||
"target": "https://google.com",
|
||||
"strict": true,
|
||||
"method": "POST",
|
||||
"component_id": 1,
|
||||
"metric_id": 4,
|
||||
"template": {
|
||||
"investigating": {
|
||||
"subject": "{{ .Monitor.Name }} - {{ .SystemName }}",
|
||||
"message": "{{ .Monitor.Name }} check **failed** (server time: {{ .now }})\n\n{{ .FailReason }}"
|
||||
},
|
||||
"fixed": {
|
||||
"subject": "I HAVE BEEN FIXED"
|
||||
}
|
||||
},
|
||||
"interval": 1,
|
||||
"timeout": 1,
|
||||
"threshold": 80,
|
||||
"component_id": null,
|
||||
"expected_status_code": 200
|
||||
"headers": {
|
||||
"Authorization": "Basic <hash>"
|
||||
},
|
||||
"expected_status_code": 200,
|
||||
"expected_body": "P.*NG"
|
||||
},
|
||||
{
|
||||
"name": "dns",
|
||||
"target": "matej.me.",
|
||||
"question": "mx",
|
||||
"type": "dns",
|
||||
"component_id": 2,
|
||||
"interval": 1,
|
||||
"timeout": 1,
|
||||
"dns": "8.8.4.4:53",
|
||||
"answers": [
|
||||
{
|
||||
"regex": "[1-9] alt[1-9].aspmx.l.google.com."
|
||||
},
|
||||
{
|
||||
"exact": "10 aspmx2.googlemail.com."
|
||||
},
|
||||
{
|
||||
"exact": "1 aspmx.l.google.com."
|
||||
},
|
||||
{
|
||||
"exact": "10 aspmx3.googlemail.com."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
65
example.config.yml
Normal file
65
example.config.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
api:
|
||||
# cachet url
|
||||
url: https://demo.cachethq.io/api/v1
|
||||
# cachet api token
|
||||
token: 9yMHsdioQosnyVK4iCVR
|
||||
insecure: false
|
||||
# https://golang.org/src/time/format.go#L57
|
||||
date_format: 02/01/2006 15:04:05 MST
|
||||
monitors:
|
||||
# http monitor example
|
||||
- name: google
|
||||
# test url
|
||||
target: https://google.com
|
||||
# strict certificate checking for https
|
||||
strict: true
|
||||
# HTTP method
|
||||
method: POST
|
||||
|
||||
# set to update component (either component_id or metric_id are required)
|
||||
component_id: 1
|
||||
# set to post lag to cachet metric (graph)
|
||||
metric_id: 4
|
||||
|
||||
# custom templates (see readme for details)
|
||||
template:
|
||||
investigating:
|
||||
subject: "{{ .Monitor.Name }} - {{ .SystemName }}"
|
||||
message: "{{ .Monitor.Name }} check **failed** (server time: {{ .now }})\n\n{{ .FailReason }}"
|
||||
fixed:
|
||||
subject: "I HAVE BEEN FIXED"
|
||||
|
||||
# seconds between checks
|
||||
interval: 1
|
||||
# seconds for timeout
|
||||
timeout: 1
|
||||
# If % of downtime is over this threshold, open an incident
|
||||
threshold: 80
|
||||
|
||||
# custom HTTP headers
|
||||
headers:
|
||||
Authorization: Basic <hash>
|
||||
# expected status code (either status code or body must be supplied)
|
||||
expected_status_code: 200
|
||||
# regex to match body
|
||||
expected_body: "P.*NG"
|
||||
# dns monitor example
|
||||
- name: dns
|
||||
# fqdn
|
||||
target: matej.me.
|
||||
# question type (A/AAAA/CNAME/...)
|
||||
question: mx
|
||||
type: dns
|
||||
# set component_id/metric_id
|
||||
component_id: 2
|
||||
# poll every 1s
|
||||
interval: 1
|
||||
timeout: 1
|
||||
# custom DNS server (defaults to system)
|
||||
dns: 8.8.4.4:53
|
||||
answers:
|
||||
# exact/regex check
|
||||
- regex: [1-9] alt[1-9].aspmx.l.google.com.
|
||||
- exact: 10 aspmx2.googlemail.com.
|
||||
- exact: 1 aspmx.l.google.com.
|
||||
- exact: 10 aspmx3.googlemail.com.
|
||||
125
http.go
Normal file
125
http.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Investigating template
|
||||
var defaultHTTPInvestigatingTpl = MessageTemplate{
|
||||
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||
Message: `{{ .Monitor.Name }} check **failed** (server time: {{ .now }})
|
||||
|
||||
{{ .FailReason }}`,
|
||||
}
|
||||
|
||||
// Fixed template
|
||||
var defaultHTTPFixedTpl = MessageTemplate{
|
||||
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||
Message: `**Resolved** - {{ .now }}
|
||||
|
||||
- - -
|
||||
|
||||
{{ .incident.Message }}`,
|
||||
}
|
||||
|
||||
type HTTPMonitor struct {
|
||||
AbstractMonitor `mapstructure:",squash"`
|
||||
|
||||
Method string
|
||||
ExpectedStatusCode int `mapstructure:"expected_status_code"`
|
||||
Headers map[string]string
|
||||
|
||||
// compiled to Regexp
|
||||
ExpectedBody string `mapstructure:"expected_body"`
|
||||
bodyRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
func (monitor *HTTPMonitor) test() bool {
|
||||
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
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if monitor.ExpectedStatusCode > 0 && resp.StatusCode != monitor.ExpectedStatusCode {
|
||||
monitor.lastFailReason = "Expected HTTP response status: " + strconv.Itoa(monitor.ExpectedStatusCode) + ", got: " + strconv.Itoa(resp.StatusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
if monitor.bodyRegexp != nil {
|
||||
// check response body
|
||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
monitor.lastFailReason = err.Error()
|
||||
return false
|
||||
}
|
||||
|
||||
if !monitor.bodyRegexp.Match(responseBody) {
|
||||
monitor.lastFailReason = "Unexpected body: " + string(responseBody) + ".\nExpected to match: " + monitor.ExpectedBody
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
func (mon *HTTPMonitor) Validate() []string {
|
||||
mon.Template.Investigating.SetDefault(defaultHTTPInvestigatingTpl)
|
||||
mon.Template.Fixed.SetDefault(defaultHTTPFixedTpl)
|
||||
|
||||
errs := mon.AbstractMonitor.Validate()
|
||||
|
||||
if len(mon.ExpectedBody) > 0 {
|
||||
exp, err := regexp.Compile(mon.ExpectedBody)
|
||||
if err != nil {
|
||||
errs = append(errs, "Regexp compilation failure: "+err.Error())
|
||||
} else {
|
||||
mon.bodyRegexp = exp
|
||||
}
|
||||
}
|
||||
|
||||
if len(mon.ExpectedBody) == 0 && mon.ExpectedStatusCode == 0 {
|
||||
errs = append(errs, "Both 'expected_body' and 'expected_status_code' fields empty")
|
||||
}
|
||||
|
||||
mon.Method = strings.ToUpper(mon.Method)
|
||||
switch mon.Method {
|
||||
case "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD":
|
||||
break
|
||||
case "":
|
||||
mon.Method = "GET"
|
||||
default:
|
||||
errs = append(errs, "Unsupported HTTP method: "+mon.Method)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (mon *HTTPMonitor) Describe() []string {
|
||||
features := mon.AbstractMonitor.Describe()
|
||||
features = append(features, "Method: "+mon.Method)
|
||||
|
||||
return features
|
||||
}
|
||||
112
incident.go
Normal file
112
incident.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Incident Cachet data model
|
||||
type Incident struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
Visible int `json"visible"`
|
||||
Notify bool `json:"notify"`
|
||||
|
||||
ComponentID int `json:"component_id"`
|
||||
ComponentStatus int `json:"component_status"`
|
||||
}
|
||||
|
||||
// Send - Create or Update incident
|
||||
func (incident *Incident) Send(cfg *CachetMonitor) error {
|
||||
switch incident.Status {
|
||||
case 1, 2, 3:
|
||||
// partial outage
|
||||
incident.ComponentStatus = 3
|
||||
|
||||
componentStatus, err := incident.GetComponentStatus(cfg)
|
||||
if componentStatus == 3 {
|
||||
// major outage
|
||||
incident.ComponentStatus = 4
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.Warnf("cannot fetch component: %v", err)
|
||||
}
|
||||
case 4:
|
||||
// fixed
|
||||
incident.ComponentStatus = 1
|
||||
}
|
||||
|
||||
requestType := "POST"
|
||||
requestURL := "/incidents"
|
||||
if incident.ID > 0 {
|
||||
requestType = "PUT"
|
||||
requestURL += "/" + strconv.Itoa(incident.ID)
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(incident)
|
||||
|
||||
resp, body, err := cfg.API.NewRequest(requestType, requestURL, jsonBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
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.ID
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("Could not create/update incident!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) {
|
||||
resp, body, err := cfg.API.NewRequest("GET", "/components/"+strconv.Itoa(incident.ComponentID), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0, fmt.Errorf("Invalid status code. Received %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
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.Status, nil
|
||||
}
|
||||
|
||||
// SetInvestigating sets status to Investigating
|
||||
func (incident *Incident) SetInvestigating() {
|
||||
incident.Status = 1
|
||||
}
|
||||
|
||||
// SetIdentified sets status to Identified
|
||||
func (incident *Incident) SetIdentified() {
|
||||
incident.Status = 2
|
||||
}
|
||||
|
||||
// SetWatching sets status to Watching
|
||||
func (incident *Incident) SetWatching() {
|
||||
incident.Status = 3
|
||||
}
|
||||
|
||||
// SetFixed sets status to Fixed
|
||||
func (incident *Incident) SetFixed() {
|
||||
incident.Status = 4
|
||||
}
|
||||
29
main.go
29
main.go
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/castawaylabs/cachet-monitor/cachet"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := cachet.Config
|
||||
log := cachet.Logger
|
||||
|
||||
log.Printf("System: %s, API: %s\n", config.SystemName, 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)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for range ticker.C {
|
||||
for _, mon := range config.Monitors {
|
||||
go mon.Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
252
monitor.go
Normal file
252
monitor.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
const DefaultInterval = time.Second * 60
|
||||
const DefaultTimeout = time.Second
|
||||
const DefaultTimeFormat = "15:04:05 Jan 2 MST"
|
||||
const HistorySize = 10
|
||||
|
||||
type MonitorInterface interface {
|
||||
ClockStart(*CachetMonitor, MonitorInterface, *sync.WaitGroup)
|
||||
ClockStop()
|
||||
tick(MonitorInterface)
|
||||
test() bool
|
||||
|
||||
Validate() []string
|
||||
GetMonitor() *AbstractMonitor
|
||||
Describe() []string
|
||||
}
|
||||
|
||||
// AbstractMonitor data model
|
||||
type AbstractMonitor struct {
|
||||
Name string
|
||||
Target string
|
||||
|
||||
// (default)http / dns
|
||||
Type string
|
||||
Strict bool
|
||||
|
||||
Interval time.Duration
|
||||
Timeout time.Duration
|
||||
|
||||
MetricID int `mapstructure:"metric_id"`
|
||||
ComponentID int `mapstructure:"component_id"`
|
||||
|
||||
// Templating stuff
|
||||
Template struct {
|
||||
Investigating MessageTemplate
|
||||
Fixed MessageTemplate
|
||||
}
|
||||
|
||||
// Threshold = percentage / number of down incidents
|
||||
Threshold float32
|
||||
ThresholdCount bool `mapstructure:"threshold_count"`
|
||||
|
||||
// lag / average(lagHistory) * 100 = percentage above average lag
|
||||
// PerformanceThreshold sets the % limit above which this monitor will trigger degraded-performance
|
||||
// PerformanceThreshold float32
|
||||
|
||||
history []bool
|
||||
// lagHistory []float32
|
||||
lastFailReason string
|
||||
incident *Incident
|
||||
config *CachetMonitor
|
||||
|
||||
// Closed when mon.Stop() is called
|
||||
stopC chan bool
|
||||
}
|
||||
|
||||
func (mon *AbstractMonitor) Validate() []string {
|
||||
errs := []string{}
|
||||
|
||||
if len(mon.Name) == 0 {
|
||||
errs = append(errs, "Name is required")
|
||||
}
|
||||
|
||||
if mon.Interval < 1 {
|
||||
mon.Interval = DefaultInterval
|
||||
}
|
||||
if mon.Timeout < 1 {
|
||||
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")
|
||||
}
|
||||
|
||||
if mon.Threshold <= 0 {
|
||||
mon.Threshold = 100
|
||||
}
|
||||
|
||||
if err := mon.Template.Fixed.Compile(); err != nil {
|
||||
errs = append(errs, "Could not compile \"fixed\" template: "+err.Error())
|
||||
}
|
||||
if err := mon.Template.Investigating.Compile(); err != nil {
|
||||
errs = append(errs, "Could not compile \"investigating\" template: "+err.Error())
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
func (mon *AbstractMonitor) GetMonitor() *AbstractMonitor {
|
||||
return mon
|
||||
}
|
||||
func (mon *AbstractMonitor) Describe() []string {
|
||||
features := []string{"Type: " + mon.Type}
|
||||
|
||||
if len(mon.Name) > 0 {
|
||||
features = append(features, "Name: "+mon.Name)
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
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(iface)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(mon.Interval * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
mon.tick(iface)
|
||||
case <-mon.stopC:
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mon *AbstractMonitor) ClockStop() {
|
||||
select {
|
||||
case <-mon.stopC:
|
||||
return
|
||||
default:
|
||||
close(mon.stopC)
|
||||
}
|
||||
}
|
||||
|
||||
func (mon *AbstractMonitor) test() bool { return false }
|
||||
|
||||
func (mon *AbstractMonitor) tick(iface MonitorInterface) {
|
||||
reqStart := getMs()
|
||||
up := iface.test()
|
||||
lag := getMs() - reqStart
|
||||
|
||||
histSize := HistorySize
|
||||
if mon.ThresholdCount {
|
||||
histSize = int(mon.Threshold)
|
||||
}
|
||||
|
||||
if len(mon.history) == histSize-1 {
|
||||
logrus.Warnf("%v is now saturated", mon.Name)
|
||||
}
|
||||
if len(mon.history) >= histSize {
|
||||
mon.history = mon.history[len(mon.history)-(histSize-1):]
|
||||
}
|
||||
mon.history = append(mon.history, up)
|
||||
mon.AnalyseData()
|
||||
|
||||
// report lag
|
||||
if mon.MetricID > 0 {
|
||||
go mon.config.API.SendMetric(mon.MetricID, lag)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
||||
func (mon *AbstractMonitor) AnalyseData() {
|
||||
// look at the past few incidents
|
||||
numDown := 0
|
||||
for _, wasUp := range mon.history {
|
||||
if wasUp == false {
|
||||
numDown++
|
||||
}
|
||||
}
|
||||
|
||||
t := (float32(numDown) / float32(len(mon.history))) * 100
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"monitor": mon.Name,
|
||||
"time": time.Now().Format(mon.config.DateFormat),
|
||||
})
|
||||
if numDown == 0 {
|
||||
l.Printf("monitor is up")
|
||||
} else if mon.ThresholdCount {
|
||||
l.Printf("monitor down %d/%d", numDown, int(mon.Threshold))
|
||||
} else {
|
||||
l.Printf("monitor down %.2f%%/%.2f%%", t, mon.Threshold)
|
||||
}
|
||||
|
||||
histSize := HistorySize
|
||||
if mon.ThresholdCount {
|
||||
histSize = int(mon.Threshold)
|
||||
}
|
||||
|
||||
if len(mon.history) != histSize {
|
||||
// not saturated
|
||||
return
|
||||
}
|
||||
|
||||
triggered := (mon.ThresholdCount && numDown == int(mon.Threshold)) || (!mon.ThresholdCount && t > mon.Threshold)
|
||||
|
||||
if triggered && mon.incident == nil {
|
||||
// create incident
|
||||
tplData := getTemplateData(mon)
|
||||
tplData["FailReason"] = mon.lastFailReason
|
||||
|
||||
subject, message := mon.Template.Investigating.Exec(tplData)
|
||||
mon.incident = &Incident{
|
||||
Name: subject,
|
||||
ComponentID: mon.ComponentID,
|
||||
Message: message,
|
||||
Notify: true,
|
||||
}
|
||||
|
||||
// is down, create an incident
|
||||
l.Warnf("creating incident. Monitor is down: %v", mon.lastFailReason)
|
||||
// set investigating status
|
||||
mon.incident.SetInvestigating()
|
||||
// create/update incident
|
||||
if err := mon.incident.Send(mon.config); err != nil {
|
||||
l.Printf("Error sending incident: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// still triggered or no incident
|
||||
if triggered || mon.incident == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// was down, created an incident, its now ok, make it resolved.
|
||||
l.Warn("Resolving incident")
|
||||
|
||||
// resolve incident
|
||||
tplData := getTemplateData(mon)
|
||||
tplData["incident"] = mon.incident
|
||||
|
||||
subject, message := mon.Template.Fixed.Exec(tplData)
|
||||
mon.incident.Name = subject
|
||||
mon.incident.Message = message
|
||||
mon.incident.SetFixed()
|
||||
if err := mon.incident.Send(mon.config); err != nil {
|
||||
l.Printf("Error sending incident: %v", err)
|
||||
}
|
||||
|
||||
mon.lastFailReason = ""
|
||||
mon.incident = nil
|
||||
}
|
||||
7
monitor_test.go
Normal file
7
monitor_test.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnalyseData(t *testing.T) {}
|
||||
227
readme.md
227
readme.md
@@ -1,84 +1,187 @@
|
||||
Cachet Monitor plugin
|
||||
=====================
|
||||

|
||||
|
||||
This is a monitoring plugin for CachetHQ.
|
||||
|
||||
Features
|
||||
--------
|
||||
## Features
|
||||
|
||||
- [x] Creates & Resolves Incidents
|
||||
- [x] Posts monitor lag every second
|
||||
- [x] Posts monitor lag to cachet graphs
|
||||
- [x] HTTP Checks (body/status code)
|
||||
- [x] DNS Checks
|
||||
- [x] Updates Component to Partial Outage
|
||||
- [x] Updates Component to Major Outage if in Partial Outage
|
||||
- [x] Updates Component to Major Outage if already in Partial Outage (works with distributed monitors)
|
||||
- [x] Can be run on multiple servers and geo regions
|
||||
|
||||
Docker Quickstart
|
||||
-----------------
|
||||
## Example Configuration
|
||||
|
||||
1. Create a configuration json
|
||||
2.
|
||||
```
|
||||
docker run -d \
|
||||
--name cachet-monitor \
|
||||
-h cachet-monitor \
|
||||
-v `pwd`/config.json:/etc/cachet-monitor.config.json \
|
||||
castawaylabs/cachet-monitor
|
||||
**Note:** configuration can be in json or yaml format. [`example.config.json`](https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json), [`example.config.yaml`](https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.yml) files.
|
||||
|
||||
```yaml
|
||||
api:
|
||||
# cachet url
|
||||
url: https://demo.cachethq.io/api/v1
|
||||
# cachet api token
|
||||
token: 9yMHsdioQosnyVK4iCVR
|
||||
insecure: false
|
||||
# https://golang.org/src/time/format.go#L57
|
||||
date_format: 02/01/2006 15:04:05 MST
|
||||
monitors:
|
||||
# http monitor example
|
||||
- name: google
|
||||
# test url
|
||||
target: https://google.com
|
||||
# strict certificate checking for https
|
||||
strict: true
|
||||
# HTTP method
|
||||
method: POST
|
||||
|
||||
# set to update component (either component_id or metric_id are required)
|
||||
component_id: 1
|
||||
# set to post lag to cachet metric (graph)
|
||||
metric_id: 4
|
||||
|
||||
# custom templates (see readme for details)
|
||||
template:
|
||||
investigating:
|
||||
subject: "{{ .Monitor.Name }} - {{ .SystemName }}"
|
||||
message: "{{ .Monitor.Name }} check **failed** (server time: {{ .now }})\n\n{{ .FailReason }}"
|
||||
fixed:
|
||||
subject: "I HAVE BEEN FIXED"
|
||||
|
||||
# seconds between checks
|
||||
interval: 1
|
||||
# seconds for timeout
|
||||
timeout: 1
|
||||
# If % of downtime is over this threshold, open an incident
|
||||
threshold: 80
|
||||
|
||||
# custom HTTP headers
|
||||
headers:
|
||||
Authorization: Basic <hash>
|
||||
# expected status code (either status code or body must be supplied)
|
||||
expected_status_code: 200
|
||||
# regex to match body
|
||||
expected_body: "P.*NG"
|
||||
# dns monitor example
|
||||
- name: dns
|
||||
# fqdn
|
||||
target: matej.me.
|
||||
# question type (A/AAAA/CNAME/...)
|
||||
question: mx
|
||||
type: dns
|
||||
# set component_id/metric_id
|
||||
component_id: 2
|
||||
# poll every 1s
|
||||
interval: 1
|
||||
timeout: 1
|
||||
# custom DNS server (defaults to system)
|
||||
dns: 8.8.4.4:53
|
||||
answers:
|
||||
# exact/regex check
|
||||
- regex: [1-9] alt[1-9].aspmx.l.google.com.
|
||||
- exact: 10 aspmx2.googlemail.com.
|
||||
- exact: 1 aspmx.l.google.com.
|
||||
- exact: 10 aspmx3.googlemail.com.
|
||||
```
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
## Installation
|
||||
|
||||
1. Download binary from [release page](https://github.com/CastawayLabs/cachet-monitor/releases)
|
||||
2. Create a configuration
|
||||
3. `cachet-monitor -c /etc/cachet-monitor.yaml`
|
||||
|
||||
pro tip: run in background using `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
||||
|
||||
```
|
||||
{
|
||||
"api_url": "https://demo.cachethq.io/api",
|
||||
"api_token": "9yMHsdioQosnyVK4iCVR",
|
||||
"monitors": [
|
||||
{
|
||||
"name": "nodegear frontend",
|
||||
"url": "https://nodegear.io/ping",
|
||||
"metric_id": 0,
|
||||
"component_id": 0,
|
||||
"threshold": 80,
|
||||
"component_id": null,
|
||||
"expected_status_code": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
Usage:
|
||||
cachet-monitor (-c PATH | --config PATH) [--log=LOGPATH] [--name=NAME] [--immediate]
|
||||
cachet-monitor -h | --help | --version
|
||||
|
||||
Arguments:
|
||||
PATH path to config.json
|
||||
LOGPATH path to log output (defaults to STDOUT)
|
||||
NAME name of this logger
|
||||
|
||||
Examples:
|
||||
cachet-monitor -c /root/cachet-monitor.json
|
||||
cachet-monitor -c /root/cachet-monitor.json --log=/var/log/cachet-monitor.log --name="development machine"
|
||||
|
||||
Options:
|
||||
-c PATH.json --config PATH Path to configuration file
|
||||
-h --help Show this screen.
|
||||
--version Show version
|
||||
--immediate Tick immediately (by default waits for first defined interval)
|
||||
|
||||
Environment varaibles:
|
||||
CACHET_API override API url from configuration
|
||||
CACHET_TOKEN override API token from configuration
|
||||
CACHET_DEV set to enable dev logging
|
||||
```
|
||||
|
||||
*Notes:*
|
||||
## Templates
|
||||
|
||||
- `metric_id` is optional
|
||||
- `component_id` is optional
|
||||
- `threshold` is a percentage
|
||||
- `expected_status_code` is a http response code
|
||||
- GET request will be performed on the `url`
|
||||
This package makes use of [`text/template`](https://godoc.org/text/template). [Default HTTP template](https://github.com/CastawayLabs/cachet-monitor/blob/master/http.go#L14)
|
||||
|
||||
How to run
|
||||
----------
|
||||
The following variables are available:
|
||||
|
||||
Example:
|
||||
| Root objects |
|
||||
| ------------- | -----------------
|
||||
| `.SystemName` | system name
|
||||
| `.API` | `api` object from configuration
|
||||
| `.Monitor` | `monitor` object from configuration
|
||||
| `.now` | formatted date string
|
||||
|
||||
1. Set up [Go](https://golang.org)
|
||||
2. `go install github.com/castawaylabs/cachet-monitor`
|
||||
3. `cachet-monitor -c https://raw.githubusercontent.com/CastawayLabs/cachet-monitor/master/example.config.json`
|
||||
| Monitor variables |
|
||||
| ------------------ |
|
||||
| `.Name` |
|
||||
| `.Target` |
|
||||
| `.Type` |
|
||||
| `.Strict` |
|
||||
| `.MetricID` |
|
||||
| ... |
|
||||
|
||||
Production:
|
||||
All monitor variables are available from `monitor.go`
|
||||
|
||||
1. Download the example config and save to `/etc/cachet-monitor.config.json`
|
||||
2. Run in background: `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
||||
## Vision and goals
|
||||
|
||||
```
|
||||
Usage of cachet-monitor:
|
||||
-c="/etc/cachet-monitor.config.json": Config path
|
||||
-log="": Log path
|
||||
-name="": System Name
|
||||
```
|
||||
We made this tool because we felt the need to have our own monitoring software (leveraging on Cachet).
|
||||
The idea is a stateless program which collects data and pushes it to a central cachet instance.
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
This gives us power to have an army of geographically distributed loggers and reveal issues in both latency & downtime on client websites.
|
||||
|
||||
| Name | Example Value | Description |
|
||||
| ------------ | --------------------------- | --------------------------- |
|
||||
| CACHET_API | http://demo.cachethq.io/api | URL endpoint for cachet api |
|
||||
| CACHET_TOKEN | randomvalue | API Authentication token |
|
||||
## Package usage
|
||||
|
||||
When using `cachet-monitor` as a package in another program, you should follow what `cli/main.go` does. It is important to call `Validate` on `CachetMonitor` and all the monitors inside.
|
||||
|
||||
[API Documentation](https://godoc.org/github.com/CastawayLabs/cachet-monitor)
|
||||
|
||||
# Contributions welcome
|
||||
|
||||
We'll happily accept contributions for the following (non exhaustive list).
|
||||
|
||||
- Implement ICMP check
|
||||
- Implement TCP check
|
||||
- Any bug fixes / code improvements
|
||||
- Test cases
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Castaway Labs LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,24 +0,0 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GetHostname returns id of the current system
|
||||
func GetHostname() string {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil || len(hostname) == 0 {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
|
||||
return hostname
|
||||
}
|
||||
53
template.go
Normal file
53
template.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package cachet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
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 (t *MessageTemplate) Exec(data interface{}) (string, string) {
|
||||
return t.exec(t.subjectTpl, data), t.exec(t.messageTpl, data)
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) exec(tpl *template.Template, data interface{}) string {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
tpl.Execute(buf, data)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func compileTemplate(text string) (*template.Template, error) {
|
||||
return template.New("").Parse(text)
|
||||
}
|
||||
Reference in New Issue
Block a user