Browse Source

Add support for iterating over external data

Brendan Abolivier 7 years ago
parent
commit
3fc703852c
Signed by: Brendan Abolivier <contact@brendanabolivier.com> GPG key ID: 8EF1500759F70623

+ 4
- 2
src/metrics-alerting/alert/alert.go View File

@@ -4,6 +4,7 @@ import (
4 4
 	"fmt"
5 5
 
6 6
 	"metrics-alerting/config"
7
+	"metrics-alerting/script_data"
7 8
 
8 9
 	"gopkg.in/gomail.v2"
9 10
 )
@@ -17,12 +18,13 @@ func (a *Alerter) Alert(
17 18
 	script config.Script,
18 19
 	result interface{},
19 20
 	labels map[string]string,
21
+	data script_data.Data,
20 22
 ) error {
21 23
 	switch script.Action {
22 24
 	case "http":
23
-		return a.alertHttp(script, result, labels)
25
+		return a.alertHttp(script, result, labels, data)
24 26
 	case "email":
25
-		return a.alertEmail(script, result, labels)
27
+		return a.alertEmail(script, result, labels, data)
26 28
 	default:
27 29
 		return fmt.Errorf("invalid action type: %s", script.Action)
28 30
 	}

+ 19
- 7
src/metrics-alerting/alert/email.go View File

@@ -6,6 +6,7 @@ import (
6 6
 	"strings"
7 7
 
8 8
 	"metrics-alerting/config"
9
+	"metrics-alerting/script_data"
9 10
 
10 11
 	"gopkg.in/gomail.v2"
11 12
 )
@@ -14,24 +15,25 @@ func (a *Alerter) alertEmail(
14 15
 	script config.Script,
15 16
 	result interface{},
16 17
 	labels map[string]string,
18
+	data script_data.Data,
17 19
 ) error {
18
-	formatNumber := "Script %s just exceeded its threshold of %.2f and now returns %f"
19
-	formatBool := "Test for script %s and returned false instead of true"
20
+	formatNumber := "Script \"%s\" just exceeded its threshold of %.2f and now returns %f"
21
+	formatBool := "Test for script \"%s\" failed and returned false instead of true"
20 22
 
21 23
 	var body, subject string
22 24
 	switch script.Type {
23 25
 	case "number", "series":
24 26
 		subject = fmt.Sprintf(
25
-			"Threshold exceeded for script %s %s", script.Key,
26
-			getIdentifyingLabels(script, labels),
27
+			"Threshold exceeded for script \"%s\" %s%s", script.Key,
28
+			getIdentifyingLabels(script, labels), getScriptData(data),
27 29
 		)
28 30
 		body = fmt.Sprintf(
29 31
 			formatNumber, script.Key, script.Threshold, result.(float64),
30 32
 		)
31 33
 	case "bool":
32 34
 		subject = fmt.Sprintf(
33
-			"Test for script %s failed %s", script.Key,
34
-			getIdentifyingLabels(script, labels),
35
+			"Test for script \"%s\" failed %s%s", script.Key,
36
+			getIdentifyingLabels(script, labels), getScriptData(data),
35 37
 		)
36 38
 		body = fmt.Sprintf(formatBool, script.Key)
37 39
 	}
@@ -65,7 +67,9 @@ func getIdentifyingLabels(
65 67
 
66 68
 	identifyingLabels := make(map[string]string)
67 69
 	for _, label := range script.IdentifyingLabels {
68
-		identifyingLabels[label] = labels[label]
70
+		if len(labels[label]) > 0 {
71
+			identifyingLabels[label] = labels[label]
72
+		}
69 73
 	}
70 74
 
71 75
 	labelsAsStrs := []string{}
@@ -77,3 +81,11 @@ func getIdentifyingLabels(
77 81
 
78 82
 	return "(" + strings.Join(labelsAsStrs, ", ") + ")"
79 83
 }
84
+
85
+func getScriptData(data script_data.Data) string {
86
+	if len(data.Key) == 0 {
87
+		return ""
88
+	}
89
+
90
+	return "(" + data.Key + "=" + data.Value + ")"
91
+}

+ 7
- 0
src/metrics-alerting/alert/http.go View File

@@ -8,18 +8,21 @@ import (
8 8
 	"strconv"
9 9
 
10 10
 	"metrics-alerting/config"
11
+	"metrics-alerting/script_data"
11 12
 )
12 13
 
13 14
 type alertBody struct {
14 15
 	Key    string            `json:"scriptKey"`
15 16
 	Value  string            `json:"value"`
16 17
 	Labels map[string]string `json:"labels"`
18
+	Data   map[string]string `json:"data"`
17 19
 }
18 20
 
19 21
 func (a *Alerter) alertHttp(
20 22
 	script config.Script,
21 23
 	result interface{},
22 24
 	labels map[string]string,
25
+	data script_data.Data,
23 26
 ) error {
24 27
 	var value string
25 28
 	switch script.Type {
@@ -29,10 +32,14 @@ func (a *Alerter) alertHttp(
29 32
 		value = strconv.FormatBool(result.(bool))
30 33
 	}
31 34
 
35
+	returnData := make(map[string]string)
36
+	returnData[data.Key] = data.Value
37
+
32 38
 	alert := alertBody{
33 39
 		Key:    script.Key,
34 40
 		Value:  value,
35 41
 		Labels: labels,
42
+		Data:   returnData,
36 43
 	}
37 44
 
38 45
 	body, err := json.Marshal(alert)

+ 62
- 2
src/metrics-alerting/config/config.go View File

@@ -1,7 +1,10 @@
1 1
 package config
2 2
 
3 3
 import (
4
+	"bufio"
5
+	"io"
4 6
 	"io/ioutil"
7
+	"os"
5 8
 
6 9
 	"gopkg.in/yaml.v2"
7 10
 )
@@ -24,6 +27,14 @@ type SMTPSettings struct {
24 27
 	Password string `yaml:"password"`
25 28
 }
26 29
 
30
+type ScriptDataSource struct {
31
+	// Data to load from a file containing the content for the slide, one
32
+	// element per line
33
+	FromFile map[string]string `yaml:"from_file,omitempty"`
34
+	// Plain data
35
+	Plain map[string][]string `yaml:"plain,omitempty"`
36
+}
37
+
27 38
 type Script struct {
28 39
 	// An identifying key for the script
29 40
 	Key string `yaml:"key"`
@@ -41,6 +52,10 @@ type Script struct {
41 52
 	// The labels that will be mentioned in the email subject, only required if
42 53
 	// the action is "email"
43 54
 	IdentifyingLabels []string `yaml:"identifying_labels,omitempty"`
55
+	// Data to use in the script
56
+	DataSource ScriptDataSource `yaml:"script_data,omitempty"`
57
+	// Loaded data
58
+	ScriptData map[string][]string
44 59
 }
45 60
 
46 61
 type Config struct {
@@ -55,12 +70,57 @@ type Config struct {
55 70
 	Scripts []Script `yaml:"scripts"`
56 71
 }
57 72
 
58
-func Load(filePath string) (cfg Config, err error) {
73
+func (cfg *Config) Load(filePath string) (err error) {
59 74
 	content, err := ioutil.ReadFile(filePath)
60 75
 	if err != nil {
61 76
 		return
62 77
 	}
63 78
 
64 79
 	err = yaml.Unmarshal(content, &cfg)
65
-	return
80
+	if err != nil {
81
+		return
82
+	}
83
+
84
+	return cfg.loadData()
85
+}
86
+
87
+func (cfg *Config) loadData() error {
88
+	var line string
89
+	var l []byte
90
+	var isPrefix bool
91
+	for i, script := range cfg.Scripts {
92
+		script.ScriptData = make(map[string][]string)
93
+		for key, fileName := range script.DataSource.FromFile {
94
+			fp, err := os.Open(fileName)
95
+			if err != nil {
96
+				return err
97
+			}
98
+			reader := bufio.NewReader(fp)
99
+
100
+			for true {
101
+				isPrefix = true
102
+				line = ""
103
+				for isPrefix {
104
+					l, isPrefix, err = reader.ReadLine()
105
+					if err != nil && err != io.EOF {
106
+						return err
107
+					}
108
+					line = line + string(l)
109
+				}
110
+
111
+				if err == io.EOF {
112
+					break
113
+				}
114
+
115
+				script.ScriptData[key] = append(script.ScriptData[key], line)
116
+			}
117
+		}
118
+		for key, slice := range script.DataSource.Plain {
119
+			script.ScriptData[key] = slice
120
+		}
121
+
122
+		cfg.Scripts[i] = script
123
+	}
124
+
125
+	return nil
66 126
 }

+ 5
- 18
src/metrics-alerting/main.go View File

@@ -2,7 +2,6 @@ package main
2 2
 
3 3
 import (
4 4
 	"flag"
5
-	"fmt"
6 5
 
7 6
 	"metrics-alerting/alert"
8 7
 	"metrics-alerting/config"
@@ -20,7 +19,10 @@ var (
20 19
 func main() {
21 20
 	flag.Parse()
22 21
 
23
-	cfg, _ := config.Load(*configPath)
22
+	cfg := config.Config{}
23
+	if err := cfg.Load(*configPath); err != nil {
24
+		logrus.Panic(err)
25
+	}
24 26
 	client := warp10.Warp10Client{
25 27
 		ExecEndpoint: cfg.Warp10Exec,
26 28
 		ReadToken:    cfg.ReadToken,
@@ -35,22 +37,7 @@ func main() {
35 37
 	}
36 38
 
37 39
 	for _, script := range cfg.Scripts {
38
-		var err error
39
-		switch script.Type {
40
-		case "number":
41
-			err = process.ProcessNumber(client, script, alerter)
42
-			break
43
-		case "bool":
44
-			err = process.ProcessBool(client, script, alerter)
45
-			break
46
-		case "series":
47
-			err = process.ProcessSeries(client, script, alerter)
48
-			break
49
-		default:
50
-			err = fmt.Errorf("invalid return type: %s", script.Type)
51
-		}
52
-
53
-		if err != nil {
40
+		if err := process.Process(client, script, alerter); err != nil {
54 41
 			logrus.Error(err)
55 42
 		}
56 43
 	}

+ 64
- 7
src/metrics-alerting/process/process.go View File

@@ -1,30 +1,85 @@
1 1
 package process
2 2
 
3 3
 import (
4
+	"fmt"
5
+	"regexp"
4 6
 	"time"
5 7
 
6 8
 	"metrics-alerting/alert"
7 9
 	"metrics-alerting/config"
10
+	"metrics-alerting/script_data"
8 11
 	"metrics-alerting/warp10"
9 12
 )
10 13
 
11
-func ProcessNumber(
14
+func Process(
12 15
 	client warp10.Warp10Client,
13 16
 	script config.Script,
14 17
 	alerter alert.Alerter,
15 18
 ) error {
19
+	var scriptData script_data.Data
20
+	// TODO: Process more than one dataset
21
+	for key, data := range script.ScriptData {
22
+		scriptData.Key = key
23
+		r, err := regexp.Compile("`" + key + "`")
24
+		if err != nil {
25
+			return err
26
+		}
27
+		match := r.Find([]byte(script.Script))
28
+		if len(match) == 0 {
29
+			return fmt.Errorf("no variable named %s in script %s", key, script.Key)
30
+		}
31
+
32
+		origScript := script.Script
33
+
34
+		for _, el := range data {
35
+			scriptData.Value = el
36
+			filledScript := r.ReplaceAll([]byte(origScript), []byte(el))
37
+			script.Script = string(filledScript)
38
+			if err = dispatchType(client, script, alerter, scriptData); err != nil {
39
+				return err
40
+			}
41
+		}
42
+	}
43
+
44
+	return nil
45
+}
46
+
47
+func dispatchType(
48
+	client warp10.Warp10Client,
49
+	script config.Script,
50
+	alerter alert.Alerter,
51
+	data script_data.Data,
52
+) error {
53
+	switch script.Type {
54
+	case "number":
55
+		return processNumber(client, script, alerter, data)
56
+	case "bool":
57
+		return processBool(client, script, alerter, data)
58
+	case "series":
59
+		return processSeries(client, script, alerter, data)
60
+	}
61
+	return fmt.Errorf("invalid return type: %s", script.Type)
62
+}
63
+
64
+func processNumber(
65
+	client warp10.Warp10Client,
66
+	script config.Script,
67
+	alerter alert.Alerter,
68
+	data script_data.Data,
69
+) error {
16 70
 	value, err := client.ReadNumber(script.Script)
17 71
 	if err != nil {
18 72
 		return err
19 73
 	}
20 74
 
21
-	return processFloat(value, script, alerter, nil)
75
+	return processFloat(value, script, alerter, nil, data)
22 76
 }
23 77
 
24
-func ProcessBool(
78
+func processBool(
25 79
 	client warp10.Warp10Client,
26 80
 	script config.Script,
27 81
 	alerter alert.Alerter,
82
+	data script_data.Data,
28 83
 ) error {
29 84
 	value, err := client.ReadBool(script.Script)
30 85
 	if err != nil {
@@ -35,13 +90,14 @@ func ProcessBool(
35 90
 		return nil
36 91
 	}
37 92
 
38
-	return alerter.Alert(script, value, nil)
93
+	return alerter.Alert(script, value, nil, data)
39 94
 }
40 95
 
41
-func ProcessSeries(
96
+func processSeries(
42 97
 	client warp10.Warp10Client,
43 98
 	script config.Script,
44 99
 	alerter alert.Alerter,
100
+	data script_data.Data,
45 101
 ) error {
46 102
 	series, err := client.ReadSeriesOfNumbers(script.Script)
47 103
 
@@ -63,7 +119,7 @@ func ProcessSeries(
63 119
 		// to find when the situation began so we can add info about time in the
64 120
 		// alert.
65 121
 		if err = processFloat(
66
-			serie.Datapoints[0][1], script, alerter, serie.Labels,
122
+			serie.Datapoints[0][1], script, alerter, serie.Labels, data,
67 123
 		); err != nil {
68 124
 			return err
69 125
 		}
@@ -77,13 +133,14 @@ func processFloat(
77 133
 	script config.Script,
78 134
 	alerter alert.Alerter,
79 135
 	labels map[string]string,
136
+	data script_data.Data,
80 137
 ) error {
81 138
 	if value < script.Threshold {
82 139
 		// Nothing to alert about
83 140
 		return nil
84 141
 	}
85 142
 
86
-	return alerter.Alert(script, value, labels)
143
+	return alerter.Alert(script, value, labels, data)
87 144
 }
88 145
 
89 146
 func isRecentEnough(datapoint []float64) bool {

+ 6
- 0
src/metrics-alerting/script_data/script_data.go View File

@@ -0,0 +1,6 @@
1
+package script_data
2
+
3
+type Data struct {
4
+	Key   string
5
+	Value string
6
+}