Signed-off-by: Wangchong Zhou <fffonion@gmail.com>
This commit is contained in:
Wangchong Zhou 2018-09-13 11:46:41 -07:00
parent c2742aa299
commit 9ebab25dfa
No known key found for this signature in database
GPG key ID: B607274584E8D5E5
2 changed files with 187 additions and 119 deletions

View file

@ -59,6 +59,7 @@ type MetricMapper struct {
Defaults mapperConfigDefaults `yaml:"defaults"` Defaults mapperConfigDefaults `yaml:"defaults"`
Mappings []MetricMapping `yaml:"mappings"` Mappings []MetricMapping `yaml:"mappings"`
FSM *mappingState FSM *mappingState
hasFSM bool
FSMNeedsBacktracking bool FSMNeedsBacktracking bool
// if doRegex is true, at least one matching rule is regex type // if doRegex is true, at least one matching rule is regex type
doRegex bool doRegex bool
@ -76,7 +77,7 @@ type templateFormatter struct {
type fsmBacktrackStackCursor struct { type fsmBacktrackStackCursor struct {
fieldIndex int fieldIndex int
captureIdx int captureIndex int
currentCapture string currentCapture string
state *mappingState state *mappingState
prev *fsmBacktrackStackCursor prev *fsmBacktrackStackCursor
@ -99,6 +100,7 @@ type MetricMapping struct {
HelpText string `yaml:"help"` HelpText string `yaml:"help"`
Action ActionType `yaml:"action"` Action ActionType `yaml:"action"`
MatchMetricType MetricType `yaml:"match_metric_type"` MatchMetricType MetricType `yaml:"match_metric_type"`
priority int
} }
type metricObjective struct { type metricObjective struct {
@ -225,7 +227,10 @@ func (m *MetricMapper) InitFromYAMLString(fileContents string) error {
currentMapping.Action = ActionTypeMap currentMapping.Action = ActionTypeMap
} }
currentMapping.priority = i
if currentMapping.MatchType == MatchTypeGlob { if currentMapping.MatchType == MatchTypeGlob {
n.hasFSM = true
if !metricLineRE.MatchString(currentMapping.Match) { if !metricLineRE.MatchString(currentMapping.Match) {
return fmt.Errorf("invalid match: %s", currentMapping.Match) return fmt.Errorf("invalid match: %s", currentMapping.Match)
} }
@ -235,6 +240,7 @@ func (m *MetricMapper) InitFromYAMLString(fileContents string) error {
// fill into our FSM // fill into our FSM
roots := []*mappingState{} roots := []*mappingState{}
if currentMapping.MatchMetricType == "" { if currentMapping.MatchMetricType == "" {
// if metricType not specified, connect the state from all three types
for _, metricType := range []MetricType{MetricTypeCounter, MetricTypeTimer, MetricTypeGauge, ""} { for _, metricType := range []MetricType{MetricTypeCounter, MetricTypeTimer, MetricTypeGauge, ""} {
roots = append(roots, n.FSM.transitions[string(metricType)]) roots = append(roots, n.FSM.transitions[string(metricType)])
} }
@ -290,20 +296,7 @@ func (m *MetricMapper) InitFromYAMLString(fileContents string) error {
currentMapping.regex = regex currentMapping.regex = regex
} }
n.doRegex = true n.doRegex = true
} /*else if currentMapping.MatchType == MatchTypeGlob { }
if !metricLineRE.MatchString(currentMapping.Match) {
return fmt.Errorf("invalid match: %s", currentMapping.Match)
}
// Translate the glob-style metric match line into a proper regex that we
// can use to match metrics later on.
metricRe := strings.Replace(currentMapping.Match, ".", "\\.", -1)
metricRe = strings.Replace(metricRe, "*", "([^.]*)", -1)
if regex, err := regexp.Compile("^" + metricRe + "$"); err != nil {
return fmt.Errorf("invalid match %s. cannot compile regex in mapping: %v", currentMapping.Match, err)
} else {
currentMapping.regex = regex
}
} */
if currentMapping.TimerType == "" { if currentMapping.TimerType == "" {
currentMapping.TimerType = n.Defaults.TimerType currentMapping.TimerType = n.Defaults.TimerType
@ -324,24 +317,15 @@ func (m *MetricMapper) InitFromYAMLString(fileContents string) error {
m.Defaults = n.Defaults m.Defaults = n.Defaults
m.Mappings = n.Mappings m.Mappings = n.Mappings
if len(n.FSM.transitions) > 0 { if n.hasFSM {
m.hasFSM = n.hasFSM
m.FSM = n.FSM m.FSM = n.FSM
m.doRegex = n.doRegex m.doRegex = n.doRegex
if m.dumpFSMPath != "" { if m.dumpFSMPath != "" {
dumpFSM(m.dumpFSMPath, m.FSM) dumpFSM(m.dumpFSMPath, m.FSM)
} }
// backtracking only makes sense when we disbled ordering of rules m.FSMNeedsBacktracking = needBacktracking(&n)
// where transistions are stored in (unordered) map
if m.Defaults.GlobDisbleOrdering || true {
backtrackingRules := findBacktrackRules(&n)
if len(backtrackingRules) > 0 {
for _, rule := range backtrackingRules {
log.Warnf("backtracking required because of match \"%s\", matching performance may be degraded\n", rule)
}
m.FSMNeedsBacktracking = true
}
}
} }
if m.MappingsCount != nil { if m.MappingsCount != nil {
@ -356,12 +340,13 @@ func (m *MetricMapper) SetDumpFSMPath(path string) error {
return nil return nil
} }
func findBacktrackRules(n *MetricMapper) []string { func needBacktracking(n *MetricMapper) bool {
var found []string needBacktrack := false
// rule A and B that has same length and // rule A and B that has same length and
// A one has * in rules but is not a superset of B makes it needed for backtracking // A one has * in rules but is not a superset of B makes it needed for backtracking
ruleByLength := make(map[int][]string) ruleByLength := make(map[int][]string)
ruleREByLength := make(map[int][]*regexp.Regexp) ruleREByLength := make(map[int][]*regexp.Regexp)
rulesOrderByLength := make(map[int][]int)
// first sort rules by length // first sort rules by length
for _, mapping := range n.Mappings { for _, mapping := range n.Mappings {
@ -379,6 +364,7 @@ func findBacktrackRules(n *MetricMapper) []string {
} }
// put into array no matter there's error or not, we will skip later if regex is nil // put into array no matter there's error or not, we will skip later if regex is nil
ruleREByLength[l] = append(ruleREByLength[l], regex) ruleREByLength[l] = append(ruleREByLength[l], regex)
rulesOrderByLength[l] = append(rulesOrderByLength[l], mapping.priority)
} }
for l, rules := range ruleByLength { for l, rules := range ruleByLength {
@ -386,26 +372,49 @@ func findBacktrackRules(n *MetricMapper) []string {
continue continue
} }
rulesRE := ruleREByLength[l] rulesRE := ruleREByLength[l]
rulesOrder := rulesOrderByLength[l]
// for each rule r1 in rules that has * inside, check if r1 is the superset of any rules // for each rule r1 in rules that has * inside, check if r1 is the superset of any rules
// if not then r1 is a rule that leads to backtrack // if not then r1 is a rule that leads to backtrack
for i1, r1 := range rules { for i1, r1 := range rules {
hasSubset := false currentRuleNeedBacktrack := true
re1 := rulesRE[i1] re1 := rulesRE[i1]
if re1 == nil || strings.Index(r1, "*") == -1 { if re1 == nil || strings.Index(r1, "*") == -1 {
continue continue
} }
for _, r2 := range rules { for i2, r2 := range rules {
if r2 != r1 && len(re1.FindStringSubmatchIndex(r2)) > 0 { if i2 != i1 && len(re1.FindStringSubmatchIndex(r2)) > 0 {
log.Warnf("rule \"%s\" is a super set of rule \"%s\", the later will never be matched\n", r1, r2) // log if we care about ordering and the superset occurs before
hasSubset = true if !n.Defaults.GlobDisbleOrdering && rulesOrder[i1] < rulesOrder[i2] {
log.Warnf("match \"%s\" is a super set of match \"%s\" but in a lower order, "+
"the first will never be matched\n", r1, r2)
}
currentRuleNeedBacktrack = false
} }
} }
if !hasSubset { for i2, re2 := range rulesRE {
found = append(found, r1) // especially, if r1 is a subset of other rule, we don't need backtrack
// because either we turned on ordering
// or we disabled ordering and can't match it with backtrack
if i2 != i1 && re2 != nil && len(re2.FindStringSubmatchIndex(r1)) > 0 {
currentRuleNeedBacktrack = false
}
}
if currentRuleNeedBacktrack {
log.Warnf("backtracking required because of match \"%s\", "+
"matching performance may be degraded\n", r1)
needBacktrack = true
} }
} }
} }
return found if !n.Defaults.GlobDisbleOrdering {
// backtracking only makes sense when we disbled ordering of rules
// where transistions are stored in (unordered) map
// note: don't move this branch to the beginning of this function
// since we need logs for superset rules
return true
} else {
return needBacktrack
}
} }
func dumpFSM(fileName string, root *mappingState) { func dumpFSM(fileName string, root *mappingState) {
@ -451,79 +460,112 @@ func (m *MetricMapper) InitFromFile(fileName string) error {
func (m *MetricMapper) GetMapping(statsdMetric string, statsdMetricType MetricType) (*MetricMapping, prometheus.Labels, bool) { func (m *MetricMapper) GetMapping(statsdMetric string, statsdMetricType MetricType) (*MetricMapping, prometheus.Labels, bool) {
// glob matching // glob matching
if root := m.FSM; root != nil { if m.hasFSM {
root = root.transitions[string(statsdMetricType)]
matchFields := strings.Split(statsdMetric, ".") matchFields := strings.Split(statsdMetric, ".")
root := m.FSM.transitions[string(statsdMetricType)]
captures := make(map[int]string, len(matchFields)) captures := make(map[int]string, len(matchFields))
captureIdx := 0 captureIdx := 0
var backtrackCursor *fsmBacktrackStackCursor var backtrackCursor *fsmBacktrackStackCursor
backtrackCursor = nil backtrackCursor = nil
resumeFromBacktrack := false
var result *MetricMapping
filedsCount := len(matchFields) filedsCount := len(matchFields)
i := 0 i := 0
var state *mappingState
for { for {
for i < filedsCount { for {
if root.transitions == nil { var prs bool
break if !resumeFromBacktrack {
} if len(root.transitions) > 0 {
field := matchFields[i] field := matchFields[i]
state, prs := root.transitions[field] state, prs = root.transitions[field]
fieldsLeft := filedsCount - i - 1 fieldsLeft := filedsCount - i - 1
// also compare length upfront to avoid unnecessary loop or backtrack // also compare length upfront to avoid unnecessary loop or backtrack
if !prs || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength { if !prs || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength {
state, prs = root.transitions["*"] state, prs = root.transitions["*"]
if !prs || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength { if !prs || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength {
break
} else {
captures[captureIdx] = field
captureIdx++
}
} else if m.FSMNeedsBacktracking {
altState, prs := root.transitions["*"]
if !prs || fieldsLeft > altState.maxRemainingLength || fieldsLeft < altState.minRemainingLength {
} else {
// push to stack
newCursor := fsmBacktrackStackCursor{prev: backtrackCursor, state: altState,
fieldIndex: i,
captureIndex: captureIdx, currentCapture: field,
}
if backtrackCursor != nil {
backtrackCursor.next = &newCursor
}
backtrackCursor = &newCursor
}
}
} else { // root.transitions == nil
break break
} else {
captures[captureIdx] = field
captureIdx++
}
} else if m.FSMNeedsBacktracking {
otherState, prs := root.transitions["*"]
if !prs || fieldsLeft > otherState.maxRemainingLength || fieldsLeft < otherState.minRemainingLength {
} else {
newCursor := fsmBacktrackStackCursor{prev: backtrackCursor, state: otherState,
fieldIndex: i + 1,
captureIdx: captureIdx + 1, currentCapture: field,
}
if backtrackCursor != nil {
backtrackCursor.next = &newCursor
}
backtrackCursor = &newCursor
} }
} // backtrack will resume from here
} // do we reach a final state?
// found! if state.result != nil && i == filedsCount-1 {
if state != nil && state.result != nil && i == filedsCount-1 { if m.Defaults.GlobDisbleOrdering {
mapping := *state.result result = state.result
state.result.Name = formatTemplate(mapping.NameFormatter, captures) // do a double break
goto formatLabels
labels := prometheus.Labels{} } else if result == nil || result.priority > state.result.priority {
for label := range mapping.Labels { // if we care about ordering, try to find a result with highest prioity
labels[label] = formatTemplate(mapping.LabelsFormatter[label], captures) result = state.result
} }
return state.result, labels, true
}
root = state
i++
}
// if we are not doing backtracking or all path has been travesaled
if backtrackCursor == nil {
// if there's no regex match type, return immediately
if !m.doRegex {
return nil, nil, false
} else {
break break
} }
i++
if i >= filedsCount {
break
}
resumeFromBacktrack = false
root = state
}
if backtrackCursor == nil {
// if we are not doing backtracking or all path has been travesaled
break
} else { } else {
// pop one from stack // pop one from stack
root = backtrackCursor.state state = backtrackCursor.state
root = state
i = backtrackCursor.fieldIndex i = backtrackCursor.fieldIndex
captureIdx = backtrackCursor.captureIdx captureIdx = backtrackCursor.captureIndex + 1
// put the * capture back // put the * capture back
captures[captureIdx-1] = backtrackCursor.currentCapture captures[captureIdx-1] = backtrackCursor.currentCapture
backtrackCursor = backtrackCursor.prev backtrackCursor = backtrackCursor.prev
if backtrackCursor != nil {
// deref for GC
backtrackCursor.next = nil
}
resumeFromBacktrack = true
} }
} }
formatLabels:
// format name and labels
if result != nil {
result.Name = formatTemplate(result.NameFormatter, captures)
labels := prometheus.Labels{}
for label := range result.Labels {
labels[label] = formatTemplate(result.LabelsFormatter[label], captures)
}
return result, labels, true
} else if !m.doRegex {
// if there's no regex match type, return immediately
return nil, nil, false
}
} }
// regex matching // regex matching

View file

@ -14,7 +14,9 @@
package mapper package mapper
import ( import (
"fmt"
"testing" "testing"
"time"
) )
type mappings map[string]struct { type mappings map[string]struct {
@ -142,24 +144,26 @@ mappings:
//Config with backtracking //Config with backtracking
{ {
config: ` config: `
defaults:
glob_disable_ordering: true
mappings: mappings:
- match: test.*.bbb - match: backtrack.*.bbb
name: "testb" name: "testb"
labels: labels:
label: "${1}_foo" label: "${1}_foo"
- match: test.justatest.aaa - match: backtrack.justatest.aaa
name: "testa" name: "testa"
labels: labels:
label: "${1}_foo" label: "${1}_foo"
`, `,
mappings: mappings{ mappings: mappings{
"test.good.bbb": { "backtrack.good.bbb": {
name: "testb", name: "testb",
labels: map[string]string{ labels: map[string]string{
"label": "good_foo", "label": "good_foo",
}, },
}, },
"test.justatest.bbb": { "backtrack.justatest.bbb": {
name: "testb", name: "testb",
labels: map[string]string{ labels: map[string]string{
"label": "justatest_foo", "label": "justatest_foo",
@ -167,22 +171,58 @@ mappings:
}, },
}, },
}, },
//Config with super sets //Config with super sets, disables ordering
{ {
config: ` config: `
defaults:
glob_disable_ordering: true
mappings: mappings:
- match: test.*.bbb - match: noorder.*.*
name: "testa"
labels:
label: "${1}_foo"
- match: noorder.*.bbb
name: "testb" name: "testb"
labels: labels:
label: "${1}_foo" label: "${1}_foo"
- match: test.*.* - match: noorder.ccc.bbb
name: "testc"
labels:
label: "ccc_foo"
`,
mappings: mappings{
"noorder.good.bbb": {
name: "testb",
labels: map[string]string{
"label": "good_foo",
},
},
"noorder.ccc.bbb": {
name: "testc",
labels: map[string]string{
"label": "ccc_foo",
},
},
},
},
//Config with super sets, keeps ordering
{
config: `
defaults:
glob_disable_ordering: false
mappings:
- match: order.*.*
name: "testa" name: "testa"
labels: labels:
label: "${1}_foo" label: "${1}_foo"
- match: order.*.bbb
name: "testb"
labels:
label: "${1}_foo"
`, `,
mappings: mappings{ mappings: mappings{
"test.good.bbb": { "order.good.bbb": {
name: "testb", name: "testa",
labels: map[string]string{ labels: map[string]string{
"label": "good_foo", "label": "good_foo",
}, },
@ -568,17 +608,17 @@ mappings:
} }
} }
/*func TestRPS(t *testing.T) { func TestRPS(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config string config string
configBad bool configBad bool
mappings mappings mappings mappings
}{ }{
// Empty config.
{},
// Config with several mapping definitions. // Config with several mapping definitions.
{ {
config: `--- config: `---
defaults:
glob_disable_ordering: true
mappings: mappings:
- match: test.dispatcher.*.*.* - match: test.dispatcher.*.*.*
name: "dispatch_events" name: "dispatch_events"
@ -678,29 +718,15 @@ mappings:
} }
var dummyMetricType MetricType = "" var dummyMetricType MetricType = ""
start := int32(time.Now().Unix()) start := time.Now()
for j := 1; j < 100000; j++ { for j := 1; j < 100000; j++ {
for metric, mapping := range scenario.mappings { for metric, _ := range scenario.mappings {
m, labels, present := mapper.GetMapping(metric, dummyMetricType) mapper.GetMapping(metric, dummyMetricType)
if present && mapping.name != "" && m.Name != mapping.name {
t.Fatalf("%d.%q: Expected name %v, got %v", i, metric, m.Name, mapping.name)
}
if mapping.notPresent && present {
t.Fatalf("%d.%q: Expected metric to not be present", i, metric)
}
if len(labels) != len(mapping.labels) {
t.Fatalf("%d.%q: Expected %d labels, got %d", i, metric, len(mapping.labels), len(labels))
}
for label, value := range labels {
if mapping.labels[label] != value {
t.Fatalf("%d.%q: Expected labels %v, got %v", i, metric, mapping, labels)
}
}
} }
} }
fmt.Println("finished in", int32(time.Now().Unix())-start) fmt.Println("finished 100000 iterations in", time.Now().Sub(start))
} }
}*/ }
func TestAction(t *testing.T) { func TestAction(t *testing.T) {
scenarios := []struct { scenarios := []struct {