Merge pull request #852 from influxdata/feature/kapacitor-detail-field
Update kapacitor alert rule to have detail fieldpull/10616/head
commit
a4aa900bf8
|
@ -3,6 +3,7 @@
|
|||
### Upcoming Bug Fixes
|
||||
|
||||
### Upcoming Features
|
||||
1. [#838](https://github.com/influxdata/chronograf/issues/838): Add detail node to kapacitor alerts
|
||||
|
||||
### Upcoming UI Improvements
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package bolt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func setupTestClient() (*TestClient, error) {
|
||||
if c, err := NewTestClient(); err != nil {
|
||||
return nil, err
|
||||
} else if err := c.Open(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure an AlertRuleStore can be stored.
|
||||
func TestAlertRuleStoreAdd(t *testing.T) {
|
||||
c, err := setupTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
s := c.AlertsStore
|
||||
|
||||
alerts := []chronograf.AlertRule{
|
||||
chronograf.AlertRule{
|
||||
ID: "one",
|
||||
},
|
||||
chronograf.AlertRule{
|
||||
ID: "two",
|
||||
Details: "howdy",
|
||||
},
|
||||
}
|
||||
|
||||
// Add new alert.
|
||||
ctx := context.Background()
|
||||
for i, a := range alerts {
|
||||
// Adding should return an identical copy
|
||||
actual, err := s.Add(ctx, 0, 0, a)
|
||||
if err != nil {
|
||||
t.Errorf("erroring adding alert to store: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, alerts[i]) {
|
||||
t.Fatalf("alert returned is different then alert saved; actual: %v, expected %v", actual, alerts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupWithRule(ctx context.Context, alert chronograf.AlertRule) (*TestClient, error) {
|
||||
c, err := setupTestClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add test alert
|
||||
if _, err := c.AlertsStore.Add(ctx, 0, 0, alert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Ensure an AlertRuleStore can be loaded.
|
||||
func TestAlertRuleStoreGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
alert := chronograf.AlertRule{
|
||||
ID: "one",
|
||||
}
|
||||
c, err := setupWithRule(ctx, alert)
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding test alert to store: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
actual, err := c.AlertsStore.Get(ctx, 0, 0, "one")
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading rule from store: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, alert) {
|
||||
t.Fatalf("alert returned is different then alert saved; actual: %v, expected %v", actual, alert)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure an AlertRuleStore can be load with a detail.
|
||||
func TestAlertRuleStoreGetDetail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
alert := chronograf.AlertRule{
|
||||
ID: "one",
|
||||
Details: "my details",
|
||||
}
|
||||
c, err := setupWithRule(ctx, alert)
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding test alert to store: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
actual, err := c.AlertsStore.Get(ctx, 0, 0, "one")
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading rule from store: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, alert) {
|
||||
t.Fatalf("alert returned is different then alert saved; actual: %v, expected %v", actual, alert)
|
||||
}
|
||||
}
|
|
@ -109,6 +109,7 @@ type AlertRule struct {
|
|||
Every string `json:"every"` // Every how often to check for the alerting criteria
|
||||
Alerts []string `json:"alerts"` // AlertServices name all the services to notify (e.g. pagerduty)
|
||||
Message string `json:"message"` // Message included with alert
|
||||
Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added.
|
||||
Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert
|
||||
TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger
|
||||
Name string `json:"name"` // Name is the user-defined name for the alert
|
||||
|
|
|
@ -199,6 +199,154 @@ trigger
|
|||
}
|
||||
}
|
||||
|
||||
func TestThresholdDetail(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "name",
|
||||
Trigger: "threshold",
|
||||
Alerts: []string{"slack", "victorops", "email"},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "greater than",
|
||||
Value: "90",
|
||||
},
|
||||
Every: "30s",
|
||||
Message: "message",
|
||||
Details: "details",
|
||||
Query: chronograf.QueryConfig{
|
||||
Database: "telegraf",
|
||||
Measurement: "cpu",
|
||||
RetentionPolicy: "autogen",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Field: "usage_user",
|
||||
Funcs: []string{"mean"},
|
||||
},
|
||||
},
|
||||
Tags: map[string][]string{
|
||||
"host": []string{
|
||||
"acc-0eabc309-eu-west-1-data-3",
|
||||
"prod",
|
||||
},
|
||||
"cpu": []string{
|
||||
"cpu_total",
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "10m",
|
||||
Tags: []string{"host", "cluster_id"},
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
RawText: "",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alert chronograf.AlertRule
|
||||
want chronograf.TICKScript
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test valid template alert",
|
||||
alert: alert,
|
||||
want: `var db = 'telegraf'
|
||||
|
||||
var rp = 'autogen'
|
||||
|
||||
var measurement = 'cpu'
|
||||
|
||||
var groupBy = ['host', 'cluster_id']
|
||||
|
||||
var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
|
||||
|
||||
var period = 10m
|
||||
|
||||
var every = 30s
|
||||
|
||||
var name = 'name'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
|
||||
var message = 'message'
|
||||
|
||||
var idTag = 'alertID'
|
||||
|
||||
var levelTag = 'level'
|
||||
|
||||
var messageField = 'message'
|
||||
|
||||
var durationField = 'duration'
|
||||
|
||||
var outputDB = 'chronograf'
|
||||
|
||||
var outputRP = 'autogen'
|
||||
|
||||
var outputMeasurement = 'alerts'
|
||||
|
||||
var triggerType = 'threshold'
|
||||
|
||||
var details = 'details'
|
||||
|
||||
var crit = 90
|
||||
|
||||
var data = stream
|
||||
|from()
|
||||
.database(db)
|
||||
.retentionPolicy(rp)
|
||||
.measurement(measurement)
|
||||
.groupBy(groupBy)
|
||||
.where(whereFilter)
|
||||
|window()
|
||||
.period(period)
|
||||
.every(every)
|
||||
.align()
|
||||
|mean('usage_user')
|
||||
.as('value')
|
||||
|
||||
var trigger = data
|
||||
|alert()
|
||||
.crit(lambda: "value" > crit)
|
||||
.stateChangesOnly()
|
||||
.message(message)
|
||||
.id(idVar)
|
||||
.idTag(idTag)
|
||||
.levelTag(levelTag)
|
||||
.messageField(messageField)
|
||||
.durationField(durationField)
|
||||
.details(details)
|
||||
.slack()
|
||||
.victorOps()
|
||||
.email()
|
||||
|
||||
trigger
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
|
||||
trigger
|
||||
|httpOut('output')
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gen := Alert{}
|
||||
got, err := gen.Generate(tt.alert)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. Threshold() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
diff := diffmatchpatch.New()
|
||||
delta := diff.DiffMain(string(tt.want), string(got), true)
|
||||
t.Errorf("%q\n%s", tt.name, diff.DiffPrettyText(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestThresholdInsideRange(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "name",
|
||||
|
|
|
@ -27,13 +27,19 @@ var AllAlerts = `
|
|||
.durationField(durationField)
|
||||
`
|
||||
|
||||
// ThresholdTrigger is the trickscript trigger for alerts that exceed a value
|
||||
// Details is used only for alerts that specify detail string
|
||||
var Details = `
|
||||
.details(details)
|
||||
`
|
||||
|
||||
// ThresholdTrigger is the tickscript trigger for alerts that exceed a value
|
||||
var ThresholdTrigger = `
|
||||
var trigger = data
|
||||
|alert()
|
||||
.crit(lambda: "value" %s crit)
|
||||
`
|
||||
|
||||
// ThresholdRangeTrigger is the alert when data does not intersect the range.
|
||||
var ThresholdRangeTrigger = `
|
||||
var trigger = data
|
||||
|alert()
|
||||
|
@ -102,7 +108,11 @@ func Trigger(rule chronograf.AlertRule) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
return trigger + AllAlerts, nil
|
||||
trigger += AllAlerts
|
||||
if rule.Details != "" {
|
||||
trigger += Details
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func relativeTrigger(rule chronograf.AlertRule) (string, error) {
|
||||
|
@ -132,7 +142,7 @@ func thresholdRangeTrigger(rule chronograf.AlertRule) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var iops []interface{} = make([]interface{}, len(ops))
|
||||
var iops = make([]interface{}, len(ops))
|
||||
for i, o := range ops {
|
||||
iops[i] = o
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
var outputMeasurement = '%s'
|
||||
var triggerType = '%s'
|
||||
`
|
||||
return fmt.Sprintf(common,
|
||||
res := fmt.Sprintf(common,
|
||||
rule.Query.Database,
|
||||
rule.Query.RetentionPolicy,
|
||||
rule.Query.Measurement,
|
||||
|
@ -117,7 +117,14 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
RP,
|
||||
Measurement,
|
||||
rule.Trigger,
|
||||
), nil
|
||||
)
|
||||
|
||||
if rule.Details != "" {
|
||||
res += fmt.Sprintf(`
|
||||
var details = '%s'
|
||||
`, rule.Details)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// window is only used if deadman or threshold/relative with aggregate. Will return empty
|
||||
|
|
|
@ -1731,6 +1731,10 @@
|
|||
"type": "string",
|
||||
"description": "Message to send when alert occurs."
|
||||
},
|
||||
"details": {
|
||||
"type": "string",
|
||||
"description": "Template for constructing a detailed HTML message for the alert. (Currently, only used for email/smtp"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"description": "Trigger defines the alerting structure; deadman alert if no data are received for the specified time range; relative alert if the data change relative to the data in a different time range; threshold alert if the data cross a boundary",
|
||||
|
|
|
@ -4,6 +4,7 @@ import {defaultRuleConfigs} from 'src/kapacitor/constants';
|
|||
import {
|
||||
chooseTrigger,
|
||||
updateRuleValues,
|
||||
updateDetails,
|
||||
updateMessage,
|
||||
updateAlerts,
|
||||
updateRuleName,
|
||||
|
@ -117,4 +118,20 @@ describe('Kapacitor.Reducers.rules', () => {
|
|||
expect(Object.keys(newState).length).to.equal(1);
|
||||
expect(newState[rule1]).to.equal(initialState[rule1]);
|
||||
});
|
||||
|
||||
it('can update details', () => {
|
||||
const ruleID = 1;
|
||||
const details = 'im some rule details';
|
||||
|
||||
const initialState = {
|
||||
[ruleID]: {
|
||||
id: ruleID,
|
||||
queryID: 988,
|
||||
details: '',
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(initialState, updateDetails(ruleID, details));
|
||||
expect(newState[ruleID].details).to.equal(details);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -87,6 +87,16 @@ export function updateMessage(ruleID, message) {
|
|||
};
|
||||
}
|
||||
|
||||
export function updateDetails(ruleID, details) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_DETAILS',
|
||||
payload: {
|
||||
ruleID,
|
||||
details,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAlerts(ruleID, alerts) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_ALERTS',
|
||||
|
|
|
@ -15,15 +15,11 @@ export const RuleMessage = React.createClass({
|
|||
rule: shape({}).isRequired,
|
||||
actions: shape({
|
||||
updateMessage: func.isRequired,
|
||||
updateDetails: func.isRequired,
|
||||
}).isRequired,
|
||||
enabledAlerts: arrayOf(string.isRequired).isRequired,
|
||||
},
|
||||
|
||||
handleChangeMessage() {
|
||||
const {actions, rule} = this.props;
|
||||
actions.updateMessage(rule.id, this.message.value);
|
||||
},
|
||||
|
||||
handleChooseAlert(item) {
|
||||
const {actions} = this.props;
|
||||
actions.updateAlerts(item.ruleID, [item.text]);
|
||||
|
@ -34,19 +30,19 @@ export const RuleMessage = React.createClass({
|
|||
const alerts = this.props.enabledAlerts.map((text) => {
|
||||
return {text, ruleID: rule.id};
|
||||
});
|
||||
const selectedAlert = rule.alerts[0];
|
||||
|
||||
return (
|
||||
<div className="kapacitor-rule-section">
|
||||
<h3 className="rule-section-heading">Alert Message</h3>
|
||||
<div className="rule-section-body">
|
||||
<textarea
|
||||
className="alert-message"
|
||||
className="alert-text message"
|
||||
ref={(r) => this.message = r}
|
||||
onChange={() => actions.updateMessage(rule.id, this.message.value)}
|
||||
placeholder='Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}'
|
||||
value={rule.message}
|
||||
/>
|
||||
|
||||
<div className="alert-message--formatting">
|
||||
<p>Templates:</p>
|
||||
{
|
||||
|
@ -62,9 +58,19 @@ export const RuleMessage = React.createClass({
|
|||
}
|
||||
<ReactTooltip effect="solid" html={true} offset={{top: -4}} class="influx-tooltip kapacitor-tooltip" />
|
||||
</div>
|
||||
{
|
||||
selectedAlert === 'smtp' ?
|
||||
<textarea
|
||||
className="alert-text details"
|
||||
ref={(r) => this.details = r}
|
||||
onChange={() => actions.updateDetails(rule.id, this.details.value)}
|
||||
placeholder="Put email body text here"
|
||||
value={rule.details}
|
||||
/> : null
|
||||
}
|
||||
<div className="rule-section--item bottom alert-message--endpoint">
|
||||
<p>Send this Alert to:</p>
|
||||
<Dropdown className="size-256 dropdown-kapacitor" selected={rule.alerts[0] || 'Choose an output'} items={alerts} onChoose={this.handleChooseAlert} />
|
||||
<Dropdown className="size-256 dropdown-kapacitor" selected={selectedAlert || 'Choose an output'} items={alerts} onChoose={this.handleChooseAlert} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -85,6 +85,14 @@ export default function rules(state = {}, action) {
|
|||
delete state[ruleID];
|
||||
return Object.assign({}, state);
|
||||
}
|
||||
|
||||
case 'UPDATE_RULE_DETAILS': {
|
||||
const {ruleID, details} = action.payload;
|
||||
|
||||
return {...state, ...{
|
||||
[ruleID]: {...state[ruleID], details},
|
||||
}};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -345,9 +345,8 @@ div.qeditor.kapacitor-metric-selector {
|
|||
}
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
.alert-text {
|
||||
border: 2px solid $g3-castle;
|
||||
border-radius: $kap-radius-lg $kap-radius-lg 0 0;
|
||||
background-color: $kapacitor-graphic-color;
|
||||
margin: 0;
|
||||
padding: $kap-padding-sm $kap-padding-lg;
|
||||
|
@ -362,8 +361,8 @@ div.qeditor.kapacitor-metric-selector {
|
|||
font-size: $kapacitor-font-sm;
|
||||
line-height: 17px;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
|
||||
@include custom-scrollbar($kapacitor-graphic-color,$kapacitor-accent);
|
||||
|
||||
|
@ -379,6 +378,14 @@ div.qeditor.kapacitor-metric-selector {
|
|||
&::-moz-placeholder { color: $g9-mountain; }
|
||||
&:-ms-input-placeholder { color: $g9-mountain; }
|
||||
&:-moz-placeholder { color: $g9-mountain; }
|
||||
|
||||
&.message {
|
||||
border-radius: $kap-radius-lg $kap-radius-lg 0 0;
|
||||
}
|
||||
|
||||
&.details {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-message--endpoint {
|
||||
|
|
Loading…
Reference in New Issue