diff --git a/filters/filter.go b/filters/filter.go index 7e2ad65..b4ab48d 100644 --- a/filters/filter.go +++ b/filters/filter.go @@ -24,7 +24,6 @@ import ( "bytes" "errors" "fmt" - "strings" ) var ( @@ -60,7 +59,10 @@ const ( multiRangeStartChar = '{' multiRangeEndChar = '}' invalidNestedChars = "?[{" - multiRangeSplit = "," +) + +var ( + multiRangeSplit = []byte(",") ) // Filter matches a string against certain conditions @@ -68,13 +70,15 @@ type Filter interface { fmt.Stringer // Matches returns true if the conditions are met - Matches(val string) bool + Matches(val []byte) bool } // NewFilter supports startsWith, endsWith, contains and a single wildcard -// along with negation and glob matching support -// TODO(martinm): Provide more detailed error messages -func NewFilter(pattern string) (Filter, error) { +// along with negation and glob matching support. +// NOTE: Currently only supports ASCII matching and has zero compatibility +// with UTF8 so you should make sure all matches are done against ASCII only. +func NewFilter(pattern []byte) (Filter, error) { + // TODO(martinm): Provide more detailed error messages if len(pattern) == 0 { return newEqualityFilter(pattern), nil } @@ -98,8 +102,8 @@ func NewFilter(pattern string) (Filter, error) { // newWildcardFilter creates a filter that segments the pattern based // on wildcards, creating a rangeFilter for each segment -func newWildcardFilter(pattern string) (Filter, error) { - wIdx := strings.IndexRune(pattern, wildcardChar) +func newWildcardFilter(pattern []byte) (Filter, error) { + wIdx := bytes.IndexRune(pattern, wildcardChar) if wIdx == -1 { // No wildcards @@ -116,7 +120,7 @@ func newWildcardFilter(pattern string) (Filter, error) { return newRangeFilter(pattern[:len(pattern)-1], false, start) } - secondWIdx := strings.IndexRune(pattern[wIdx+1:], wildcardChar) + secondWIdx := bytes.IndexRune(pattern[wIdx+1:], wildcardChar) if secondWIdx == -1 { if wIdx == 0 { // Single wildcard at start @@ -147,7 +151,7 @@ func newWildcardFilter(pattern string) (Filter, error) { // newRangeFilter creates a filter that checks for ranges (? or [] or {}) and segments // the pattern into a multiple chain filters based on ranges found -func newRangeFilter(pattern string, backwards bool, seg chainSegment) (Filter, error) { +func newRangeFilter(pattern []byte, backwards bool, seg chainSegment) (Filter, error) { var filters []chainFilter eqIdx := -1 for i := 0; i < len(pattern); i++ { @@ -159,7 +163,7 @@ func newRangeFilter(pattern string, backwards bool, seg chainSegment) (Filter, e eqIdx = -1 } - endIdx := strings.IndexRune(pattern[i+1:], singleRangeEndChar) + endIdx := bytes.IndexRune(pattern[i+1:], singleRangeEndChar) if endIdx == -1 { return nil, errInvalidFilterPattern } @@ -179,7 +183,7 @@ func newRangeFilter(pattern string, backwards bool, seg chainSegment) (Filter, e eqIdx = -1 } - endIdx := strings.IndexRune(pattern[i+1:], multiRangeEndChar) + endIdx := bytes.IndexRune(pattern[i+1:], multiRangeEndChar) if endIdx == -1 { return nil, errInvalidFilterPattern } @@ -218,32 +222,32 @@ type allowFilter struct{} func newAllowFilter() Filter { return allowAllFilter } func (f allowFilter) String() string { return "All" } -func (f allowFilter) Matches(val string) bool { return true } +func (f allowFilter) Matches(val []byte) bool { return true } // equalityFilter is a filter that matches exact values type equalityFilter struct { - pattern string + pattern []byte } -func newEqualityFilter(pattern string) Filter { +func newEqualityFilter(pattern []byte) Filter { return &equalityFilter{pattern: pattern} } func (f *equalityFilter) String() string { - return "Equals(\"" + f.pattern + "\")" + return "Equals(\"" + string(f.pattern) + "\")" } -func (f *equalityFilter) Matches(val string) bool { - return f.pattern == val +func (f *equalityFilter) Matches(val []byte) bool { + return bytes.Equal(f.pattern, val) } // containsFilter is a filter that performs contains matches type containsFilter struct { - pattern string + pattern []byte } -func newContainsFilter(pattern string) (Filter, error) { - if strings.ContainsAny(pattern, invalidNestedChars) { +func newContainsFilter(pattern []byte) (Filter, error) { + if bytes.ContainsAny(pattern, invalidNestedChars) { return nil, errInvalidFilterPattern } @@ -251,11 +255,11 @@ func newContainsFilter(pattern string) (Filter, error) { } func (f *containsFilter) String() string { - return "Contains(\"" + f.pattern + "\")" + return "Contains(\"" + string(f.pattern) + "\")" } -func (f *containsFilter) Matches(val string) bool { - return strings.Contains(val, f.pattern) +func (f *containsFilter) Matches(val []byte) bool { + return bytes.Contains(val, f.pattern) } // negationFilter is a filter that matches the opposite of the provided filter @@ -271,7 +275,7 @@ func (f *negationFilter) String() string { return "Not(" + f.filter.String() + ")" } -func (f *negationFilter) Matches(val string) bool { +func (f *negationFilter) Matches(val []byte) bool { return !f.filter.Matches(val) } @@ -300,7 +304,7 @@ func (f *multiFilter) String() string { return buf.String() } -func (f *multiFilter) Matches(val string) bool { +func (f *multiFilter) Matches(val []byte) bool { if len(f.filters) == 0 { return true } @@ -324,34 +328,34 @@ func (f *multiFilter) Matches(val string) bool { type chainFilter interface { fmt.Stringer - matches(val string) (string, bool) + matches(val []byte) ([]byte, bool) } // equalityChainFilter is a filter that performs equality string matches // from either the front or back of the string type equalityChainFilter struct { - pattern string + pattern []byte backwards bool } -func newEqualityChainFilter(pattern string, backwards bool) chainFilter { +func newEqualityChainFilter(pattern []byte, backwards bool) chainFilter { return &equalityChainFilter{pattern: pattern, backwards: backwards} } func (f *equalityChainFilter) String() string { - return "Equals(\"" + f.pattern + "\")" + return "Equals(\"" + string(f.pattern) + "\")" } -func (f *equalityChainFilter) matches(val string) (string, bool) { - if f.backwards && strings.HasSuffix(val, f.pattern) { +func (f *equalityChainFilter) matches(val []byte) ([]byte, bool) { + if f.backwards && bytes.HasSuffix(val, f.pattern) { return val[:len(val)-len(f.pattern)], true } - if !f.backwards && strings.HasPrefix(val, f.pattern) { + if !f.backwards && bytes.HasPrefix(val, f.pattern) { return val[len(f.pattern):], true } - return "", false + return nil, false } // singleAnyCharFilter is a filter that allows any one char @@ -369,9 +373,9 @@ func newSingleAnyCharFilter(backwards bool) chainFilter { func (f *singleAnyCharFilter) String() string { return "AnyChar" } -func (f *singleAnyCharFilter) matches(val string) (string, bool) { +func (f *singleAnyCharFilter) matches(val []byte) ([]byte, bool) { if len(val) == 0 { - return "", false + return nil, false } if f.backwards { @@ -383,7 +387,7 @@ func (f *singleAnyCharFilter) matches(val string) (string, bool) { // newSingleRangeFilter creates a filter that performs range matching // on a single char -func newSingleRangeFilter(pattern string, backwards bool) (chainFilter, error) { +func newSingleRangeFilter(pattern []byte, backwards bool) (chainFilter, error) { if len(pattern) == 0 { return nil, errInvalidFilterPattern } @@ -401,7 +405,7 @@ func newSingleRangeFilter(pattern string, backwards bool) (chainFilter, error) { return nil, errInvalidFilterPattern } - patterns := make([]string, 0, len(pattern)%3) + patterns := make([][]byte, 0, len(pattern)%3) for i := 0; i < len(pattern); i += 3 { if pattern[i+1] != rangeChar || pattern[i] > pattern[i+2] { return nil, errInvalidFilterPattern @@ -419,7 +423,7 @@ func newSingleRangeFilter(pattern string, backwards bool) (chainFilter, error) { // singleRangeFilter is a filter that performs a single character match against // a range of chars given in a range format eg. [a-z] type singleRangeFilter struct { - patterns []string + patterns [][]byte backwards bool negate bool } @@ -431,12 +435,14 @@ func (f *singleRangeFilter) String() string { negateSuffix = ")" } - return negatePrefix + "Range(\"" + strings.Join(f.patterns, " "+string(Disjunction)+" ") + "\")" + negateSuffix + return negatePrefix + "Range(\"" + + string(bytes.Join(f.patterns, []byte(fmt.Sprintf(" %s ", Disjunction)))) + + "\")" + negateSuffix } -func (f *singleRangeFilter) matches(val string) (string, bool) { +func (f *singleRangeFilter) matches(val []byte) ([]byte, bool) { if len(val) == 0 { - return "", false + return nil, false } match := false @@ -464,7 +470,7 @@ func (f *singleRangeFilter) matches(val string) (string, bool) { // singleCharSetFilter is a filter that performs a single character match against // a set of chars given explicity eg. [abcdefg] type singleCharSetFilter struct { - pattern string + pattern []byte backwards bool negate bool } @@ -476,12 +482,12 @@ func (f *singleCharSetFilter) String() string { negateSuffix = ")" } - return negatePrefix + "Range(\"" + f.pattern + "\")" + negateSuffix + return negatePrefix + "Range(\"" + string(f.pattern) + "\")" + negateSuffix } -func (f *singleCharSetFilter) matches(val string) (string, bool) { +func (f *singleCharSetFilter) matches(val []byte) ([]byte, bool) { if len(val) == 0 { - return "", false + return nil, false } match := false @@ -511,41 +517,41 @@ func (f *singleCharSetFilter) matches(val string) (string, bool) { // multiCharRangeFilter is a filter that performs matches against multiple sets of chars // eg. {abc,defg} type multiCharSequenceFilter struct { - patterns []string + patterns [][]byte backwards bool } -func newMultiCharSequenceFilter(patterns string, backwards bool) (chainFilter, error) { +func newMultiCharSequenceFilter(patterns []byte, backwards bool) (chainFilter, error) { if len(patterns) == 0 { return nil, errInvalidFilterPattern } return &multiCharSequenceFilter{ - patterns: strings.Split(patterns, multiRangeSplit), + patterns: bytes.Split(patterns, multiRangeSplit), backwards: backwards, }, nil } func (f *multiCharSequenceFilter) String() string { - return "Range(\"" + strings.Join(f.patterns, multiRangeSplit) + "\")" + return "Range(\"" + string(bytes.Join(f.patterns, multiRangeSplit)) + "\")" } -func (f *multiCharSequenceFilter) matches(val string) (string, bool) { +func (f *multiCharSequenceFilter) matches(val []byte) ([]byte, bool) { if len(val) == 0 { - return "", false + return nil, false } for _, pattern := range f.patterns { - if f.backwards && strings.HasSuffix(val, pattern) { + if f.backwards && bytes.HasSuffix(val, pattern) { return val[:len(val)-len(pattern)], true } - if !f.backwards && strings.HasPrefix(val, pattern) { + if !f.backwards && bytes.HasPrefix(val, pattern) { return val[len(pattern):], true } } - return "", false + return nil, false } // multiChainFilter chains multiple chainFilters together with && @@ -586,7 +592,7 @@ func (f *multiChainFilter) String() string { return buf.String() } -func (f *multiChainFilter) Matches(val string) bool { +func (f *multiChainFilter) Matches(val []byte) bool { if len(f.filters) == 0 { return true } @@ -609,7 +615,7 @@ func (f *multiChainFilter) Matches(val string) bool { } } - if f.seg == middle && val != "" { + if f.seg == middle && len(val) != 0 { // chain was middle segment and some value was left over at end of chain return false } diff --git a/filters/filter_benchmark_test.go b/filters/filter_benchmark_test.go index dcc3f8f..61fc78f 100644 --- a/filters/filter_benchmark_test.go +++ b/filters/filter_benchmark_test.go @@ -21,13 +21,16 @@ package filters import ( + "bytes" "fmt" - "strings" "testing" ) var ( - testFlatID = "tagname1=tagvalue1,tagname3=tagvalue3,tagname4=tagvalue4,tagname2=tagvalue2,tagname6=tagvalue6,tagname5=tagvalue5,name=my.test.metric.name,tagname7=tagvalue7" + testFlatID = []byte("tagname1=tagvalue1,tagname3=tagvalue3," + + "tagname4=tagvalue4,tagname2=tagvalue2,tagname6=tagvalue6," + + "tagname5=tagvalue5,name=my.test.metric.name,tagname7=tagvalue7") + testTagsFilterMapOne = map[string]string{ "tagname1": "tagvalue1", } @@ -41,20 +44,22 @@ var ( ) func BenchmarkEquityFilter(b *testing.B) { - f1 := newEqualityFilter("test") - f2 := newEqualityFilter("test2") + f1 := newEqualityFilter([]byte("test")) + f2 := newEqualityFilter([]byte("test2")) + val := []byte("test") for n := 0; n < b.N; n++ { - testUnionFilter("test", []Filter{f1, f2}) + testUnionFilter(val, []Filter{f1, f2}) } } func BenchmarkEquityFilterByValue(b *testing.B) { - f1 := newTestEqualityFilter("test") - f2 := newTestEqualityFilter("test2") + f1 := newTestEqualityFilter([]byte("test")) + f2 := newTestEqualityFilter([]byte("test2")) + val := []byte("test") for n := 0; n < b.N; n++ { - testUnionFilter("test", []Filter{f1, f2}) + testUnionFilter(val, []Filter{f1, f2}) } } @@ -77,70 +82,70 @@ func BenchmarkMapTagsFilterThree(b *testing.B) { } func BenchmarkRangeFilterStructsMatchRange(b *testing.B) { - benchRangeFilterStructs(b, "a-z", "p", true) + benchRangeFilterStructs(b, []byte("a-z"), []byte("p"), true) } func BenchmarkRangeFilterRangeMatchRange(b *testing.B) { - benchRangeFilterRange(b, "a-z", "p", true) + benchRangeFilterRange(b, []byte("a-z"), []byte("p"), true) } func BenchmarkRangeFilterStructsNotMatchRange(b *testing.B) { - benchRangeFilterStructs(b, "a-z", "P", false) + benchRangeFilterStructs(b, []byte("a-z"), []byte("P"), false) } func BenchmarkRangeFilterRangeNotMatchRange(b *testing.B) { - benchRangeFilterRange(b, "a-z", "P", false) + benchRangeFilterRange(b, []byte("a-z"), []byte("P"), false) } func BenchmarkRangeFilterStructsMatch(b *testing.B) { - benchRangeFilterStructs(b, "02468", "6", true) + benchRangeFilterStructs(b, []byte("02468"), []byte("6"), true) } func BenchmarkRangeFilterRangeMatch(b *testing.B) { - benchRangeFilterRange(b, "02468", "6", true) + benchRangeFilterRange(b, []byte("02468"), []byte("6"), true) } func BenchmarkRangeFilterStructsNotMatch(b *testing.B) { - benchRangeFilterStructs(b, "13579", "6", false) + benchRangeFilterStructs(b, []byte("13579"), []byte("6"), false) } func BenchmarkRangeFilterRangeNotMatch(b *testing.B) { - benchRangeFilterRange(b, "13579", "6", false) + benchRangeFilterRange(b, []byte("13579"), []byte("6"), false) } func BenchmarkRangeFilterStructsMatchNegation(b *testing.B) { - benchRangeFilterStructs(b, "!a-z", "p", false) + benchRangeFilterStructs(b, []byte("!a-z"), []byte("p"), false) } func BenchmarkRangeFilterRangeMatchNegation(b *testing.B) { - benchRangeFilterRange(b, "!a-z", "p", false) + benchRangeFilterRange(b, []byte("!a-z"), []byte("p"), false) } func BenchmarkMultiRangeFilterTwo(b *testing.B) { - benchMultiRangeFilter(b, "test_1,test_2", false, []string{"test_1", "fake_1"}) + benchMultiRangeFilter(b, []byte("test_1,test_2"), false, [][]byte{[]byte("test_1"), []byte("fake_1")}) } func BenchmarkMultiRangeFilterSelectTwo(b *testing.B) { - benchMultiRangeFilterSelect(b, "test_1,test_2", false, []string{"test_1", "fake_1"}) + benchMultiRangeFilterSelect(b, []byte("test_1,test_2"), false, [][]byte{[]byte("test_1"), []byte("fake_1")}) } func BenchmarkMultiRangeFilterTrieTwo(b *testing.B) { - benchMultiRangeFilterTrie(b, "test_1,test_2", false, []string{"test_1", "fake_1"}) + benchMultiRangeFilterTrie(b, []byte("test_1,test_2"), false, [][]byte{[]byte("test_1"), []byte("fake_1")}) } func BenchmarkMultiRangeFilterSix(b *testing.B) { - benchMultiRangeFilter(b, "test_1,test_2,staging_1,staging_2,prod_1,prod_2", false, []string{"prod_1", "staging_3"}) + benchMultiRangeFilter(b, []byte("test_1,test_2,staging_1,staging_2,prod_1,prod_2"), false, [][]byte{[]byte("prod_1"), []byte("staging_3")}) } func BenchmarkMultiRangeFilterSelectSix(b *testing.B) { - benchMultiRangeFilterSelect(b, "test_1,test_2,staging_1,staging_2,prod_1,prod_2", false, []string{"prod_1", "staging_3"}) + benchMultiRangeFilterSelect(b, []byte("test_1,test_2,staging_1,staging_2,prod_1,prod_2"), false, [][]byte{[]byte("prod_1"), []byte("staging_3")}) } func BenchmarkMultiRangeFilterTrieSix(b *testing.B) { - benchMultiRangeFilterTrie(b, "test_1,test_2,staging_1,staging_2,prod_1,prod_2", false, []string{"prod_1", "staging_3"}) + benchMultiRangeFilterTrie(b, []byte("test_1,test_2,staging_1,staging_2,prod_1,prod_2"), false, [][]byte{[]byte("prod_1"), []byte("staging_3")}) } -func benchMultiRangeFilter(b *testing.B, patterns string, backwards bool, vals []string) { +func benchMultiRangeFilter(b *testing.B, patterns []byte, backwards bool, vals [][]byte) { f, _ := newMultiCharSequenceFilter(patterns, backwards) for n := 0; n < b.N; n++ { for _, val := range vals { @@ -149,7 +154,7 @@ func benchMultiRangeFilter(b *testing.B, patterns string, backwards bool, vals [ } } -func benchMultiRangeFilterSelect(b *testing.B, patterns string, backwards bool, vals []string) { +func benchMultiRangeFilterSelect(b *testing.B, patterns []byte, backwards bool, vals [][]byte) { f, _ := newTestMultiCharRangeSelectFilter(patterns, backwards) for n := 0; n < b.N; n++ { for _, val := range vals { @@ -158,7 +163,7 @@ func benchMultiRangeFilterSelect(b *testing.B, patterns string, backwards bool, } } -func benchMultiRangeFilterTrie(b *testing.B, patterns string, backwards bool, vals []string) { +func benchMultiRangeFilterTrie(b *testing.B, patterns []byte, backwards bool, vals [][]byte) { f, _ := newTestMultiCharRangeTrieFilter(patterns, backwards) for n := 0; n < b.N; n++ { for _, val := range vals { @@ -167,7 +172,7 @@ func benchMultiRangeFilterTrie(b *testing.B, patterns string, backwards bool, va } } -func benchRangeFilterStructs(b *testing.B, pattern, val string, expectedMatch bool) { +func benchRangeFilterStructs(b *testing.B, pattern, val []byte, expectedMatch bool) { f, _ := newSingleRangeFilter(pattern, false) for n := 0; n < b.N; n++ { _, match := f.matches(val) @@ -177,7 +182,7 @@ func benchRangeFilterStructs(b *testing.B, pattern, val string, expectedMatch bo } } -func benchRangeFilterRange(b *testing.B, pattern string, val string, expectedMatch bool) { +func benchRangeFilterRange(b *testing.B, pattern, val []byte, expectedMatch bool) { for n := 0; n < b.N; n++ { match, _ := validateRangeByScan(pattern, val) if match != expectedMatch { @@ -186,13 +191,13 @@ func benchRangeFilterRange(b *testing.B, pattern string, val string, expectedMat } } -func benchTagsFilter(b *testing.B, id string, tagsFilter Filter) { +func benchTagsFilter(b *testing.B, id []byte, tagsFilter Filter) { for n := 0; n < b.N; n++ { tagsFilter.Matches(id) } } -func testUnionFilter(val string, filters []Filter) bool { +func testUnionFilter(val []byte, filters []Filter) bool { for _, filter := range filters { if !filter.Matches(val) { return false @@ -203,10 +208,10 @@ func testUnionFilter(val string, filters []Filter) bool { } type testEqualityFilter struct { - pattern string + pattern []byte } -func newTestEqualityFilter(pattern string) Filter { +func newTestEqualityFilter(pattern []byte) Filter { return testEqualityFilter{pattern: pattern} } @@ -214,8 +219,8 @@ func (f testEqualityFilter) String() string { return fmt.Sprintf("Equals(%q)", f.pattern) } -func (f testEqualityFilter) Matches(id string) bool { - return f.pattern == id +func (f testEqualityFilter) Matches(id []byte) bool { + return bytes.Equal(f.pattern, id) } type testMapTagsFilter struct { @@ -226,7 +231,7 @@ type testMapTagsFilter struct { func newTestMapTagsFilter(tagFilters map[string]string, iterFn NewSortedTagIteratorFn) Filter { filters := make(map[string]Filter, len(tagFilters)) for name, value := range tagFilters { - filter, _ := NewFilter(value) + filter, _ := NewFilter([]byte(value)) filters[name] = filter } @@ -240,7 +245,7 @@ func (f *testMapTagsFilter) String() string { return "" } -func (f *testMapTagsFilter) Matches(id string) bool { +func (f *testMapTagsFilter) Matches(id []byte) bool { if len(f.filters) == 0 { return true } @@ -252,13 +257,17 @@ func (f *testMapTagsFilter) Matches(id string) bool { for iter.Next() { name, value := iter.Current() - if filter, exists := f.filters[name]; exists { + for n, filter := range f.filters { + if !bytes.Equal([]byte(n), name) { + continue + } + if filter.Matches(value) { matches++ if matches == len(f.filters) { return true } - continue + break } return false @@ -268,12 +277,12 @@ func (f *testMapTagsFilter) Matches(id string) bool { return iter.Err() == nil && matches == len(f.filters) } -func newTestMultiCharRangeSelectFilter(pattern string, backwards bool) (chainFilter, error) { +func newTestMultiCharRangeSelectFilter(pattern []byte, backwards bool) (chainFilter, error) { if len(pattern) == 0 { return nil, errInvalidFilterPattern } - patterns := strings.Split(pattern, multiRangeSplit) + patterns := bytes.Split(pattern, multiRangeSplit) filters := make([]chainFilter, len(patterns)) for i, p := range patterns { f, _ := newMultiCharSequenceFilter(p, backwards) @@ -290,13 +299,13 @@ type testMultiCharRangeTrieFilter struct { backwards bool } -func newTestMultiCharRangeTrieFilter(patterns string, backwards bool) (chainFilter, error) { +func newTestMultiCharRangeTrieFilter(patterns []byte, backwards bool) (chainFilter, error) { if len(patterns) == 0 { return nil, errInvalidFilterPattern } b := &byteTrie{} - for _, p := range strings.Split(patterns, multiRangeSplit) { + for _, p := range bytes.Split(patterns, multiRangeSplit) { b.insert(p, backwards) } @@ -309,18 +318,20 @@ func newTestMultiCharRangeTrieFilter(patterns string, backwards bool) (chainFilt func (f *testMultiCharRangeTrieFilter) String() string { results := &traverseResults{} f.b.listTraverse(nil, results, f.backwards) - return "Range(\"" + strings.Join(results.vals, multiRangeSplit) + "\")" + return "Range(\"" + string(bytes.Join(results.vals, multiRangeSplit)) + "\")" } -func (f *testMultiCharRangeTrieFilter) matches(val string) (string, bool) { +func (f *testMultiCharRangeTrieFilter) matches(val []byte) ([]byte, bool) { return f.b.lookup(val, f.backwards) } -func validateRangeByScan(pattern string, val string) (bool, error) { +func validateRangeByScan(pattern, val []byte) (bool, error) { if len(pattern) == 0 { return false, errInvalidFilterPattern } + // TODO(r): utf8 decode to ensure slicing is valid + negate := false if pattern[0] == negationChar { pattern = pattern[1:] @@ -367,7 +378,7 @@ func (f *testSelectChainFilter) String() string { return "" } -func (f *testSelectChainFilter) matches(val string) (string, bool) { +func (f *testSelectChainFilter) matches(val []byte) ([]byte, bool) { if len(f.filters) == 0 { return val, true } @@ -379,7 +390,7 @@ func (f *testSelectChainFilter) matches(val string) (string, bool) { } } - return "", false + return nil, false } // byteTrie is a trie for bytes that provides multi-direction inserts and lookups with @@ -391,7 +402,7 @@ type byteTrie struct { children []*byteTrie } -func (t *byteTrie) insert(val string, backwards bool) { +func (t *byteTrie) insert(val []byte, backwards bool) { if len(val) == 0 { return } @@ -424,9 +435,9 @@ func (t *byteTrie) insert(val string, backwards bool) { child.insert(remainder, backwards) } -func (t *byteTrie) lookup(val string, backwards bool) (string, bool) { +func (t *byteTrie) lookup(val []byte, backwards bool) ([]byte, bool) { if len(val) == 0 { - return "", false + return nil, false } idx := 0 @@ -446,11 +457,11 @@ func (t *byteTrie) lookup(val string, backwards bool) (string, bool) { } } - return "", false + return nil, false } type traverseResults struct { - vals []string + vals [][]byte } func (t *byteTrie) listTraverse(val []byte, results *traverseResults, backwards bool) { @@ -465,7 +476,7 @@ func (t *byteTrie) listTraverse(val []byte, results *traverseResults, backwards } if c.leaf { - results.vals = append(results.vals, string(newVal)) + results.vals = append(results.vals, newVal) } c.listTraverse(newVal, results, backwards) diff --git a/filters/filter_test.go b/filters/filter_test.go index 4d780e0..2bd8cf7 100644 --- a/filters/filter_test.go +++ b/filters/filter_test.go @@ -57,18 +57,18 @@ func TestEqualityFilter(t *testing.T) { {val: "fo", match: false}, {val: "foob", match: false}, } - f := newEqualityFilter("foo") + f := newEqualityFilter([]byte("foo")) for _, input := range inputs { - require.Equal(t, input.match, f.Matches(input.val)) + require.Equal(t, input.match, f.Matches([]byte(input.val))) } } func TestEmptyFilter(t *testing.T) { - f, err := NewFilter("") + f, err := NewFilter(nil) require.NoError(t, err) - require.True(t, f.Matches("")) - require.False(t, f.Matches(" ")) - require.False(t, f.Matches("foo")) + require.True(t, f.Matches([]byte(""))) + require.False(t, f.Matches([]byte(" "))) + require.False(t, f.Matches([]byte("foo"))) } func TestWildcardFilters(t *testing.T) { @@ -138,14 +138,14 @@ func TestRangeFilters(t *testing.T) { } func TestMultiFilter(t *testing.T) { - cf, _ := newContainsFilter("bar") + cf, _ := newContainsFilter([]byte("bar")) filters := []Filter{ NewMultiFilter([]Filter{}, Conjunction), NewMultiFilter([]Filter{}, Disjunction), - NewMultiFilter([]Filter{newEqualityFilter("foo")}, Conjunction), - NewMultiFilter([]Filter{newEqualityFilter("foo")}, Disjunction), - NewMultiFilter([]Filter{newEqualityFilter("foo"), cf}, Conjunction), - NewMultiFilter([]Filter{newEqualityFilter("foo"), cf}, Disjunction), + NewMultiFilter([]Filter{newEqualityFilter([]byte("foo"))}, Conjunction), + NewMultiFilter([]Filter{newEqualityFilter([]byte("foo"))}, Disjunction), + NewMultiFilter([]Filter{newEqualityFilter([]byte("foo")), cf}, Conjunction), + NewMultiFilter([]Filter{newEqualityFilter([]byte("foo")), cf}, Disjunction), } inputs := []testInput{ @@ -217,16 +217,16 @@ func TestBadPatterns(t *testing.T) { } for _, pattern := range patterns { - _, err := NewFilter(pattern) + _, err := NewFilter([]byte(pattern)) require.Error(t, err, fmt.Sprintf("pattern: %s", pattern)) } } func TestMultiCharSequenceFilter(t *testing.T) { - f, err := newMultiCharSequenceFilter("", false) + f, err := newMultiCharSequenceFilter([]byte(""), false) require.Error(t, err) - f, err = newMultiCharSequenceFilter("test2,test,tent,book", false) + f, err = newMultiCharSequenceFilter([]byte("test2,test,tent,book"), false) validateLookup(t, f, "", false, "") validateLookup(t, f, "t", false, "") validateLookup(t, f, "tes", false, "") @@ -238,7 +238,7 @@ func TestMultiCharSequenceFilter(t *testing.T) { validateLookup(t, f, "test2", true, "") validateLookup(t, f, "book123", true, "123") - f, err = newMultiCharSequenceFilter("test2,test,tent,book", true) + f, err = newMultiCharSequenceFilter([]byte("test2,test,tent,book"), true) validateLookup(t, f, "", false, "") validateLookup(t, f, "t", false, "") validateLookup(t, f, "tes", false, "") @@ -253,9 +253,9 @@ func TestMultiCharSequenceFilter(t *testing.T) { } func validateLookup(t *testing.T, f chainFilter, val string, expectedMatch bool, expectedRemainder string) { - remainder, match := f.matches(val) + remainder, match := f.matches([]byte(val)) require.Equal(t, expectedMatch, match) - require.Equal(t, expectedRemainder, remainder) + require.Equal(t, expectedRemainder, string(remainder)) } type testPattern struct { @@ -264,19 +264,19 @@ type testPattern struct { } type testInput struct { - val string + val []byte matches []bool } func newTestInput(val string, matches ...bool) testInput { - return testInput{val: val, matches: matches} + return testInput{val: []byte(val), matches: matches} } func genAndValidateFilters(t *testing.T, patterns []testPattern) []Filter { var err error filters := make([]Filter, len(patterns)) for i, pattern := range patterns { - filters[i], err = NewFilter(pattern.pattern) + filters[i], err = NewFilter([]byte(pattern.pattern)) require.NoError(t, err, fmt.Sprintf("No error expected, but got: %v for pattern: %s", err, pattern.pattern)) require.Equal(t, pattern.expectedStr, filters[i].String()) } diff --git a/filters/mock_filter.go b/filters/mock_filter.go index 3253064..d7a46ce 100644 --- a/filters/mock_filter.go +++ b/filters/mock_filter.go @@ -35,8 +35,8 @@ type mockFilterData struct { } type mockTagPair struct { - name string - value string + name []byte + value []byte } type mockSortedTagIterator struct { @@ -45,18 +45,18 @@ type mockSortedTagIterator struct { pairs []mockTagPair } -func idToMockTagPairs(id string) []mockTagPair { - tagPairs := strings.Split(id, mockTagPairSeparator) +func idToMockTagPairs(id []byte) []mockTagPair { + tagPairs := strings.Split(string(id), mockTagPairSeparator) var pairs []mockTagPair for _, pair := range tagPairs { p := strings.Split(pair, mockTagValueSeparator) - pairs = append(pairs, mockTagPair{name: p[0], value: p[1]}) + pairs = append(pairs, mockTagPair{name: []byte(p[0]), value: []byte(p[1])}) } return pairs } // NewMockSortedTagIterator creates a mock SortedTagIterator based on given ID -func NewMockSortedTagIterator(id string) SortedTagIterator { +func NewMockSortedTagIterator(id []byte) SortedTagIterator { pairs := idToMockTagPairs(id) return &mockSortedTagIterator{idx: -1, pairs: pairs} } @@ -69,7 +69,7 @@ func (it *mockSortedTagIterator) Next() bool { return it.err == nil && it.idx < len(it.pairs) } -func (it *mockSortedTagIterator) Current() (string, string) { +func (it *mockSortedTagIterator) Current() ([]byte, []byte) { return it.pairs[it.idx].name, it.pairs[it.idx].value } diff --git a/filters/tags_filter.go b/filters/tags_filter.go index 8e46d55..e7c8e29 100644 --- a/filters/tags_filter.go +++ b/filters/tags_filter.go @@ -33,7 +33,7 @@ type SortedTagIterator interface { Next() bool // Current returns the current tag name and value - Current() (string, string) + Current() ([]byte, []byte) // Err returns any errors encountered Err() error @@ -43,23 +43,23 @@ type SortedTagIterator interface { } // NewSortedTagIteratorFn creates a tag iterator given an id -type NewSortedTagIteratorFn func(id string) SortedTagIterator +type NewSortedTagIteratorFn func(id []byte) SortedTagIterator // tagFilter is a filter associated with a given tag type tagFilter struct { - name string + name []byte valueFilter Filter } func (f *tagFilter) String() string { - return fmt.Sprintf("%s:%s", f.name, f.valueFilter.String()) + return fmt.Sprintf("%s:%s", string(f.name), f.valueFilter.String()) } type tagFiltersByNameAsc []tagFilter func (tn tagFiltersByNameAsc) Len() int { return len(tn) } func (tn tagFiltersByNameAsc) Swap(i, j int) { tn[i], tn[j] = tn[j], tn[i] } -func (tn tagFiltersByNameAsc) Less(i, j int) bool { return tn[i].name < tn[j].name } +func (tn tagFiltersByNameAsc) Less(i, j int) bool { return bytes.Compare(tn[i].name, tn[j].name) < 0 } // tagsFilter contains a list of tag filters. type tagsFilter struct { @@ -72,13 +72,13 @@ type tagsFilter struct { func NewTagsFilter(tagFilters map[string]string, iterFn NewSortedTagIteratorFn, op LogicalOp) (Filter, error) { filters := make([]tagFilter, 0, len(tagFilters)) for name, value := range tagFilters { - valFilter, err := NewFilter(value) + valFilter, err := NewFilter([]byte(value)) if err != nil { return nil, err } filters = append(filters, tagFilter{ - name: name, + name: []byte(name), valueFilter: valFilter, }) } @@ -103,7 +103,7 @@ func (f *tagsFilter) String() string { return buf.String() } -func (f *tagsFilter) Matches(id string) bool { +func (f *tagsFilter) Matches(id []byte) bool { if len(f.filters) == 0 { return true } @@ -113,11 +113,13 @@ func (f *tagsFilter) Matches(id string) bool { currIdx := 0 for iter.Next() && currIdx < len(f.filters) { name, value := iter.Current() - if name < f.filters[currIdx].name { + + comparison := bytes.Compare(name, f.filters[currIdx].name) + if comparison < 0 { continue } - if name > f.filters[currIdx].name { + if comparison > 0 { if f.op == Conjunction { // For AND, if the current filter tag doesn't exist, bail immediately return false @@ -125,7 +127,7 @@ func (f *tagsFilter) Matches(id string) bool { // Iterate filters for the OR case currIdx++ - for currIdx < len(f.filters) && name > f.filters[currIdx].name { + for currIdx < len(f.filters) && bytes.Compare(name, f.filters[currIdx].name) > 0 { currIdx++ } @@ -134,7 +136,7 @@ func (f *tagsFilter) Matches(id string) bool { return false } - if name < f.filters[currIdx].name { + if bytes.Compare(name, f.filters[currIdx].name) < 0 { continue } } diff --git a/filters/tags_filter_test.go b/filters/tags_filter_test.go index 6e3d1b7..6bd6875 100644 --- a/filters/tags_filter_test.go +++ b/filters/tags_filter_test.go @@ -29,7 +29,7 @@ import ( func TestEmptyTagsFilterMatches(t *testing.T) { f, err := NewTagsFilter(nil, NewMockSortedTagIterator, Conjunction) require.NoError(t, err) - require.True(t, f.Matches("foo")) + require.True(t, f.Matches([]byte("foo"))) } func TestTagsFilterMatches(t *testing.T) { @@ -47,7 +47,7 @@ func TestTagsFilterMatches(t *testing.T) { } require.NoError(t, err) for _, input := range inputs { - require.Equal(t, input.match, f.Matches(input.val)) + require.Equal(t, input.match, f.Matches([]byte(input.val))) } f, err = NewTagsFilter(filters, NewMockSortedTagIterator, Disjunction) @@ -64,7 +64,7 @@ func TestTagsFilterMatches(t *testing.T) { } require.NoError(t, err) for _, input := range inputs { - require.Equal(t, input.match, f.Matches(input.val), "val:", input.val) + require.Equal(t, input.match, f.Matches([]byte(input.val)), "val:", input.val) } } diff --git a/rules/options.go b/rules/options.go index 513670f..3db109f 100644 --- a/rules/options.go +++ b/rules/options.go @@ -23,7 +23,7 @@ package rules import "github.com/m3db/m3metrics/filters" // NewIDFn creates a new metric ID based on the metric name and metric tag pairs -type NewIDFn func(name string, tags []TagPair) string +type NewIDFn func(name []byte, tags []TagPair) []byte // Options provide a set of options for rule matching type Options interface { diff --git a/rules/rule.go b/rules/rule.go index ae5b282..eb6bba4 100644 --- a/rules/rule.go +++ b/rules/rule.go @@ -21,6 +21,7 @@ package rules import ( + "bytes" "sort" "time" @@ -39,8 +40,8 @@ var ( // target will be grouped and rolled up across the provided set of tags, named // with the provided name, and aggregated and retained under the provided policies type RollupTarget struct { - Name string // name of the rollup metric - Tags []string // a set of sorted tags rollups are performed on + Name []byte // name of the rollup metric + Tags [][]byte // a set of sorted tags rollups are performed on Policies []policy.Policy // defines how the rollup metric is aggregated and retained } @@ -55,22 +56,22 @@ func newRollupTarget(target *schema.RollupTarget) (RollupTarget, error) { sort.Strings(tags) return RollupTarget{ - Name: target.Name, - Tags: tags, + Name: []byte(target.Name), + Tags: bytesArrayFromStringArray(tags), Policies: policies, }, nil } // sameTransform determines whether two targets have the same transformation func (t RollupTarget) sameTransform(other RollupTarget) bool { - if t.Name != other.Name { + if !bytes.Equal(t.Name, other.Name) { return false } if len(t.Tags) != len(other.Tags) { return false } for i := 0; i < len(t.Tags); i++ { - if t.Tags[i] != other.Tags[i] { + if !bytes.Equal(t.Tags[i], other.Tags[i]) { return false } } @@ -79,26 +80,41 @@ func (t RollupTarget) sameTransform(other RollupTarget) bool { // clone clones a rollup target func (t RollupTarget) clone() RollupTarget { - Tags := make([]string, len(t.Tags)) - copy(Tags, t.Tags) policies := make([]policy.Policy, len(t.Policies)) copy(policies, t.Policies) return RollupTarget{ Name: t.Name, - Tags: Tags, + Tags: bytesArrayCopy(t.Tags), Policies: policies, } } +func bytesArrayFromStringArray(values []string) [][]byte { + result := make([][]byte, len(values)) + for i, str := range values { + result[i] = []byte(str) + } + return result +} + +func bytesArrayCopy(values [][]byte) [][]byte { + result := make([][]byte, len(values)) + for i, b := range values { + result[i] = make([]byte, len(b)) + copy(result[i], b) + } + return result +} + // TagPair contains a tag name and a tag value type TagPair struct { - Name string - Value string + Name []byte + Value []byte } // RollupResult contains the rollup metric id and the associated policies type RollupResult struct { - ID string + ID []byte Policies []policy.Policy } @@ -135,7 +151,7 @@ func (r *MatchResult) Mappings() policy.VersionedPolicies { } // Rollups returns the rollup metric id and corresponding policies at a given index -func (r *MatchResult) Rollups(idx int) (string, policy.VersionedPolicies) { +func (r *MatchResult) Rollups(idx int) ([]byte, policy.VersionedPolicies) { rollup := r.rollups[idx] return rollup.ID, r.versionedPolicies(rollup.Policies) } @@ -165,7 +181,7 @@ type RuleSet interface { // Match matches the set of rules against a metric id, returning // the applicable mapping policies and rollup policies - Match(id string) MatchResult + Match(id []byte) MatchResult } // mappingRule defines a rule such that if a metric matches the provided filters, @@ -273,7 +289,7 @@ func (rs *ruleSet) Version() int { return rs.version } func (rs *ruleSet) Cutover() time.Time { return rs.cutover } func (rs *ruleSet) TombStoned() bool { return rs.tombStoned } -func (rs *ruleSet) Match(id string) MatchResult { +func (rs *ruleSet) Match(id []byte) MatchResult { var ( mappings []policy.Policy rollups []RollupResult @@ -285,7 +301,7 @@ func (rs *ruleSet) Match(id string) MatchResult { return NewMatchResult(rs.version, rs.cutover, mappings, rollups) } -func (rs *ruleSet) mappingPolicies(id string) []policy.Policy { +func (rs *ruleSet) mappingPolicies(id []byte) []policy.Policy { // TODO(xichen): pool the policies var policies []policy.Policy for _, rule := range rs.mappingRules { @@ -296,7 +312,7 @@ func (rs *ruleSet) mappingPolicies(id string) []policy.Policy { return resolvePolicies(policies) } -func (rs *ruleSet) rollupResults(id string) []RollupResult { +func (rs *ruleSet) rollupResults(id []byte) []RollupResult { // TODO(xichen): pool the rollup targets var rollups []RollupTarget for _, rule := range rs.rollupRules { @@ -329,53 +345,30 @@ func (rs *ruleSet) rollupResults(id string) []RollupResult { return rs.toRollupResults(id, rollups) } -type matchedValue struct { - value string - matched bool -} - -func (rs *ruleSet) toRollupResults(id string, targets []RollupTarget) []RollupResult { - // Put all the tags in one map so we only need to scan the id once to get all tag values - numTags := 0 - for _, t := range targets { - numTags += len(t.Tags) - } - allTags := make(map[string]matchedValue, numTags) - for _, target := range targets { - for _, tag := range target.Tags { - _, exists := allTags[tag] - if !exists { - allTags[tag] = matchedValue{} - } - } - } - - // Find all the tag values associated with the tags in the rollup targets - iter := rs.iterFn(id) - defer iter.Close() - for iter.Next() { - name, value := iter.Current() - matched, exists := allTags[name] - if !exists { - continue - } - matched.value = value - matched.matched = true - allTags[name] = matched - } +// toRollupResults encodes rollup target name and values into ids for each rollup target +func (rs *ruleSet) toRollupResults(id []byte, targets []RollupTarget) []RollupResult { + // NB(r): This is n^2 however this should be quite fast still as + // long as there is not an absurdly high number of rollup + // targets for any given ID and that iterfn is alloc free. + // + // Even with a very high number of rules its still predicted that + // any given ID would match a relatively low number of rollups. - // Encode rollup target name and values into ids for each rollup target // TODO(xichen): pool tag pairs and rollup results var tagPairs []TagPair rollups := make([]RollupResult, 0, len(targets)) for _, target := range targets { tagPairs = tagPairs[:0] for _, tag := range target.Tags { - value := allTags[tag] - if !value.matched { - continue + iter := rs.iterFn(id) + for iter.Next() { + name, value := iter.Current() + if bytes.Equal(name, tag) { + tagPairs = append(tagPairs, TagPair{Name: tag, Value: value}) + break + } } - tagPairs = append(tagPairs, TagPair{Name: tag, Value: value.value}) + iter.Close() } result := RollupResult{ ID: rs.newIDFn(target.Name, tagPairs), diff --git a/rules/rule_test.go b/rules/rule_test.go index db809de..f842f87 100644 --- a/rules/rule_test.go +++ b/rules/rule_test.go @@ -22,7 +22,6 @@ package rules import ( "bytes" - "fmt" "testing" "time" @@ -34,22 +33,34 @@ import ( "github.com/stretchr/testify/require" ) +func b(v string) []byte { + return []byte(v) +} + +func bs(v ...string) [][]byte { + result := make([][]byte, len(v)) + for i, str := range v { + result[i] = []byte(str) + } + return result +} + func TestRollupTargetSameTransform(t *testing.T) { policies := []policy.Policy{ policy.NewPolicy(10*time.Second, xtime.Second, 2*24*time.Hour), } - target := RollupTarget{Name: "foo", Tags: []string{"bar1", "bar2"}} + target := RollupTarget{Name: b("foo"), Tags: bs("bar1", "bar2")} inputs := []testRollupTargetData{ { - target: RollupTarget{Name: "foo", Tags: []string{"bar1", "bar2"}, Policies: policies}, + target: RollupTarget{Name: b("foo"), Tags: bs("bar1", "bar2"), Policies: policies}, result: true, }, { - target: RollupTarget{Name: "baz", Tags: []string{"bar1", "bar2"}}, + target: RollupTarget{Name: b("baz"), Tags: bs("bar1", "bar2")}, result: false, }, { - target: RollupTarget{Name: "foo", Tags: []string{"bar1", "bar3"}}, + target: RollupTarget{Name: b("foo"), Tags: bs("bar1", "bar3")}, result: false, }, } @@ -62,16 +73,16 @@ func TestRollupTargetClone(t *testing.T) { policies := []policy.Policy{ policy.NewPolicy(10*time.Second, xtime.Second, 2*24*time.Hour), } - target := RollupTarget{Name: "foo", Tags: []string{"bar1", "bar2"}, Policies: policies} + target := RollupTarget{Name: b("foo"), Tags: bs("bar1", "bar2"), Policies: policies} cloned := target.clone() // Cloned object should look exactly the same as the original one require.Equal(t, target, cloned) // Change references in the cloned object should not mutate the original object - cloned.Tags[0] = "bar3" + cloned.Tags[0] = b("bar3") cloned.Policies[0] = policy.EmptyPolicy - require.Equal(t, target.Tags, []string{"bar1", "bar2"}) + require.Equal(t, target.Tags, bs("bar1", "bar2")) require.Equal(t, target.Policies, policies) } @@ -101,7 +112,7 @@ func TestRuleSetMatchMappingRules(t *testing.T) { }, } for _, input := range inputs { - res := ruleSet.Match(input.id) + res := ruleSet.Match(b(input.id)) require.Equal(t, ruleSet.Version(), res.Version()) require.Equal(t, ruleSet.Cutover(), res.Cutover()) require.Equal(t, input.result, res.Mappings().Policies()) @@ -120,7 +131,7 @@ func TestRuleSetMatchRollupRules(t *testing.T) { id: "rtagName1=rtagValue1,rtagName2=rtagValue2,rtagName3=rtagValue3", result: []RollupResult{ { - ID: "rName1|rtagName1=rtagValue1,rtagName2=rtagValue2", + ID: b("rName1|rtagName1=rtagValue1,rtagName2=rtagValue2"), Policies: []policy.Policy{ policy.NewPolicy(10*time.Second, xtime.Second, 12*time.Hour), policy.NewPolicy(time.Minute, xtime.Minute, 24*time.Hour), @@ -128,7 +139,7 @@ func TestRuleSetMatchRollupRules(t *testing.T) { }, }, { - ID: "rName2|rtagName1=rtagValue1", + ID: b("rName2|rtagName1=rtagValue1"), Policies: []policy.Policy{ policy.NewPolicy(10*time.Second, xtime.Second, 24*time.Hour), }, @@ -139,7 +150,7 @@ func TestRuleSetMatchRollupRules(t *testing.T) { id: "rtagName1=rtagValue2", result: []RollupResult{ { - ID: "rName3|rtagName1=rtagValue2", + ID: b("rName3|rtagName1=rtagValue2"), Policies: []policy.Policy{ policy.NewPolicy(time.Minute, xtime.Minute, time.Hour), }, @@ -152,7 +163,7 @@ func TestRuleSetMatchRollupRules(t *testing.T) { }, } for _, input := range inputs { - res := ruleSet.Match(input.id) + res := ruleSet.Match(b(input.id)) require.Equal(t, ruleSet.Version(), res.Version()) require.Equal(t, ruleSet.Cutover(), res.Cutover()) require.Equal(t, len(input.result), res.NumRollups()) @@ -177,7 +188,7 @@ func TestTombstonedRuleSetMatch(t *testing.T) { expected := NewMatchResult(ruleSet.Version(), ruleSet.Cutover(), nil, nil) id := "rtagName1=rtagValue1" - require.Equal(t, expected, ruleSet.Match(id)) + require.Equal(t, expected, ruleSet.Match(b(id))) } type testRollupTargetData struct { @@ -201,24 +212,24 @@ func testRuleSetOptions() Options { SetNewIDFn(mockNewID) } -func mockNewID(name string, tags []TagPair) string { +func mockNewID(name []byte, tags []TagPair) []byte { if len(tags) == 0 { return name } var buf bytes.Buffer - buf.WriteString(fmt.Sprintf("%s", name)) + buf.Write(name) if len(tags) > 0 { buf.WriteString("|") for idx, p := range tags { - buf.WriteString(p.Name) + buf.Write(p.Name) buf.WriteString("=") - buf.WriteString(p.Value) + buf.Write(p.Value) if idx < len(tags)-1 { buf.WriteString(",") } } } - return buf.String() + return buf.Bytes() } func testMappingRulesConfig() []*schema.MappingRule {