Skip to content

Commit c07e8eb

Browse files
committed
<feature>init project
1 parent e8157b7 commit c07e8eb

File tree

13 files changed

+1417
-0
lines changed

13 files changed

+1417
-0
lines changed

action/action.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Package action implements all the different actions an alert can take should
2+
// its condition be found to be true
3+
package action
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"net/http"
11+
"strings"
12+
13+
"esalert/config"
14+
"esalert/context"
15+
"github.com/mitchellh/mapstructure"
16+
log "github.com/sirupsen/logrus"
17+
)
18+
19+
// Actioner describes an action type. There all multiple action types, but they
20+
// all simply attempt to perform one action and that's it
21+
type Actioner interface {
22+
23+
// Do takes in the alert context, and possibly returnes an error if the
24+
// action failed
25+
Do(context.Context) error
26+
}
27+
28+
// Action is a wrapper around an Actioner which contains some type information
29+
type Action struct {
30+
Type string
31+
Actioner
32+
}
33+
34+
// ToActioner takes in some arbitrary data (hopefully a map[string]interface{},
35+
// looks at its "type" key, and any other fields necessary based on that type,
36+
// and returns an Actioner (or an error)
37+
func ToActioner(in interface{}) (Action, error) {
38+
min, ok := in.(map[string]interface{})
39+
if !ok {
40+
return Action{}, errors.New("action definition is not an object")
41+
}
42+
43+
var a Actioner
44+
typ, _ := min["type"].(string)
45+
typ = strings.ToLower(typ)
46+
switch typ {
47+
case "log":
48+
a = &Log{}
49+
case "http":
50+
a = &HTTP{}
51+
case "slack":
52+
a = &Slack{}
53+
default:
54+
return Action{}, fmt.Errorf("unknown action type: %q", typ)
55+
}
56+
57+
if err := mapstructure.Decode(min, a); err != nil {
58+
return Action{}, err
59+
}
60+
return Action{Type: typ, Actioner: a}, nil
61+
}
62+
63+
// Log is an action which does nothing but print a log message. Useful when
64+
// testing alerts and you don't want to set up any actions yet
65+
type Log struct {
66+
Message string `mapstructure:"message"`
67+
}
68+
69+
// Do logs the Log's message. It doesn't actually need any context
70+
func (l *Log) Do(_ context.Context) error {
71+
log.WithFields(log.Fields{
72+
"message": l.Message,
73+
}).Infoln("doing log action")
74+
return nil
75+
}
76+
77+
// HTTP is an action which performs a single http request. If the request's
78+
// response doesn't have a 2xx response code then it's considered an error
79+
type HTTP struct {
80+
Method string `mapstructure:"method"`
81+
URL string `mapstructure:"url"`
82+
Headers map[string]string `mapstructure:"headers"`
83+
Body string `mapstructure:"body"`
84+
}
85+
86+
// Do performs the actual http request. It doesn't need the alert context
87+
func (h *HTTP) Do(_ context.Context) error {
88+
r, err := http.NewRequest(h.Method, h.URL, bytes.NewBufferString(h.Body))
89+
if err != nil {
90+
return err
91+
}
92+
93+
if h.Headers != nil {
94+
for k, v := range h.Headers {
95+
r.Header.Set(k, v)
96+
}
97+
}
98+
99+
resp, err := http.DefaultClient.Do(r)
100+
if err != nil {
101+
return err
102+
}
103+
resp.Body.Close()
104+
105+
if resp.StatusCode < 200 || resp.StatusCode > 299 {
106+
return fmt.Errorf("non 2xx response code returned: %d", resp.StatusCode)
107+
}
108+
109+
return nil
110+
}
111+
112+
// OpsGenie submits an alert to an Slack endpoint
113+
type Slack struct {
114+
Text string `json:"text" mapstructure:"text"`
115+
}
116+
117+
// Do performs the actual trigger request to the Slack api
118+
func (s *Slack) Do(c context.Context) error {
119+
if config.Opts.SlackWebhook == "" {
120+
return errors.New("Slack key not set in config")
121+
}
122+
123+
if s.Text == "" {
124+
return errors.New("missing required field text in Slack")
125+
}
126+
127+
bodyb, err := json.Marshal(&s)
128+
if err != nil {
129+
return err
130+
}
131+
132+
r, err := http.NewRequest("POST", config.Opts.SlackWebhook, bytes.NewBuffer(bodyb))
133+
if err != nil {
134+
return err
135+
}
136+
r.Header.Set("Content-Type", "application/json")
137+
138+
resp, err := http.DefaultClient.Do(r)
139+
if err != nil {
140+
return err
141+
}
142+
resp.Body.Close()
143+
return nil
144+
}

