Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f7f35a2028
|
|||
|
5ab1c1c9d4
|
|||
|
10edc210b2
|
|||
|
beca1c0aaa
|
|||
|
8bbe60a8b0
|
|||
|
7974d0f3fd
|
|||
|
2130b3b3dd
|
|||
|
|
e8dc6e5789 | ||
|
|
5d78504138 | ||
|
|
896c0d28da | ||
|
|
7657f99ac9 | ||
|
|
6e0816c077 | ||
|
|
f6ca8816e7 | ||
|
|
dc0170a4c8 | ||
|
|
e07b2fb574 | ||
|
|
cb857b6d10 | ||
|
|
efb6de4c4c | ||
|
|
c3d22a40db | ||
|
|
7e5187764b | ||
|
|
1c44776138 | ||
|
|
d33688d189 | ||
|
|
29b2bf457a | ||
|
|
de4d2451d5 | ||
|
|
79676357f3 | ||
|
|
2963a9f610 | ||
|
|
c186e4b292 | ||
|
|
3f82108f98 | ||
|
|
33bb722e06 | ||
|
|
3c343fb0a2 | ||
|
|
f9f5c278ec | ||
|
|
43510e18e2 | ||
|
|
df31238a1f | ||
|
|
a6b879bcee | ||
|
|
c4f7544640 | ||
|
|
ae3a18591f | ||
|
|
7cbc234c42 | ||
|
|
0b63b0e63d | ||
|
|
5d34e1cf38 | ||
|
|
581b1465e6 | ||
|
|
8bf7a9921e | ||
|
|
f17ee284a9 | ||
|
|
c2c9898d68 | ||
|
|
0ea950f819 | ||
|
|
0e93d140e8 | ||
|
|
aacd04b2b8 | ||
|
|
3a68b19633 | ||
|
|
423c8d3a23 | ||
|
|
f48b5feb11 | ||
|
|
b7f7f934ec | ||
|
|
927aca5ac0 | ||
|
|
18705d1faf | ||
|
|
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 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
gin-bin
|
/config.yml
|
||||||
example.config.local.json
|
/config.json
|
||||||
|
examples/
|
||||||
|
/cachet-monitor
|
||||||
|
/cli
|
||||||
|
build
|
||||||
|
|||||||
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/
|
||||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 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.
|
||||||
85
api.go
Normal file
85
api.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
return res, body, err
|
||||||
|
}
|
||||||
12
cli/Dockerfile
Normal file
12
cli/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# build stage
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache --virtual .build-deps \
|
||||||
|
git gcc libc-dev;
|
||||||
|
ENV GO111MODULE on
|
||||||
|
WORKDIR /go/main
|
||||||
|
ADD go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
# WORKDIR /go/src/status-monitor
|
||||||
|
ADD main.go ./
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -a -installsuffix cgo -o main main.go
|
||||||
3
cli/Makefile
Normal file
3
cli/Makefile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build:
|
||||||
|
docker build .
|
||||||
|
|
||||||
12
cli/go.mod
Normal file
12
cli/go.mod
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/castawaylabs/cachet-monitor v1.1.0 // indirect
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
|
github.com/milkinteractive/cachet-monitor v1.1.0
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2
|
||||||
|
github.com/sirupsen/logrus v1.4.2
|
||||||
|
gopkg.in/yaml.v2 v2.2.7
|
||||||
|
)
|
||||||
20
cli/go.sum
Normal file
20
cli/go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/castawaylabs/cachet-monitor v1.1.0 h1:T8NshhwHjEIbdlWkeRBuYdnU2ktpBnZpwgh6nxrp62w=
|
||||||
|
github.com/castawaylabs/cachet-monitor v1.1.0/go.mod h1:FBK1+4+fLd80wd/+U4Zy8YshPlpgWEvup79AoapOZ2E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/milkinteractive/cachet-monitor v1.1.0 h1:IyX/gYVDfSEwO4kbNMs9W4hLt5FXIV/ig/4c1nFVlfU=
|
||||||
|
github.com/milkinteractive/cachet-monitor v1.1.0/go.mod h1:Y+g0f0C8fAj2ub5RhfQgh/oU85pYx6QbL/56WMFkmVo=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
185
cli/main.go
185
cli/main.go
@@ -3,89 +3,130 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
cachet "github.com/castawaylabs/cachet-monitor"
|
docopt "github.com/docopt/docopt-go"
|
||||||
|
cachet "github.com/milkinteractive/cachet-monitor"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath string
|
const usage = `cachet-monitor
|
||||||
var systemName string
|
|
||||||
var logPath string
|
Usage:
|
||||||
|
cachet-monitor (-c PATH | --config PATH) [--log=LOGPATH] [--name=NAME] [--immediate] [--restarted]
|
||||||
|
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)
|
||||||
|
--restarted Get open incidents before start monitoring (if monitor died or restarted)
|
||||||
|
|
||||||
|
Environment varaibles:
|
||||||
|
CACHET_API override API url from configuration
|
||||||
|
CACHET_TOKEN override API token from configuration
|
||||||
|
CACHET_DEV set to enable dev logging`
|
||||||
|
|
||||||
|
var version string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.StringVar(&configPath, "c", "/etc/cachet-monitor.config.json", "Config path")
|
arguments, _ := docopt.Parse(usage, nil, true, version, false)
|
||||||
flag.StringVar(&systemName, "name", "", "System Name")
|
|
||||||
flag.StringVar(&logPath, "log", "", "Log path")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
cfg, err := getConfiguration(configPath)
|
cfg, err := getConfiguration(arguments["--config"].(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
logrus.Panicf("Unable to start (reading config): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(systemName) > 0 {
|
if immediate, ok := arguments["--immediate"]; ok {
|
||||||
cfg.SystemName = systemName
|
cfg.Immediate = immediate.(bool)
|
||||||
}
|
}
|
||||||
if len(logPath) > 0 {
|
|
||||||
cfg.LogPath = logPath
|
if restarted, ok := arguments["--restarted"]; ok {
|
||||||
|
cfg.Restarted = restarted.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name := arguments["--name"]; name != nil {
|
||||||
|
cfg.SystemName = name.(string)
|
||||||
|
}
|
||||||
|
logrus.SetOutput(getLogger(arguments["--log"]))
|
||||||
|
|
||||||
if len(os.Getenv("CACHET_API")) > 0 {
|
if len(os.Getenv("CACHET_API")) > 0 {
|
||||||
cfg.APIUrl = os.Getenv("CACHET_API")
|
cfg.API.URL = os.Getenv("CACHET_API")
|
||||||
}
|
}
|
||||||
if len(os.Getenv("CACHET_TOKEN")) > 0 {
|
if len(os.Getenv("CACHET_TOKEN")) > 0 {
|
||||||
cfg.APIToken = os.Getenv("CACHET_TOKEN")
|
cfg.API.Token = os.Getenv("CACHET_TOKEN")
|
||||||
|
}
|
||||||
|
if len(os.Getenv("CACHET_DEV")) > 0 {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.ValidateConfiguration(); err != nil {
|
if valid := cfg.Validate(); !valid {
|
||||||
panic(err)
|
logrus.Errorf("Invalid configuration")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Logger.Printf("System: %s\nAPI: %s\nMonitors: %d\n\n", cfg.SystemName, cfg.APIUrl, len(cfg.Monitors))
|
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{}
|
wg := &sync.WaitGroup{}
|
||||||
for _, mon := range cfg.Monitors {
|
for index, monitor := range cfg.Monitors {
|
||||||
go mon.Start(cfg, wg)
|
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)
|
signals := make(chan os.Signal, 1)
|
||||||
signal.Notify(signals, os.Interrupt, os.Kill)
|
signal.Notify(signals, os.Interrupt, os.Kill)
|
||||||
<-signals
|
<-signals
|
||||||
|
|
||||||
cfg.Logger.Println("Abort: Waiting monitors to finish")
|
logrus.Warnf("Abort: Waiting monitors to finish")
|
||||||
for _, mon := range cfg.Monitors {
|
for _, mon := range cfg.Monitors {
|
||||||
mon.Stop()
|
mon.GetMonitor().ClockStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogger(logPath string) *log.Logger {
|
func getLogger(logPath interface{}) *os.File {
|
||||||
var logWriter = os.Stdout
|
if logPath == nil || len(logPath.(string)) == 0 {
|
||||||
var err error
|
return os.Stdout
|
||||||
|
|
||||||
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
|
file, err := os.Create(logPath.(string))
|
||||||
if len(os.Getenv("CACHET_DEV")) > 0 {
|
if err != nil {
|
||||||
flags = 0
|
logrus.Errorf("Unable to open file '%v' for logging: \n%v", logPath, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return log.New(logWriter, "", flags)
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfiguration(path string) (*cachet.CachetMonitor, error) {
|
func getConfiguration(path string) (*cachet.CachetMonitor, error) {
|
||||||
@@ -98,26 +139,74 @@ func getConfiguration(path string) (*cachet.CachetMonitor, error) {
|
|||||||
// download config
|
// download config
|
||||||
response, err := http.Get(path)
|
response, err := http.Get(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Cannot download network config: " + err.Error())
|
logrus.Warn("Unable to download network configuration")
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
data, _ = ioutil.ReadAll(response.Body)
|
data, _ = ioutil.ReadAll(response.Body)
|
||||||
|
|
||||||
fmt.Println("Downloaded network configuration.")
|
logrus.Info("Downloaded network configuration.")
|
||||||
} else {
|
} else {
|
||||||
data, err = ioutil.ReadFile(path)
|
data, err = ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Config file '" + path + "' missing!")
|
return nil, errors.New("Unable to open file: '" + path + "'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
|
||||||
fmt.Println(err)
|
err = yaml.Unmarshal(data, &cfg)
|
||||||
return nil, errors.New("Cannot parse config!")
|
} else {
|
||||||
|
err = json.Unmarshal(data, &cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Logger = getLogger(cfg.LogPath)
|
if err != nil {
|
||||||
|
logrus.Warnf("Unable to parse configuration file")
|
||||||
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
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
|
||||||
|
case "tcp":
|
||||||
|
var s cachet.TCPMonitor
|
||||||
|
err = mapstructure.Decode(rawMonitor, &s)
|
||||||
|
t = &s
|
||||||
|
|
||||||
|
case "icmp":
|
||||||
|
var s cachet.ICMPMonitor
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
96
config.go
96
config.go
@@ -1,65 +1,97 @@
|
|||||||
package cachet
|
package cachet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CachetMonitor struct {
|
type CachetMonitor struct {
|
||||||
Logger *log.Logger `json:"-"`
|
SystemName string `json:"system_name" yaml:"system_name"`
|
||||||
|
DateFormat string `json:"date_format" yaml:"date_format"`
|
||||||
|
SlackWebhook string `json:"slack_webhook" yaml:"slack_webhook"`
|
||||||
|
API CachetAPI `json:"api"`
|
||||||
|
RawMonitors []map[string]interface{} `json:"monitors" yaml:"monitors"`
|
||||||
|
|
||||||
APIUrl string `json:"api_url"`
|
Monitors []MonitorInterface `json:"-" yaml:"-"`
|
||||||
APIToken string `json:"api_token"`
|
Immediate bool `json:"-" yaml:"-"`
|
||||||
SystemName string `json:"system_name"`
|
Restarted bool `json:"-" yaml:"-"`
|
||||||
LogPath string `json:"log_path"`
|
|
||||||
InsecureAPI bool `json:"insecure_api"`
|
|
||||||
|
|
||||||
Monitors []*Monitor `json:"monitors"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *CachetMonitor) ValidateConfiguration() error {
|
// Validate configuration
|
||||||
if cfg.Logger == nil {
|
func (cfg *CachetMonitor) Validate() bool {
|
||||||
cfg.Logger = log.New(os.Stdout, "", log.Llongfile|log.Ldate|log.Ltime)
|
valid := true
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.SystemName) == 0 {
|
if len(cfg.SystemName) == 0 {
|
||||||
// get hostname
|
// get hostname
|
||||||
cfg.SystemName = getHostname()
|
cfg.SystemName = getHostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.APIToken) == 0 || len(cfg.APIUrl) == 0 {
|
if len(cfg.DateFormat) == 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")
|
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 {
|
if len(cfg.Monitors) == 0 {
|
||||||
return errors.New("No monitors defined!\nSee sample configuration: https://github.com/castawaylabs/cachet-monitor/blob/master/example.config.json\n")
|
logrus.Warnf("No monitors defined!\nSee help for example configuration")
|
||||||
|
valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, monitor := range cfg.Monitors {
|
for index, monitor := range cfg.Monitors {
|
||||||
if err := monitor.ValidateConfiguration(); err != nil {
|
if errs := monitor.Validate(); len(errs) > 0 {
|
||||||
return err
|
logrus.Warnf("Monitor validation errors (index %d): %v", index, "\n - "+strings.Join(errs, "\n - "))
|
||||||
|
valid = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHostname returns id of the current system
|
// getHostname returns id of the current system
|
||||||
func getHostname() string {
|
func getHostname() string {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil || len(hostname) == 0 {
|
if err == nil && len(hostname) > 0 {
|
||||||
addrs, err := net.InterfaceAddrs()
|
return hostname
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
return addr.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MainUrl(cfg *CachetMonitor) string {
|
||||||
|
var url = cfg.API.URL
|
||||||
|
var index = strings.Index(url,"/api/")
|
||||||
|
return url[0:index]
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
20
example.cachet-monitor.service
Normal file
20
example.cachet-monitor.service
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Cachet Monitor
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
#After=mysqld.service
|
||||||
|
#After=postgresql.service
|
||||||
|
#After=memcached.service
|
||||||
|
#After=redis.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
WorkingDirectory=/root
|
||||||
|
ExecStart=/root/cachet-monitor -c /etc/cachet-monitor.yaml
|
||||||
|
Restart=always
|
||||||
|
Environment=USER=root HOME=/root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,17 +1,71 @@
|
|||||||
{
|
{
|
||||||
"api_url": "https://demo.cachethq.io/api/v1",
|
"api": {
|
||||||
"api_token": "9yMHsdioQosnyVK4iCVR",
|
"url": "https://demo.cachethq.io/api/v1",
|
||||||
"interval": 5,
|
"token": "9yMHsdioQosnyVK4iCVR",
|
||||||
|
"insecure": false
|
||||||
|
},
|
||||||
|
"date_format": "02/01/2006 15:04:05 MST",
|
||||||
"monitors": [
|
"monitors": [
|
||||||
{
|
{
|
||||||
"name": "nodegear frontend",
|
"active": false,
|
||||||
"url": "https://nodegear.io/ping",
|
"name": "google",
|
||||||
"metric_id": 1,
|
"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,
|
"threshold": 80,
|
||||||
"component_id": null,
|
"headers": {
|
||||||
|
"Authorization": "Basic <hash>"
|
||||||
|
},
|
||||||
"expected_status_code": 200,
|
"expected_status_code": 200,
|
||||||
"strict_tls": true
|
"expected_body": "P.*NG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"active": true,
|
||||||
|
"name": "dns",
|
||||||
|
"target": "opnsense.bennetg.dev.",
|
||||||
|
"question": "a",
|
||||||
|
"component_id": 2,
|
||||||
|
"interval": 1,
|
||||||
|
"timeout": 1,
|
||||||
|
"dns": "10.0.0.60:53",
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"exact": "10.0.0.1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tcp",
|
||||||
|
"name": "TCP",
|
||||||
|
"active": false,
|
||||||
|
"target": "10.0.0.1",
|
||||||
|
"interval": 10,
|
||||||
|
"timeout": 1,
|
||||||
|
"port": "443",
|
||||||
|
"component_id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"type": "icmp",
|
||||||
|
"name": "icmp",
|
||||||
|
"target": "10.0.0.1",
|
||||||
|
"component_id": 4,
|
||||||
|
"interval": 10,
|
||||||
|
"timeout": 3
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"insecure_api": false
|
|
||||||
}
|
}
|
||||||
|
|||||||
70
example.config.yml
Normal file
70
example.config.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
slack_webhook: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
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:
|
||||||
|
- 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.
|
||||||
|
- name: smtpnine
|
||||||
|
target: smtp.nine.ch
|
||||||
|
type: tcp
|
||||||
|
port: 25
|
||||||
|
component_id: 4
|
||||||
14
example.upstart.conf
Normal file
14
example.upstart.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
description "Cachet Monitor"
|
||||||
|
|
||||||
|
start on startup
|
||||||
|
|
||||||
|
env USER=root
|
||||||
|
env HOME=/root
|
||||||
|
|
||||||
|
setuid root
|
||||||
|
setgid root
|
||||||
|
chdir /root
|
||||||
|
|
||||||
|
script
|
||||||
|
exec cachet-monitor -c /cachet-monitor.json --immediate
|
||||||
|
end script
|
||||||
47
go-executable-build.sh
Executable file
47
go-executable-build.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
package=$1
|
||||||
|
if [[ -z "$package" ]]; then
|
||||||
|
echo "usage: $0 <package-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
package_name=$package
|
||||||
|
|
||||||
|
#the full list of the platforms: https://golang.org/doc/install/source#environment
|
||||||
|
platforms=(
|
||||||
|
# "darwin/386"
|
||||||
|
# "darwin/amd64"
|
||||||
|
# "darwin/arm"
|
||||||
|
# "darwin/arm64"
|
||||||
|
# "dragonfly/amd64"
|
||||||
|
# "freebsd/386"
|
||||||
|
# "freebsd/amd64"
|
||||||
|
# "freebsd/arm"
|
||||||
|
# "linux/386"
|
||||||
|
"linux/amd64"
|
||||||
|
# "linux/arm"
|
||||||
|
# "linux/arm64"
|
||||||
|
# "netbsd/386"
|
||||||
|
# "netbsd/amd64"
|
||||||
|
# "netbsd/arm"
|
||||||
|
# "openbsd/386"
|
||||||
|
# "openbsd/amd64"
|
||||||
|
# "openbsd/arm"
|
||||||
|
# "plan9/386"
|
||||||
|
# "plan9/amd64"
|
||||||
|
# "solaris/amd64"
|
||||||
|
# "windows/amd64"
|
||||||
|
# "windows/386"
|
||||||
|
)
|
||||||
|
|
||||||
|
for platform in "${platforms[@]}"; do
|
||||||
|
platform_split=(${platform//\// })
|
||||||
|
GOOS=${platform_split[0]}
|
||||||
|
GOARCH=${platform_split[1]}
|
||||||
|
output_name=build/$package_name'-'$GOOS'-'$GOARCH
|
||||||
|
if [ $GOOS = "windows" ]; then
|
||||||
|
output_name+='.exe'
|
||||||
|
fi
|
||||||
|
|
||||||
|
env CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package
|
||||||
|
done
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module github.com/milkinteractive/cachet-monitor
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chenjiandongx/yap v0.2.0 // indirect
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||||
|
github.com/miekg/dns v1.1.53
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2
|
||||||
|
github.com/prometheus-community/pro-bing v0.1.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.0
|
||||||
|
github.com/stretchr/objx v0.1.1 // indirect
|
||||||
|
golang.org/x/crypto v0.8.0 // indirect
|
||||||
|
golang.org/x/tools v0.8.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.7
|
||||||
|
)
|
||||||
146
go.sum
Normal file
146
go.sum
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
github.com/chenjiandongx/yap v0.2.0 h1:QggNKR8wJdvHGFCWAWwRgPb5O4oTG3BnTqg03ukrKvg=
|
||||||
|
github.com/chenjiandongx/yap v0.2.0/go.mod h1:gCZjg9aqv/RcLpWictmDlDSUk+vBS/GAxoJbLJnSw48=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||||
|
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
|
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||||
|
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
|
github.com/miekg/dns v1.1.34 h1:SgTzfkN+oLoIHF1bgUP+C71mzuDl3AhLApHzCCIAMWM=
|
||||||
|
github.com/miekg/dns v1.1.34/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
|
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||||
|
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus-community/pro-bing v0.1.0 h1:zjzLGhfNPP0bP1OlzGB+SJcguOViw7df12LPg2vUJh8=
|
||||||
|
github.com/prometheus-community/pro-bing v0.1.0/go.mod h1:BpWlHurD9flHtzq8wrh8QGWYz9ka9z9ZJAyOel8ej58=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||||
|
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||||
|
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200108215511-5d647ca15757 h1:pJ9H8lzdBh301qPX4VpwJ8TRpLt1IhNK1PxVOICyP54=
|
||||||
|
golang.org/x/crypto v0.0.0-20200108215511-5d647ca15757/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||||
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
||||||
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||||
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||||
|
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg=
|
||||||
|
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220615171555-694bf12d69de/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200107162124-548cf772de50 h1:YvQ10rzcqWXLlJZ3XCUoO25savxmscf4+SC+ZqiCHhA=
|
||||||
|
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE=
|
||||||
|
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||||
|
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
160
http.go
160
http.go
@@ -1,58 +1,156 @@
|
|||||||
package cachet
|
package cachet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"crypto/md5"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (monitor *CachetMonitor) makeRequest(requestType string, url string, reqBody []byte) (*http.Response, []byte, error) {
|
// Investigating template
|
||||||
req, err := http.NewRequest(requestType, monitor.APIUrl+url, bytes.NewBuffer(reqBody))
|
var defaultHTTPInvestigatingTpl = MessageTemplate{
|
||||||
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
|
Message: `{{ .Monitor.Name }} check **failed** (server time: {{ .now }})
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
{{ .FailReason }}`,
|
||||||
req.Header.Set("X-Cachet-Token", monitor.APIToken)
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
// Fixed template
|
||||||
if monitor.InsecureAPI == true {
|
var defaultHTTPFixedTpl = MessageTemplate{
|
||||||
client.Transport = &http.Transport{
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
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
|
||||||
|
|
||||||
|
// content check
|
||||||
|
ExpectedMd5Sum string `mapstructure:"expected_md5sum"`
|
||||||
|
ExpectedLength int `mapstructure:"expected_length"`
|
||||||
|
|
||||||
|
// data
|
||||||
|
Data string `mapstructure:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test
|
||||||
|
func (monitor *HTTPMonitor) test() bool {
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if monitor.Data != "" {
|
||||||
|
dataBuffer := strings.NewReader(monitor.Data)
|
||||||
|
req, err = http.NewRequest(monitor.Method, monitor.Target, dataBuffer)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
responseLength := len(string(responseBody))
|
||||||
|
if err != nil {
|
||||||
|
monitor.lastFailReason = err.Error()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.ExpectedLength > 0 && responseLength != monitor.ExpectedLength {
|
||||||
|
monitor.lastFailReason = "Expected response body length: " + strconv.Itoa(monitor.ExpectedLength) + ", got: " + strconv.Itoa(responseLength)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.ExpectedMd5Sum != "" {
|
||||||
|
sum := fmt.Sprintf("%x", (md5.Sum(responseBody)))
|
||||||
|
if strings.Compare(sum, monitor.ExpectedMd5Sum) != 0 {
|
||||||
|
monitor.lastFailReason = "Expected respsone body MD5 checksum: " + monitor.ExpectedMd5Sum + ", got: " + sum
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := client.Do(req)
|
if monitor.bodyRegexp != nil {
|
||||||
if err != nil {
|
if !monitor.bodyRegexp.Match(responseBody) {
|
||||||
return nil, []byte{}, err
|
monitor.lastFailReason = "Unexpected body: " + string(responseBody) + ".\nExpected to match: " + monitor.ExpectedBody
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
return true
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
|
||||||
|
|
||||||
return res, body, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMetric sends lag metric point
|
// TODO: test
|
||||||
func (monitor *Monitor) SendMetric(delay int64) error {
|
func (mon *HTTPMonitor) Validate() []string {
|
||||||
if monitor.MetricID == 0 {
|
mon.Template.Investigating.SetDefault(defaultHTTPInvestigatingTpl)
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(&map[string]interface{}{
|
if len(mon.ExpectedBody) == 0 && mon.ExpectedStatusCode == 0 {
|
||||||
"value": delay,
|
errs = append(errs, "Both 'expected_body' and 'expected_status_code' fields empty")
|
||||||
})
|
|
||||||
|
|
||||||
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
|
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 getMs() int64 {
|
func (mon *HTTPMonitor) Describe() []string {
|
||||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
features := mon.AbstractMonitor.Describe()
|
||||||
|
features = append(features, "Method: "+mon.Method)
|
||||||
|
|
||||||
|
return features
|
||||||
}
|
}
|
||||||
|
|||||||
77
icmp.go
Normal file
77
icmp.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Thanks go to https://github.com/Soontao/cachet-monitor/blob/master/tcp.go
|
||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/chenjiandongx/yap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Investigating template
|
||||||
|
var defaultICMPInvestigatingTpl = MessageTemplate{
|
||||||
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
|
Message: `{{ .Monitor.Name }} check **failed** (server time: {{ .now }})
|
||||||
|
|
||||||
|
{{ .FailReason }}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed template
|
||||||
|
var defaultICMPFixedTpl = MessageTemplate{
|
||||||
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
|
Message: `**Resolved** - {{ .now }}
|
||||||
|
|
||||||
|
Down seconds: {{ .downSeconds }}s`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICMPMonitor struct
|
||||||
|
type ICMPMonitor struct {
|
||||||
|
AbstractMonitor `mapstructure:",squash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckICMPAlive func
|
||||||
|
func CheckICMPAlive(ip string, timeout int) (bool, error) {
|
||||||
|
|
||||||
|
pg, err := yap.NewPinger()
|
||||||
|
if err != nil {
|
||||||
|
defer pg.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
response := pg.Call(yap.Request{
|
||||||
|
Target: ip,
|
||||||
|
Count: 1,
|
||||||
|
Timeout: timeout * 1000})
|
||||||
|
|
||||||
|
if response.Error != nil {
|
||||||
|
return false, response.Error
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if it available
|
||||||
|
func (m *ICMPMonitor) test() bool {
|
||||||
|
if alive, e := CheckICMPAlive(m.Target, int(m.Timeout)); alive {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
m.lastFailReason = fmt.Sprintf("ICMP check failed: %v", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
func (m *ICMPMonitor) Validate() []string {
|
||||||
|
|
||||||
|
// set incident temp
|
||||||
|
m.Template.Investigating.SetDefault(defaultICMPInvestigatingTpl)
|
||||||
|
m.Template.Fixed.SetDefault(defaultICMPFixedTpl)
|
||||||
|
|
||||||
|
// super.Validate()
|
||||||
|
errs := m.AbstractMonitor.Validate()
|
||||||
|
|
||||||
|
if m.Target == "" {
|
||||||
|
errs = append(errs, "Target is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
91
incident.go
91
incident.go
@@ -4,6 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Incident Cachet data model
|
// Incident Cachet data model
|
||||||
@@ -12,13 +15,39 @@ type Incident struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Visible int `json"visible"`
|
Visible int `json:"visible"`
|
||||||
Notify bool `json:"notify"`
|
Notify bool `json:"notify"`
|
||||||
|
|
||||||
ComponentID int `json:"component_id"`
|
ComponentID int `json:"component_id"`
|
||||||
ComponentStatus int `json:"component_status"`
|
ComponentStatus int `json:"component_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Get the last still open incident
|
||||||
|
func (mon *AbstractMonitor) Get(cfg *CachetMonitor) (*Incident, error) {
|
||||||
|
requestType := "GET"
|
||||||
|
requestURL := fmt.Sprintf("/incidents?component_id=%d", mon.ComponentID)
|
||||||
|
_, body, err := cfg.API.NewRequest(requestType, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := make([]Incident, 0)
|
||||||
|
if err := json.Unmarshal(body.Data, &data); err != nil {
|
||||||
|
return nil, fmt.Errorf("Cannot parse incident body: %v, %v", err, string(body.Data))
|
||||||
|
}
|
||||||
|
//filter out resolved incidents
|
||||||
|
openIncidents := make([]Incident, 0)
|
||||||
|
for _, i := range data {
|
||||||
|
if i.Status < 4 {
|
||||||
|
openIncidents = append(openIncidents, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(openIncidents) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &openIncidents[0], nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Send - Create or Update incident
|
// Send - Create or Update incident
|
||||||
func (incident *Incident) Send(cfg *CachetMonitor) error {
|
func (incident *Incident) Send(cfg *CachetMonitor) error {
|
||||||
switch incident.Status {
|
switch incident.Status {
|
||||||
@@ -33,7 +62,7 @@ func (incident *Incident) Send(cfg *CachetMonitor) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg.Logger.Printf("cannot fetch component: %v", err)
|
logrus.Warnf("cannot fetch component: %v", err)
|
||||||
}
|
}
|
||||||
case 4:
|
case 4:
|
||||||
// fixed
|
// fixed
|
||||||
@@ -49,30 +78,31 @@ func (incident *Incident) Send(cfg *CachetMonitor) error {
|
|||||||
|
|
||||||
jsonBytes, _ := json.Marshal(incident)
|
jsonBytes, _ := json.Marshal(incident)
|
||||||
|
|
||||||
resp, body, err := cfg.makeRequest(requestType, requestURL, jsonBytes)
|
resp, body, err := cfg.API.NewRequest(requestType, requestURL, jsonBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
Incident struct {
|
ID int `json:"id"`
|
||||||
ID int `json:"id"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &data); err != nil {
|
if err := json.Unmarshal(body.Data, &data); err != nil {
|
||||||
return fmt.Errorf("Cannot parse incident body: %v, %v", err, string(body))
|
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 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("Could not create/update incident!")
|
return fmt.Errorf("Could not create/update incident")
|
||||||
|
}
|
||||||
|
// send slack message
|
||||||
|
if cfg.SlackWebhook != "" {
|
||||||
|
incident.sendSlack(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) {
|
func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) {
|
||||||
resp, body, err := cfg.makeRequest("GET", "/components/"+strconv.Itoa(incident.ComponentID), nil)
|
resp, body, err := cfg.API.NewRequest("GET", "/components/"+strconv.Itoa(incident.ComponentID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -82,15 +112,13 @@ func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
Component struct {
|
Status int `json:"status"`
|
||||||
Status int `json:"status"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &data); err != nil {
|
if err := json.Unmarshal(body.Data, &data); err != nil {
|
||||||
return 0, fmt.Errorf("Cannot parse component body: %v. Err = %v", string(body), err)
|
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
|
// SetInvestigating sets status to Investigating
|
||||||
@@ -112,3 +140,30 @@ func (incident *Incident) SetWatching() {
|
|||||||
func (incident *Incident) SetFixed() {
|
func (incident *Incident) SetFixed() {
|
||||||
incident.Status = 4
|
incident.Status = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send slack message
|
||||||
|
func (incident *Incident) sendSlack(cfg *CachetMonitor) {
|
||||||
|
color := "#bf1932" //red
|
||||||
|
if incident.ComponentStatus == 1 {
|
||||||
|
color = "#36a64f" //green
|
||||||
|
}
|
||||||
|
titleLink := MainUrl(cfg) + "/dashboard/incidents/" + strconv.Itoa(incident.ID)
|
||||||
|
slack := Slack{
|
||||||
|
WebhookURL: cfg.SlackWebhook,
|
||||||
|
Attachments: []Attachments{
|
||||||
|
Attachments{
|
||||||
|
Fallback: incident.Name,
|
||||||
|
Color: color,
|
||||||
|
Title: incident.Name,
|
||||||
|
TitleLink: titleLink,
|
||||||
|
Text: incident.Message,
|
||||||
|
Footer: "Cachet Monitor",
|
||||||
|
FooterIcon: "https://i.imgur.com/spck1w6.png",
|
||||||
|
Ts: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
err := slack.SendSlackNotification()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("Cannot send slack message. Err = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
368
monitor.go
368
monitor.go
@@ -1,41 +1,60 @@
|
|||||||
package cachet
|
package cachet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const HttpTimeout = time.Duration(time.Second)
|
const DefaultInterval = time.Second * 60
|
||||||
const DefaultInterval = 60
|
const DefaultTimeout = time.Second
|
||||||
const DefaultTimeFormat = "15:04:05 Jan 2 MST"
|
const DefaultTimeFormat = "15:04:05 Jan 2 MST"
|
||||||
|
const HistorySize = 10
|
||||||
|
|
||||||
// Monitor data model
|
type MonitorInterface interface {
|
||||||
type Monitor struct {
|
ClockStart(*CachetMonitor, MonitorInterface, *sync.WaitGroup)
|
||||||
Name string `json:"name"`
|
ClockStop()
|
||||||
URL string `json:"url"`
|
tick(MonitorInterface)
|
||||||
Method string `json:"method"`
|
test() bool
|
||||||
StrictTLS bool `json:"strict_tls"`
|
|
||||||
CheckInterval time.Duration `json:"interval"`
|
|
||||||
|
|
||||||
MetricID int `json:"metric_id"`
|
Validate() []string
|
||||||
ComponentID int `json:"component_id"`
|
GetMonitor() *AbstractMonitor
|
||||||
|
Describe() []string
|
||||||
|
}
|
||||||
|
|
||||||
// Threshold = percentage
|
// AbstractMonitor data model
|
||||||
Threshold float32 `json:"threshold"`
|
type AbstractMonitor struct {
|
||||||
ExpectedStatusCode int `json:"expected_status_code"`
|
Name string
|
||||||
// compiled to Regexp
|
Target string
|
||||||
ExpectedBody string `json:"expected_body"`
|
Active bool
|
||||||
bodyRegexp *regexp.Regexp
|
|
||||||
|
|
||||||
history []bool
|
// (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
|
lastFailReason string
|
||||||
incident *Incident
|
incident *Incident
|
||||||
config *CachetMonitor
|
config *CachetMonitor
|
||||||
@@ -44,34 +63,84 @@ type Monitor struct {
|
|||||||
stopC chan bool
|
stopC chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mon *Monitor) Start(cfg *CachetMonitor, wg *sync.WaitGroup) {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mon.Active {
|
||||||
|
features = append(features, "Active: No")
|
||||||
|
}
|
||||||
|
|
||||||
|
return features
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mon *AbstractMonitor) ClockStart(cfg *CachetMonitor, iface MonitorInterface, wg *sync.WaitGroup) {
|
||||||
|
if !mon.Active {
|
||||||
|
return
|
||||||
|
};
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
mon.config = cfg
|
mon.config = cfg
|
||||||
mon.stopC = make(chan bool)
|
mon.stopC = make(chan bool)
|
||||||
|
if cfg.Immediate {
|
||||||
mon.config.Logger.Printf(" Starting %s: %d seconds check interval\n - %v %s", mon.Name, mon.CheckInterval, mon.Method, mon.URL)
|
mon.tick(iface)
|
||||||
|
|
||||||
// print features
|
|
||||||
if mon.ExpectedStatusCode > 0 {
|
|
||||||
mon.config.Logger.Printf(" - Expect HTTP %d", mon.ExpectedStatusCode)
|
|
||||||
}
|
|
||||||
if len(mon.ExpectedBody) > 0 {
|
|
||||||
mon.config.Logger.Printf(" - Expect Body to match \"%v\"", mon.ExpectedBody)
|
|
||||||
}
|
|
||||||
if mon.MetricID > 0 {
|
|
||||||
mon.config.Logger.Printf(" - Log lag to metric id %d\n", mon.MetricID)
|
|
||||||
}
|
|
||||||
if mon.ComponentID > 0 {
|
|
||||||
mon.config.Logger.Printf(" - Update component id %d\n\n", mon.ComponentID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mon.Tick()
|
if cfg.Restarted {
|
||||||
|
initialIncident, err := mon.Get(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warn("could not fetch initial incident: %v", err)
|
||||||
|
}
|
||||||
|
if initialIncident != nil {
|
||||||
|
mon.incident = initialIncident
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(mon.CheckInterval * time.Second)
|
ticker := time.NewTicker(mon.Interval * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
mon.Tick()
|
mon.tick(iface)
|
||||||
case <-mon.stopC:
|
case <-mon.stopC:
|
||||||
wg.Done()
|
wg.Done()
|
||||||
return
|
return
|
||||||
@@ -79,174 +148,123 @@ func (mon *Monitor) Start(cfg *CachetMonitor, wg *sync.WaitGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (monitor *Monitor) Stop() {
|
func (mon *AbstractMonitor) ClockStop() {
|
||||||
if monitor.Stopped() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
close(monitor.stopC)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor *Monitor) Stopped() bool {
|
|
||||||
select {
|
select {
|
||||||
case <-monitor.stopC:
|
case <-mon.stopC:
|
||||||
return true
|
return
|
||||||
default:
|
default:
|
||||||
return false
|
close(mon.stopC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (monitor *Monitor) Tick() {
|
func (mon *AbstractMonitor) test() bool { return false }
|
||||||
|
|
||||||
|
func (mon *AbstractMonitor) tick(iface MonitorInterface) {
|
||||||
reqStart := getMs()
|
reqStart := getMs()
|
||||||
isUp := monitor.doRequest()
|
up := iface.test()
|
||||||
lag := getMs() - reqStart
|
lag := getMs() - reqStart
|
||||||
|
|
||||||
if len(monitor.history) == 9 {
|
histSize := HistorySize
|
||||||
monitor.config.Logger.Printf("%v is now saturated\n", monitor.Name)
|
if mon.ThresholdCount {
|
||||||
|
histSize = int(mon.Threshold)
|
||||||
}
|
}
|
||||||
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 {
|
if len(mon.history) == histSize-1 {
|
||||||
monitor.SendMetric(lag)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (monitor *Monitor) doRequest() bool {
|
// TODO: test
|
||||||
client := &http.Client{
|
|
||||||
Timeout: HttpTimeout,
|
|
||||||
}
|
|
||||||
if monitor.StrictTLS == false {
|
|
||||||
client.Transport = &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Get(monitor.URL)
|
|
||||||
if err != nil {
|
|
||||||
monitor.lastFailReason = err.Error()
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if monitor.ExpectedStatusCode > 0 && resp.StatusCode != monitor.ExpectedStatusCode {
|
|
||||||
monitor.lastFailReason = "Unexpected response code: " + strconv.Itoa(resp.StatusCode) + ". Expected " + strconv.Itoa(monitor.ExpectedStatusCode)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if monitor.bodyRegexp != nil {
|
|
||||||
// check body
|
|
||||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
monitor.lastFailReason = err.Error()
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
match := monitor.bodyRegexp.Match(responseBody)
|
|
||||||
if !match {
|
|
||||||
monitor.lastFailReason = "Unexpected body: " + string(responseBody) + ". Expected to match " + monitor.ExpectedBody
|
|
||||||
}
|
|
||||||
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
||||||
func (monitor *Monitor) AnalyseData() {
|
func (mon *AbstractMonitor) AnalyseData() {
|
||||||
// look at the past few incidents
|
// look at the past few incidents
|
||||||
numDown := 0
|
numDown := 0
|
||||||
for _, wasUp := range monitor.history {
|
for _, wasUp := range mon.history {
|
||||||
if wasUp == false {
|
if wasUp == false {
|
||||||
numDown++
|
numDown++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t := (float32(numDown) / float32(len(monitor.history))) * 100
|
t := (float32(numDown) / float32(len(mon.history))) * 100
|
||||||
monitor.config.Logger.Printf("%s %.2f%%/%.2f%% down at %v\n", monitor.Name, t, monitor.Threshold, time.Now().UnixNano()/int64(time.Second))
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if len(monitor.history) != 10 {
|
histSize := HistorySize
|
||||||
|
if mon.ThresholdCount {
|
||||||
|
histSize = int(mon.Threshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mon.history) != histSize {
|
||||||
// not saturated
|
// not saturated
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if t > monitor.Threshold && monitor.incident == nil {
|
triggered := (mon.ThresholdCount && numDown == int(mon.Threshold)) || (!mon.ThresholdCount && t > mon.Threshold)
|
||||||
monitor.incident = &Incident{
|
|
||||||
Name: monitor.Name + " - " + monitor.config.SystemName,
|
if triggered && mon.incident == nil {
|
||||||
ComponentID: monitor.ComponentID,
|
// create incident
|
||||||
Message: monitor.Name + " check **failed** - " + time.Now().Format(DefaultTimeFormat),
|
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,
|
Notify: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(monitor.lastFailReason) > 0 {
|
|
||||||
monitor.incident.Message += "\n\n `" + monitor.lastFailReason + "`"
|
|
||||||
}
|
|
||||||
|
|
||||||
// is down, create an incident
|
// is down, create an incident
|
||||||
monitor.config.Logger.Printf("%v creating incident. Monitor is down: %v", monitor.Name, monitor.lastFailReason)
|
l.Warnf("creating incident. Monitor is down: %v", mon.lastFailReason)
|
||||||
// set investigating status
|
// set investigating status
|
||||||
monitor.incident.SetInvestigating()
|
mon.incident.SetInvestigating()
|
||||||
// create/update incident
|
// create/update incident
|
||||||
if err := monitor.incident.Send(monitor.config); err != nil {
|
if err := mon.incident.Send(mon.config); err != nil {
|
||||||
monitor.config.Logger.Printf("Error sending incident: %v\n", err)
|
l.Printf("Error sending incident: %v", err)
|
||||||
}
|
|
||||||
} else if t < monitor.Threshold && monitor.incident != nil {
|
|
||||||
// was down, created an incident, its now ok, make it resolved.
|
|
||||||
monitor.config.Logger.Printf("%v resolved downtime incident", monitor.Name)
|
|
||||||
|
|
||||||
// resolve incident
|
|
||||||
monitor.incident.Message = "\n**Resolved** - " + time.Now().Format(DefaultTimeFormat) + "\n\n - - - \n\n" + monitor.incident.Message
|
|
||||||
monitor.incident.SetFixed()
|
|
||||||
monitor.incident.Send(monitor.config)
|
|
||||||
|
|
||||||
monitor.lastFailReason = ""
|
|
||||||
monitor.incident = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor *Monitor) ValidateConfiguration() error {
|
|
||||||
if len(monitor.ExpectedBody) > 0 {
|
|
||||||
exp, err := regexp.Compile(monitor.ExpectedBody)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.bodyRegexp = exp
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(monitor.ExpectedBody) == 0 && monitor.ExpectedStatusCode == 0 {
|
// still triggered or no incident
|
||||||
return errors.New("Nothing to check, both 'expected_body' and 'expected_status_code' fields empty")
|
if triggered || mon.incident == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if monitor.CheckInterval < 1 {
|
// was down, created an incident, its now ok, make it resolved.
|
||||||
monitor.CheckInterval = DefaultInterval
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.Method = strings.ToUpper(monitor.Method)
|
mon.lastFailReason = ""
|
||||||
switch monitor.Method {
|
mon.incident = nil
|
||||||
case "GET", "POST", "DELETE", "OPTIONS", "HEAD":
|
|
||||||
break
|
|
||||||
case "":
|
|
||||||
monitor.Method = "GET"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported check method: %v", monitor.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
if monitor.ComponentID == 0 && monitor.MetricID == 0 {
|
|
||||||
return errors.New("component_id & metric_id are unset")
|
|
||||||
}
|
|
||||||
|
|
||||||
if monitor.Threshold <= 0 {
|
|
||||||
monitor.Threshold = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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) {}
|
||||||
253
readme.md
253
readme.md
@@ -1,91 +1,212 @@
|
|||||||

|
# cachet-monitor
|
||||||
|
|
||||||
Features
|
a fork of the cachet-monitor project originally developed by CastawayLabs but abandoned. Extended with new features needed by the people.
|
||||||
--------
|
|
||||||
|
|
||||||
- [x] Creates & Resolves Incidents
|
## Features
|
||||||
- [x] Posts monitor lag to cachet graphs
|
|
||||||
- [x] Updates Component to Partial Outage
|
|
||||||
- [x] Updates Component to Major Outage if already in Partial Outage (works well with distributed monitoring)
|
|
||||||
- [x] Can be run on multiple servers and geo regions
|
|
||||||
|
|
||||||
Configuration
|
:heavy_check_mark: Interval based checking of predefined Resources
|
||||||
-------------
|
:heavy_check_mark: Posts monitor lag to cachet graphs
|
||||||
|
:heavy_check_mark: Creates & Resolves Incidents
|
||||||
|
:heavy_check_mark: Updates Component to Partial Outage
|
||||||
|
:heavy_check_mark: Updates Component to Major Outage if already in Partial Outage (works with distributed monitors)
|
||||||
|
:heavy_check_mark: Can be run on multiple servers and geo regions
|
||||||
|
:heavy_check_mark: HTTP Checks (body/status code)
|
||||||
|
:heavy_check_mark: DNS Checks
|
||||||
|
:heavy_check_mark: TCP Checks
|
||||||
|
:heavy_check_mark: ICMP Checks
|
||||||
|
|
||||||
```
|
## Quick Start
|
||||||
|
|
||||||
|
Configuration can be done in either yaml or json format. An example JSON-File would look something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
// URL for the API. Note: Must end with /api/v1
|
"api": {
|
||||||
"api_url": "https://<cachet domain>/api/v1",
|
"url": "https://demo.cachethq.io/api/v1",
|
||||||
// Your API token for Cachet
|
"token": "9yMHsdioQosnyVK4iCVR",
|
||||||
"api_token": "<cachet api token>",
|
"insecure": false
|
||||||
// optional, false default, set if your certificate is self-signed/untrusted
|
},
|
||||||
"insecure_api": false,
|
"date_format": "02/01/2006 15:04:05 MST",
|
||||||
"monitors": [{
|
"monitors": [
|
||||||
// required, friendly name for your monitor
|
{
|
||||||
"name": "Name of your monitor",
|
"active": false,
|
||||||
// required, url to probe
|
"name": "google",
|
||||||
"url": "Ping URL",
|
"target": "https://google.com",
|
||||||
// optional, http method (defaults GET)
|
"strict": true,
|
||||||
"method": "get",
|
"method": "POST",
|
||||||
// self-signed ssl certificate
|
"component_id": 1,
|
||||||
"strict_tls": true,
|
"metric_id": 4,
|
||||||
// seconds between checks
|
"template": {
|
||||||
"interval": 10,
|
"investigating": {
|
||||||
// post lag to cachet metric (graph)
|
"subject": "{{ .Monitor.Name }} - {{ .SystemName }}",
|
||||||
// note either metric ID or component ID are required
|
"message": "{{ .Monitor.Name }} check **failed** (server time: {{ .now }})\n\n{{ .FailReason }}"
|
||||||
"metric_id": <metric id>,
|
},
|
||||||
// post incidents to this component
|
"fixed": {
|
||||||
"component_id": <component id>,
|
"subject": "I HAVE BEEN FIXED"
|
||||||
// If % of downtime is over this threshold, open an incident
|
}
|
||||||
"threshold": 80,
|
},
|
||||||
// optional, expected status code (either status code or body must be supplied)
|
"interval": 1,
|
||||||
"expected_status_code": 200,
|
"timeout": 1,
|
||||||
// optional, regular expression to match body content
|
"threshold": 80,
|
||||||
"expected_body": "P.*NG"
|
"headers": {
|
||||||
}],
|
"Authorization": "Basic <hash>"
|
||||||
// optional, system name to identify bot (uses hostname by default)
|
},
|
||||||
"system_name": "",
|
"expected_status_code": 200,
|
||||||
// optional, defaults to stdout
|
"expected_body": "P.*NG"
|
||||||
"log_path": ""
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Installation
|
## Installation
|
||||||
------------
|
|
||||||
|
|
||||||
1. Download binary from [release page](https://github.com/CastawayLabs/cachet-monitor/releases)
|
1. Download binary from [release page](https://github.com/bennetgallein/cachet-monitor/releases)
|
||||||
2. Create your configuration ([example](https://raw.githubusercontent.com/CastawayLabs/cachet-monitor/master/example.config.json))
|
2. Add the binary to an executable path (/usr/bin, etc.)
|
||||||
3. `cachet-monitor -c /etc/cachet-monitor.config.json`
|
3. Create a configuration following provided examples
|
||||||
|
4. `cachet-monitor -c /etc/cachet-monitor.json`
|
||||||
|
|
||||||
pro tip: run in background using `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
pro tip: run in background using `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`, or use a tmux/screen session
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage of cachet-monitor:
|
Usage:
|
||||||
-c="/etc/cachet-monitor.config.json": Config path
|
cachet-monitor (-c PATH | --config PATH) [--log=LOGPATH] [--name=NAME] [--immediate]
|
||||||
-log="": Log path
|
cachet-monitor -h | --help | --version
|
||||||
-name="": System Name
|
|
||||||
|
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)
|
||||||
|
--restarted Get open incidents before start monitoring (if monitor died or restarted)
|
||||||
|
|
||||||
|
Environment varaibles:
|
||||||
|
CACHET_API override API url from configuration
|
||||||
|
CACHET_TOKEN override API token from configuration
|
||||||
|
CACHET_DEV set to enable dev logging
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment variables
|
## Init script
|
||||||
---------------------
|
|
||||||
|
|
||||||
| Name | Example Value | Description |
|
If your system is running systemd (like Debian, Ubuntu 16.04, Fedora, RHEL7, or Archlinux) you can use the provided example file: [example.cachet-monitor.service](https://github.com/bennetgallein/cachet-monitor/blob/master/example.cachet-monitor.service).
|
||||||
| ------------ | ------------------------------ | --------------------------- |
|
|
||||||
| CACHET_API | http://demo.cachethq.io/api/v1 | URL endpoint for cachet api |
|
|
||||||
| CACHET_TOKEN | APIToken123 | API Authentication token |
|
|
||||||
| CACHET_DEV | 1 | Strips logging |
|
|
||||||
|
|
||||||
Vision and goals
|
1. Simply put it in the right place with `cp example.cachet-monitor.service /etc/systemd/system/cachet-monitor.service`
|
||||||
----------------
|
2. Then do a `systemctl daemon-reload` in your terminal to update Systemd configuration
|
||||||
|
3. Finally you can start cachet-monitor on every startup with `systemctl enable cachet-monitor.service`! 👍
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
The following variables are available:
|
||||||
|
|
||||||
|
| Root objects | Description |
|
||||||
|
| ------------- | ----------------------------------- |
|
||||||
|
| `.SystemName` | system name |
|
||||||
|
| `.API` | `api` object from configuration |
|
||||||
|
| `.Monitor` | `monitor` object from configuration |
|
||||||
|
| `.now` | formatted date string |
|
||||||
|
|
||||||
|
| Monitor variables |
|
||||||
|
| ----------------- |
|
||||||
|
| `.Name` |
|
||||||
|
| `.Target` |
|
||||||
|
| `.Type` |
|
||||||
|
| `.Strict` |
|
||||||
|
| `.MetricID` |
|
||||||
|
| ... |
|
||||||
|
|
||||||
|
All monitor variables are available from `monitor.go`
|
||||||
|
|
||||||
|
## Monitor Types
|
||||||
|
|
||||||
|
We support a variety of monitor-types. Here are the configuration parameters for each of them
|
||||||
|
|
||||||
|
Also, the following parameters are shared for all monitors.
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ----------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| name | a friendly name for the monitor |
|
||||||
|
| target | target for the check (e.g. a domain or IP) |
|
||||||
|
| active | a boolean wether or not this test is currently active |
|
||||||
|
| type | type type of the check, see supported types above or below |
|
||||||
|
| interval | the interval in seconds in which to check the monitor |
|
||||||
|
| timeout | the timeout for each check. Needs to be smaller than the interval |
|
||||||
|
| metric_id | the ID of the metric. Metrics are used for graphing values |
|
||||||
|
| component_id | the ID of the component inside of Cachet. Used for creating incidents |
|
||||||
|
| templates.investigating | template to use as a message for when the check enters the investigating stage |
|
||||||
|
| templates.fixed | template to use as a message for when the check enters the fixed stage |
|
||||||
|
| threshold | If % of downtime is over this threshold, open an incident |
|
||||||
|
| threshold_count | the number of checks that count into the threshold (defaults to 10) |
|
||||||
|
|
||||||
|
### HTTP
|
||||||
|
|
||||||
|
Either expected_body or expected_status_code needs to be set.
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| method | the HTTP Request method to use (Defaults to GET) |
|
||||||
|
| headers | a key-value array of additional headers to use for the request |
|
||||||
|
| expected_status_code | the expected status-code returned from the request |
|
||||||
|
| expected_body | a regex or normal string that will be used to test against the returned body of the request |
|
||||||
|
| expected_md5sum | a md5 checksum of the body, which will be checked against |
|
||||||
|
| expected_length | the length of the string of the response body |
|
||||||
|
| data | body-data for a post request |
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| -------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| dns | set a custom DNS-Resolver (IP:Port format) |
|
||||||
|
| question | the type of DNS-Request to execute (e.g. A, MX, CNAME...). Can also be a List (['A', 'MX', 'CNAME']) |
|
||||||
|
| answers | an array of response objects. see below |
|
||||||
|
|
||||||
|
#### Answer Object
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ----- | --------------------------------------------------------------------- |
|
||||||
|
| regex | if you want to use a regexp, use this key and the regexp as the value |
|
||||||
|
| exact | exact match for the response value |
|
||||||
|
|
||||||
|
### TCP
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ---- | ---------------------------------- |
|
||||||
|
| port | the port to do a tcp connection to |
|
||||||
|
|
||||||
|
### ICMP
|
||||||
|
|
||||||
|
_No special variables needed_
|
||||||
|
|
||||||
|
## Vision and goals
|
||||||
|
|
||||||
We made this tool because we felt the need to have our own monitoring software (leveraging on Cachet).
|
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.
|
The idea is a stateless program which collects data and pushes it to a central cachet instance.
|
||||||
|
|
||||||
This gives us power to have an army of geographically distributed loggers and reveal issues in both latency & downtime on client websites.
|
This gives us power to have an army of geographically distributed loggers and reveal issues in both latency & downtime on client websites.
|
||||||
|
|
||||||
Package usage
|
## 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 `ValidateConfiguration` on `CachetMonitor` and all the monitors inside.
|
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)
|
[API Documentation](https://godoc.org/github.com/CastawayLabs/cachet-monitor)
|
||||||
|
|
||||||
|
# Contributions welcome
|
||||||
|
|
||||||
|
We'll happily accept contributions for anything usefull.
|
||||||
|
|
||||||
|
# Build on Linux/MacOS
|
||||||
|
|
||||||
|
1. Read and install with https://ahmadawais.com/install-go-lang-on-macos-with-homebrew/
|
||||||
|
2. Test in console with `go get -u` and `go build cli/main.go`
|
||||||
|
3. Run `./go-executable-build.sh cli/main.go`
|
||||||
|
4. This will create a `build/cli/main.go-linux-amd64`-file, which is the executable binary
|
||||||
|
5. `mv build/cli/main.go-linux-amd64 /usr/bin/cachet-monitor`
|
||||||
|
|||||||
80
slack.go
Normal file
80
slack.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Slack struct {
|
||||||
|
WebhookURL string
|
||||||
|
Attachments []Attachments `json:"attachments"`
|
||||||
|
}
|
||||||
|
type Fields struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Short bool `json:"short"`
|
||||||
|
}
|
||||||
|
type Attachments struct {
|
||||||
|
Fallback string `json:"fallback"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Pretext string `json:"pretext"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
TitleLink string `json:"title_link"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Fields []Fields `json:"fields"`
|
||||||
|
ThumbURL string `json:"thumb_url"`
|
||||||
|
Footer string `json:"footer"`
|
||||||
|
FooterIcon string `json:"footer_icon"`
|
||||||
|
Ts int64 `json:"ts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func test() {
|
||||||
|
slack := Slack{
|
||||||
|
Attachments: []Attachments{
|
||||||
|
Attachments{
|
||||||
|
Fallback: "Required plain-text summary of the attachment.",
|
||||||
|
Color: "#36a64f",
|
||||||
|
Title: "Slack API Documentation",
|
||||||
|
TitleLink: "https://status.easyship.com",
|
||||||
|
Text: "Optional text that appears within the attachment",
|
||||||
|
Footer: "Cachet Monitor",
|
||||||
|
FooterIcon: "https://i.imgur.com/spck1w6.png",
|
||||||
|
Ts: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slack.WebhookURL = "https://hooks.slack.com/services/0000000/00000000/xxxxxxxxxxxxxxxxxxx"
|
||||||
|
err := slack.SendSlackNotification()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSlackNotification will post to an 'Incoming Webook' url setup in Slack Apps. It accepts
|
||||||
|
// some text and the slack channel is saved within Slack.
|
||||||
|
func (slack *Slack) SendSlackNotification() error {
|
||||||
|
|
||||||
|
slackBody, _ := json.Marshal(slack)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, slack.WebhookURL, bytes.NewBuffer(slackBody))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(resp.Body)
|
||||||
|
if buf.String() != "ok" {
|
||||||
|
return errors.New("Non-ok response returned from Slack")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
tcp.go
Normal file
76
tcp.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Thanks go to https://github.com/Soontao/cachet-monitor/blob/master/tcp.go
|
||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Investigating template
|
||||||
|
var defaultTCPInvestigatingTpl = MessageTemplate{
|
||||||
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
|
Message: `{{ .Monitor.Name }} check **failed** (server time: {{ .now }})
|
||||||
|
|
||||||
|
{{ .FailReason }}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed template
|
||||||
|
var defaultTCPFixedTpl = MessageTemplate{
|
||||||
|
Subject: `{{ .Monitor.Name }} - {{ .SystemName }}`,
|
||||||
|
Message: `**Resolved** - {{ .now }}
|
||||||
|
|
||||||
|
Down seconds: {{ .downSeconds }}s`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCPMonitor struct
|
||||||
|
type TCPMonitor struct {
|
||||||
|
AbstractMonitor `mapstructure:",squash"`
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTCPPortAlive func
|
||||||
|
func CheckTCPPortAlive(ip, port string, timeout int64) (bool, error) {
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, port), time.Duration(timeout)*time.Second)
|
||||||
|
if conn != nil {
|
||||||
|
defer conn.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if it available
|
||||||
|
func (m *TCPMonitor) test() bool {
|
||||||
|
if alive, e := CheckTCPPortAlive(m.Target, m.Port, int64(m.Timeout)); alive {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
m.lastFailReason = fmt.Sprintf("TCP check failed: %v", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
func (m *TCPMonitor) Validate() []string {
|
||||||
|
|
||||||
|
// set incident temp
|
||||||
|
m.Template.Investigating.SetDefault(defaultTCPInvestigatingTpl)
|
||||||
|
m.Template.Fixed.SetDefault(defaultTCPFixedTpl)
|
||||||
|
|
||||||
|
// super.Validate()
|
||||||
|
errs := m.AbstractMonitor.Validate()
|
||||||
|
|
||||||
|
if m.Target == "" {
|
||||||
|
errs = append(errs, "Target is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Port == "" {
|
||||||
|
errs = append(errs, "Port is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
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