Fix several kapacitor validation failures

pull/10616/head
Chris Goller 2017-11-07 15:59:41 -06:00
parent f4fa08e36b
commit b1ad6443e5
9 changed files with 321 additions and 30 deletions

4
Gopkg.lock generated
View File

@ -49,7 +49,7 @@
[[projects]]
name = "github.com/google/go-cmp"
packages = ["cmp"]
packages = ["cmp","cmp/cmpopts"]
revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8"
[[projects]]
@ -140,6 +140,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "f34fb88755292baba8b52c14bf5b9a028daff96a763368a7cf1de90004d33695"
inputs-digest = "85a5451fc9e0596e486a676204eb2de0b12900522341ee0804cf9ec86fb2765e"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
client "github.com/influxdata/kapacitor/client/v1"
)
@ -945,10 +946,22 @@ func TestClient_Update(t *testing.T) {
ctx: context.Background(),
href: "/kapacitor/v1/tasks/howdy",
rule: chronograf.AlertRule{
ID: "howdy",
ID: "howdy",
Name: "myname",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
Fields: []chronograf.Field{
{
Type: "field",
Value: "usage_user",
},
},
},
Trigger: "threshold",
TriggerValues: chronograf.TriggerValues{
Operator: greaterThan,
},
},
},
@ -1009,10 +1022,22 @@ func TestClient_Update(t *testing.T) {
ctx: context.Background(),
href: "/kapacitor/v1/tasks/howdy",
rule: chronograf.AlertRule{
ID: "howdy",
ID: "howdy",
Name: "myname",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
Fields: []chronograf.Field{
{
Type: "field",
Value: "usage_user",
},
},
},
Trigger: "threshold",
TriggerValues: chronograf.TriggerValues{
Operator: greaterThan,
},
},
},
@ -1061,6 +1086,135 @@ func TestClient_Update(t *testing.T) {
},
wantStatus: client.Disabled,
},
{
name: "error because relative cannot have inside range",
wantErr: true,
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/error",
rule: chronograf.AlertRule{
ID: "error",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Fields: []chronograf.Field{
{
Value: "usage_user",
Type: "field",
},
},
},
Trigger: Relative,
TriggerValues: chronograf.TriggerValues{
Operator: InsideRange,
},
},
},
},
{
name: "error because rule has an unknown trigger mechanism",
wantErr: true,
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/error",
rule: chronograf.AlertRule{
ID: "error",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
},
},
},
},
{
name: "error because query has no fields",
wantErr: true,
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/error",
rule: chronograf.AlertRule{
ID: "error",
Trigger: Threshold,
TriggerValues: chronograf.TriggerValues{
Period: "1d",
},
Name: "myname",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
},
},
},
},
{
name: "error because alert has no name",
wantErr: true,
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/error",
rule: chronograf.AlertRule{
ID: "error",
Trigger: Deadman,
TriggerValues: chronograf.TriggerValues{
Period: "1d",
},
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
},
},
},
},
{
name: "error because alert period cannot be an empty string in deadman alert",
wantErr: true,
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/error",
rule: chronograf.AlertRule{
ID: "error",
Name: "myname",
Trigger: Deadman,
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
},
},
},
},
}
for _, tt := range tests {
kapa.ResTask = tt.resTask
@ -1079,11 +1233,17 @@ func TestClient_Update(t *testing.T) {
t.Errorf("Client.Update() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
if !cmp.Equal(got, tt.want) {
t.Errorf("%q. Client.Update() = -got/+want %s", tt.name, cmp.Diff(got, tt.want))
}
if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) {
t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions)
var cmpOptions = cmp.Options{
cmpopts.IgnoreFields(client.UpdateTaskOptions{}, "TICKscript"),
}
if !cmp.Equal(kapa.UpdateTaskOptions, tt.updateTaskOptions, cmpOptions...) {
t.Errorf("Client.Update() = %s", cmp.Diff(got, tt.updateTaskOptions, cmpOptions...))
}
if tt.wantStatus != kapa.LastStatus {
t.Errorf("Client.Update() = %v, want %v", kapa.LastStatus, tt.wantStatus)
@ -1130,10 +1290,16 @@ func TestClient_Create(t *testing.T) {
args: args{
ctx: context.Background(),
rule: chronograf.AlertRule{
ID: "howdy",
ID: "howdy",
Name: "mynames",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
},
Trigger: Deadman,
TriggerValues: chronograf.TriggerValues{
Period: "1d",
},
},
},
@ -1152,10 +1318,79 @@ func TestClient_Create(t *testing.T) {
},
},
createTaskOptions: &client.CreateTaskOptions{
TICKscript: "",
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
TICKscript: `var db = 'db'
var rp = 'rp'
var measurement = 'meas'
var groupBy = []
var whereFilter = lambda: TRUE
var period = 1d
var name = 'mynames'
var idVar = name + ':{{.Group}}'
var message = ''
var idTag = 'alertID'
var levelTag = 'level'
var messageField = 'message'
var durationField = 'duration'
var outputDB = 'chronograf'
var outputRP = 'autogen'
var outputMeasurement = 'alerts'
var triggerType = 'deadman'
var threshold = 0.0
var data = stream
|from()
.database(db)
.retentionPolicy(rp)
.measurement(measurement)
.groupBy(groupBy)
.where(whereFilter)
var trigger = data
|deadman(threshold, period)
.stateChangesOnly()
.message(message)
.id(idVar)
.idTag(idTag)
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
trigger
|eval(lambda: "emitted")
.as('value')
.keep('value', messageField, durationField)
|influxDBOut()
.create()
.database(outputDB)
.retentionPolicy(outputRP)
.measurement(outputMeasurement)
.tag('alertName', name)
.tag('triggerType', triggerType)
trigger
|httpOut('output')
`,
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
DBRPs: []client.DBRP{
{
Database: "db",
@ -1205,10 +1440,9 @@ func TestClient_Create(t *testing.T) {
},
resError: fmt.Errorf("error"),
createTaskOptions: &client.CreateTaskOptions{
TICKscript: "",
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
DBRPs: []client.DBRP{
{
Database: "db",
@ -1236,6 +1470,9 @@ func TestClient_Create(t *testing.T) {
t.Errorf("Client.Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
if !cmp.Equal(got, tt.want) {
t.Errorf("%q. Client.Create() = -got/+want %s", tt.name, cmp.Diff(got, tt.want))
}

View File

@ -1,6 +1,8 @@
package kapacitor
import "fmt"
import (
"fmt"
)
const (
greaterThan = "greater than"

View File

@ -15,11 +15,11 @@ type Alert struct{}
func (a *Alert) Generate(rule chronograf.AlertRule) (chronograf.TICKScript, error) {
vars, err := Vars(rule)
if err != nil {
return "", nil
return "", err
}
data, err := Data(rule)
if err != nil {
return "", nil
return "", err
}
trigger, err := Trigger(rule)
if err != nil {

View File

@ -1,7 +1,10 @@
package kapacitor
import "github.com/influxdata/chronograf"
import "fmt"
import (
"fmt"
"github.com/influxdata/chronograf"
)
const (
// Deadman triggers when data is missing for a period of time

View File

@ -76,7 +76,37 @@ func Vars(rule chronograf.AlertRule) (string, error) {
}
}
type NotEmpty struct {
Err error
}
func (n *NotEmpty) Valid(name, s string) error {
if n.Err != nil {
return n.Err
}
if s == "" {
n.Err = fmt.Errorf("%s cannot be an empty string", name)
}
return n.Err
}
func commonVars(rule chronograf.AlertRule) (string, error) {
n := new(NotEmpty)
n.Valid("database", rule.Query.Database)
n.Valid("retention policy", rule.Query.RetentionPolicy)
n.Valid("measurement", rule.Query.Measurement)
n.Valid("alert name", rule.Name)
n.Valid("trigger type", rule.Trigger)
if n.Err != nil {
return "", n.Err
}
wind, err := window(rule)
if err != nil {
return "", err
}
common := `
var db = '%s'
var rp = '%s'
@ -104,7 +134,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
rule.Query.Measurement,
groupBy(rule.Query),
whereFilter(rule.Query),
window(rule),
wind,
rule.Name,
rule.Message,
IDTag,
@ -127,17 +157,27 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
// window is only used if deadman or threshold/relative with aggregate. Will return empty
// if no period.
func window(rule chronograf.AlertRule) string {
func window(rule chronograf.AlertRule) (string, error) {
if rule.Trigger == Deadman {
return fmt.Sprintf("var period = %s", rule.TriggerValues.Period)
if rule.TriggerValues.Period == "" {
return "", fmt.Errorf("period cannot be an empty string in deadman alert")
}
return fmt.Sprintf("var period = %s", rule.TriggerValues.Period), nil
}
// Period only makes sense if the field has a been grouped via a time duration.
for _, field := range rule.Query.Fields {
if field.Type == "func" {
return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every)
n := new(NotEmpty)
n.Valid("group by time", rule.Query.GroupBy.Time)
n.Valid("every", rule.Every)
if n.Err != nil {
return "", n.Err
}
return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every), nil
}
}
return ""
return "", nil
}
func groupBy(q *chronograf.QueryConfig) string {

View File

@ -1,11 +1,11 @@
import React, {PropTypes} from 'react'
import {CHANGES, OPERATORS, SHIFTS} from 'src/kapacitor/constants'
import {CHANGES, RELATIVE_OPERATORS, SHIFTS} from 'src/kapacitor/constants'
import Dropdown from 'shared/components/Dropdown'
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
const changes = mapToItems(CHANGES, 'change')
const shifts = mapToItems(SHIFTS, 'shift')
const operators = mapToItems(OPERATORS, 'operator')
const operators = mapToItems(RELATIVE_OPERATORS, 'operator')
const Relative = ({
onRuleTypeInputChange,

View File

@ -1,10 +1,10 @@
import React, {PropTypes} from 'react'
import {OPERATORS} from 'src/kapacitor/constants'
import {THRESHOLD_OPERATORS} from 'src/kapacitor/constants'
import Dropdown from 'shared/components/Dropdown'
import _ from 'lodash'
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
const operators = mapToItems(OPERATORS, 'operator')
const operators = mapToItems(THRESHOLD_OPERATORS, 'operator')
const noopSubmit = e => e.preventDefault()
const getField = ({fields}) => {
const alias = _.get(fields, ['0', 'alias'], false)

View File

@ -31,7 +31,7 @@ export const OUTSIDE_RANGE = 'outside range'
export const EQUAL_TO_OR_GREATER_THAN = 'equal to or greater'
export const EQUAL_TO_OR_LESS_THAN = 'equal to or less than'
export const OPERATORS = [
export const THRESHOLD_OPERATORS = [
GREATER_THAN,
EQUAL_TO_OR_GREATER_THAN,
EQUAL_TO_OR_LESS_THAN,
@ -42,6 +42,15 @@ export const OPERATORS = [
OUTSIDE_RANGE,
]
export const RELATIVE_OPERATORS = [
GREATER_THAN,
EQUAL_TO_OR_GREATER_THAN,
EQUAL_TO_OR_LESS_THAN,
LESS_THAN,
EQUAL_TO,
NOT_EQUAL_TO,
]
// export const RELATIONS = ['once', 'more than ', 'less than'];
export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h']
export const CHANGES = ['change', '% change']