action/action_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package action_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"esalert/action"
9+
"esalert/context"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestToActioner(t *testing.T) {
15+
m := map[string]interface{}{
16+
"type": "http",
17+
"method": "get",
18+
"url": "http://example.com",
19+
"body": "wat",
20+
}
21+
a, err := action.ToActioner(m)
22+
assert.Nil(t, err)
23+
assert.Equal(t, &action.HTTP{Method: "get", URL: "http://example.com", Body: "wat"}, a.Actioner)
24+
25+
m = map[string]interface{}{
26+
"type": "slack",
27+
"text": "foo",
28+
}
29+
a, err = action.ToActioner(m)
30+
assert.Nil(t, err)
31+
assert.Equal(t, &action.Slack{Text: "foo"}, a.Actioner)
32+
}
33+
34+
func TestHTTPAction(t *testing.T) {
35+
mux := http.NewServeMux()
36+
mux.HandleFunc("/good", func(w http.ResponseWriter, r *http.Request) {
37+
w.WriteHeader(200)
38+
})
39+
mux.HandleFunc("/bad", func(w http.ResponseWriter, r *http.Request) {
40+
w.WriteHeader(400)
41+
})
42+
s := httptest.NewServer(mux)
43+
44+
h := &action.HTTP{
45+
Method: "GET",
46+
URL: s.URL + "/good",
47+
Body: "OHAI",
48+
}
49+
require.Nil(t, h.Do(context.Context{}))
50+
51+
h.URL = s.URL + "/bad"
52+
require.NotNil(t, h.Do(context.Context{}))
53+
}

