Add validation to Alert Rule message templates

pull/4479/head
Deniz Kusefoglu 2018-09-20 18:13:40 -07:00
parent 49f5c39c76
commit 7e2c7877bb
10 changed files with 217 additions and 17 deletions

View File

@ -53,6 +53,7 @@
1. [#4388](https://github.com/influxdata/chronograf/pull/4388): Fix logs to progressively load results and provide feedback on search
1. [#4408](https://github.com/influxdata/chronograf/pull/4408): Render null data point values in Alerts Table as mdashes
1. [#4466](https://github.com/influxdata/chronograf/pull/4466): Maintain focus on Flux Editor text area when adding nodes via code
1. [#4479](https://github.com/influxdata/chronograf/pull/4479): Add validation to Alert Rule messages
## v1.6.2 [2018-09-06]

View File

@ -9,6 +9,7 @@ import ValuesSection from 'src/kapacitor/components/ValuesSection'
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
import RuleHandlers from 'src/kapacitor/components/RuleHandlers'
import RuleMessage from 'src/kapacitor/components/RuleMessage'
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
import {createRule, editRule} from 'src/kapacitor/apis'
import buildInfluxQLQuery from 'src/utils/influxql'
@ -234,6 +235,10 @@ class KapacitorRule extends Component<Props, State> {
return notifyAlertRuleRequiresConditionValue()
}
if (rule.message && !isValidMessage(rule.message)) {
return 'Please correct template values in the alert message.'
}
return ''
}

View File

@ -6,7 +6,7 @@ import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes'
// Utils
import buildQueries from 'src/utils/buildQueriesForGraphs'
import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay'
import underlayCallback from 'src/kapacitor/utils/ruleGraphUnderlay'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions'
import {

View File

@ -3,7 +3,7 @@ import ReactTooltip from 'react-tooltip'
interface Props {
onSave: () => void
validationError?: string
validationError: string
}
const RuleHeaderSave: SFC<Props> = ({onSave, validationError}) => (

View File

@ -1,4 +1,5 @@
import React, {SFC, ChangeEvent} from 'react'
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
import {AlertRule} from 'src/types'
@ -7,16 +8,40 @@ interface Props {
updateMessage: (e: ChangeEvent<HTMLTextAreaElement>) => void
}
const RuleMessageText: SFC<Props> = ({rule, updateMessage}) => (
<div className="rule-builder--message">
<textarea
className="form-control input-sm form-malachite monotype"
onChange={updateMessage}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
</div>
)
const RuleMessageText: SFC<Props> = ({rule, updateMessage}) => {
const isValid = isValidMessage(rule.message)
const textAreaClass = isValid ? 'form-malachite' : 'form-volcano'
const iconName = isValid ? 'checkmark' : 'stop'
const validationCopy = isValid
? 'Alert message is syntactically correct.'
: 'Please correct templates in alert message.'
const outputStatusClass = isValid
? 'query-status-output--success'
: 'query-status-output--error'
return (
<div className="rule-builder--message">
<textarea
className={`form-control input-sm monotype ${textAreaClass}`}
onChange={updateMessage}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
{rule.message ? (
<div className="query-editor--status">
<span className={`query-status-output ${outputStatusClass}`}>
<span className={`icon ${iconName}`} />
{validationCopy}
</span>
</div>
) : null}
</div>
)
}
export default RuleMessageText

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import {
Handler,
KeyMappings,
@ -147,8 +148,11 @@ export const RULE_MESSAGE_TEMPLATES: RuleMessageTemplate = {
},
tags: {
label: '{{.Tags}}',
text:
'Map of tags. Use <code>&#123;&#123; index .Tags &quot;key&quot; &#125;&#125;</code> to get a specific tag value',
text: 'Map of tags.',
},
tag: {
label: '{{ index .Tags "value" }}',
text: 'A specific tag value',
},
level: {
label: '{{.Level}}',
@ -156,15 +160,26 @@ export const RULE_MESSAGE_TEMPLATES: RuleMessageTemplate = {
'Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>',
},
fields: {
label: '{{.Fields}}',
text: 'Map of fields',
},
field: {
label: '{{ index .Fields "value" }}',
text:
'Map of fields. Use <code>&#123;&#123; index .Fields &quot;key&quot; &#125;&#125;</code> to get a specific field value',
text: 'A specific field value',
},
time: {
label: '{{.Time}}',
text: 'The time of the point that triggered the event',
},
}
export const RULE_MESSAGE_TEMPLATE_TEXTS = _.map(RULE_MESSAGE_TEMPLATES, t => {
const label = t.label
const templateRegexp = /((?:{{)([^{}]+)(?:}}))/g // matches {{*}} where star does not contain '{' or '}'
const match = templateRegexp.exec(label)
return match[2].trim()
})
// DEFAULT_HANDLERS are empty alert templates for handlers that don't exist in the kapacitor config
export const DEFAULT_HANDLERS: Handler[] = [
{

View File

@ -0,0 +1,58 @@
import _ from 'lodash'
import {RULE_MESSAGE_TEMPLATE_TEXTS} from 'src/kapacitor/constants'
export const isValidTemplate = (template: string): boolean => {
const exactMatch = !!_.find(RULE_MESSAGE_TEMPLATE_TEXTS, t => t === template)
const fieldsRegex = /(index .Fields ".+")/
const tagsRegex = /(index .Tags ".+")/
const fuzzyMatch = fieldsRegex.test(template) || tagsRegex.test(template)
return exactMatch || fuzzyMatch
}
export const mismatchedBrackets = (str: string): boolean => {
const arr = str.split('')
const accumulator: string[] = []
let isMismatched = false
arr.forEach(cur => {
if (cur === '{') {
accumulator.push('{')
}
if (cur === '}') {
const lastElt = accumulator.pop()
if (lastElt !== '{') {
isMismatched = true
}
}
})
if (accumulator.length !== 0) {
isMismatched = true
}
return isMismatched
}
export const isValidMessage = (message: string): boolean => {
if (message[message.length] === '}') {
message = message + ' '
}
const malformedTemplateRegexp1 = RegExp('(({{)([^{}]*)(})([^}]+))') // matches {{*} where star does not contain '{' or '}'
if (malformedTemplateRegexp1.test(message)) {
return false
}
if (mismatchedBrackets(message)) {
return false
}
const templateRegexp = /((?:{{)([^{}]*)(?:}}))/g // matches {{*}} where star does not contain '{' or '}'
const matches = []
let match = templateRegexp.exec(message)
while (match) {
matches[matches.length] = match[2].trim()
match = templateRegexp.exec(message)
}
return _.every(matches, m => isValidTemplate(m))
}
export default isValidMessage

View File

@ -228,8 +228,10 @@ export interface RuleMessageTemplate {
taskName: RuleMessage
group: RuleMessage
tags: RuleMessage
tag: RuleMessage
level: RuleMessage
fields: RuleMessage
field: RuleMessage
time: RuleMessage
}

View File

@ -0,0 +1,94 @@
import {
isValidMessage,
mismatchedBrackets,
isValidTemplate,
} from 'src/kapacitor/utils/alertMessageValidation'
import {RULE_MESSAGE_TEMPLATE_TEXTS} from 'src/kapacitor/constants'
describe('kapacitor.utils.alertMessageValidation', () => {
describe('isValidMessage', () => {
it('accepts message containing one simple template', () => {
const isValid = isValidMessage('{{.ID}}')
expect(isValid).toEqual(true)
})
it('accepts message containing one simple and one complex template', () => {
const isValid = isValidMessage('{{ index .Tags "something" }} {{.Name}}')
expect(isValid).toEqual(true)
})
it('accepts message containing templates and strings mixed', () => {
const isValid = isValidMessage(
'{{ index .Tags "moo" }} lkajsdflkjasdf {{.Name}}lksjdflsj'
)
expect(isValid).toEqual(true)
})
it('rejects message with invalid template', () => {
const isValid = isValidMessage('{{ I am invalid}}')
expect(isValid).toEqual(false)
})
it('rejects message containing template with missing closing bracket', () => {
const isValid = isValidMessage('{{ index .Tags "value" } {{.Name}}')
expect(isValid).toEqual(false)
})
it('rejects message containing non-matching brackets', () => {
const isValid = isValidMessage('{{ index .Tags "value" {{.Name}}')
expect(isValid).toEqual(false)
})
})
describe('mismatchedBrackets', () => {
it('String containing matched brackets is not mismatched', () => {
const isMismatched = mismatchedBrackets('{{}}')
expect(isMismatched).toEqual(false)
})
it('String containing matched brackets and other characters is not mismatched', () => {
const isMismatched = mismatchedBrackets('asdf{{asdfaasdas}}asdfa')
expect(isMismatched).toEqual(false)
})
it('String containing unmatched brackets is mismatched', () => {
const isMismatched = mismatchedBrackets('{{}')
expect(isMismatched).toEqual(true)
})
it('String containing unmatched brackets and other characters is mismatched', () => {
const isMismatched = mismatchedBrackets('asdf{{as}asdfa)')
expect(isMismatched).toEqual(true)
})
})
describe('isValidTemplate', () => {
it('is True for an exact match to a valid template', () => {
const isValid = isValidTemplate(RULE_MESSAGE_TEMPLATE_TEXTS[0])
expect(isValid).toEqual(true)
})
it('is False for a jibberish input', () => {
const isValid = isValidTemplate('laslkj;owaiu0294u,mxn')
expect(isValid).toEqual(false)
})
it('is True for a fuzzy match to tags', () => {
const isValid = isValidTemplate('(index .Tags "lalala")')
expect(isValid).toEqual(true)
})
it('is False for distorted version of tags', () => {
const isValid = isValidTemplate('(indeasdfx .Tags "lalala")')
expect(isValid).toEqual(false)
})
})
})