alert/alert.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Package alert alert rules and templatize.
2+
package alert
3+
4+
import (
5+
"bytes"
6+
"fmt"
7+
"text/template"
8+
"time"
9+
10+
"github.com/Akagi201/utilgo/jobber"
11+
"esalert/action"
12+
"esalert/context"
13+
"esalert/luautil"
14+
"esalert/search"
15+
log "github.com/sirupsen/logrus"
16+
yaml "gopkg.in/yaml.v2"
17+
)
18+
19+
// Alert encompasses a search query which will be run periodically, the results
20+
// of which will be checked against a condition. If the condition returns true a
21+
// set of actions will be performed
22+
type Alert struct {
23+
Name string `yaml:"name"`
24+
Interval string `yaml:"interval"`
25+
SearchIndex string `yaml:"search_index"`
26+
SearchType string `yaml:"search_type"`
27+
Search search.Dict `yaml:"search"`
28+
SearchQuery string `yaml:"search_query"`
29+
Process luautil.LuaRunner `yaml:"process"`
30+
31+
Jobber *jobber.FullTimeSpec
32+
SearchIndexTPL *template.Template
33+
SearchTypeTPL *template.Template
34+
SearchTPL *template.Template
35+
}
36+
37+
func templatizeHelper(i interface{}, lastErr error) (*template.Template, error) {
38+
if lastErr != nil {
39+
return nil, lastErr
40+
}
41+
var str string
42+
if s, ok := i.(string); ok {
43+
str = s
44+
} else {
45+
b, err := yaml.Marshal(i)
46+
if err != nil {
47+
return nil, err
48+
}
49+
str = string(b)
50+
}
51+
52+
return template.New("").Parse(str)
53+
}
54+
55+
// Init initializes some internal data inside the Alert, and must be called
56+
// after the Alert is unmarshaled from yaml (or otherwise created)
57+
func (a *Alert) Init() error {
58+
var err error
59+
a.SearchIndexTPL, err = templatizeHelper(a.SearchIndex, err)
60+
a.SearchTypeTPL, err = templatizeHelper(a.SearchType, err)
61+
if a.Search == nil {
62+
a.SearchTPL, err = templatizeHelper(a.SearchQuery, err)
63+
} else {
64+
a.SearchTPL, err = templatizeHelper(&a.Search, err)
65+
}
66+
if err != nil {
67+
return err
68+
}
69+
70+
jb, err := jobber.ParseFullTimeSpec(a.Interval)
71+
if err != nil {
72+
return fmt.Errorf("parsing interval: %s", err)
73+
}
74+
a.Jobber = jb
75+
76+
return nil
77+
}
78+
79+
func (a Alert) Run() {
80+
kv := log.Fields{
81+
"name": a.Name,
82+
}
83+
log.WithFields(kv).Infoln("running alert")
84+
85+
now := time.Now()
86+
c := context.Context{
87+
Name: a.Name,
88+
StartedTS: uint64(now.Unix()),
89+
Time: now,
90+
}
91+
92+
searchIndex, searchType, searchQuery, err := a.CreateSearch(c)
93+
if err != nil {
94+
kv["err"] = err
95+
log.WithFields(kv).Errorln("failed to create search data")
96+
return
97+
}
98+
99+
log.WithFields(kv).Debugln("running search step")
100+
res, err := search.Search(searchIndex, searchType, searchQuery)
101+
if err != nil {
102+
kv["err"] = err
103+
log.WithFields(kv).Errorln("failed at search step")
104+
return
105+
}
106+
c.Result = res
107+
108+
log.WithFields(kv).Debugln("running process step")
109+
processRes, ok := a.Process.Do(c)
110+
if !ok {
111+
log.WithFields(kv).Errorln("failed at process step")
112+
return
113+
}
114+
115+
// if processRes isn't an []interface{}, actionsRaw will be the nil value of
116+
// []interface{}, which has a length of 0, so either way this works
117+
actionsRaw, _ := processRes.([]interface{})
118+
if len(actionsRaw) == 0 {
119+
log.WithFields(kv).Debugln("no actions returned")
120+
}
121+
122+
actions := make([]action.Action, len(actionsRaw))
123+
for i := range actionsRaw {
124+
a, err := action.ToActioner(actionsRaw[i])
125+
if err != nil {
126+
kv["err"] = err
127+
log.WithFields(kv).Errorln("error unpacking action")
128+
return
129+
}
130+
actions[i] = a
131+
}
132+
133+
for i := range actions {
134+
kv["action"] = actions[i].Type
135+
log.WithFields(kv).Infoln("performing action")
136+
if err := actions[i].Do(c); err != nil {
137+
kv["err"] = err
138+
log.WithFields(kv).Errorln("failed to complete action")
139+
return
140+
}
141+
}
142+
}
143+
144+
func (a Alert) CreateSearch(c context.Context) (string, string, interface{}, error) {
145+
buf := bytes.NewBuffer(make([]byte, 0, 1024))
146+
if err := a.SearchIndexTPL.Execute(buf, &c); err != nil {
147+
return "", "", nil, err
148+
}
149+
searchIndex := buf.String()
150+
151+
buf.Reset()
152+
if err := a.SearchTypeTPL.Execute(buf, &c); err != nil {
153+
return "", "", nil, err
154+
}
155+
searchType := buf.String()
156+
157+
buf.Reset()
158+
if err := a.SearchTPL.Execute(buf, &c); err != nil {
159+
return "", "", nil, err
160+
}
161+
searchRaw := buf.Bytes()
162+
163+
var search search.Dict
164+
if err := yaml.Unmarshal(searchRaw, &search); err != nil {
165+
return "", "", nil, err
166+
}
167+
168+
return searchIndex, searchType, search, nil
169+
}

0 commit comments

Comments
 (0)