Skip to content

Commit 14cf858

Browse files
authored
feat: NR-469064 Implement exclude_matching_entities filter in nri-winservice (#221)
* feat: NR-469064 Implement exclude_matching_entities filter in nri-winservice * added an example in sample config yml file * added integration test * changed vaiable name and method name test
1 parent f10a974 commit 14cf858

File tree

6 files changed

+266
-19
lines changed

6 files changed

+266
-19
lines changed

src/matcher/matcher.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,69 @@ import (
1313

1414
// Matcher groups the rules to validate the service name
1515
type Matcher struct {
16-
patterns []pattern
16+
includePatterns []pattern
17+
excludePatterns []pattern
1718
}
1819
type pattern struct {
1920
regex *regexp.Regexp
2021
}
2122

22-
// Match returns true if the string matches one of the patterns
23+
// Match returns true if the string matches include patterns and doesn't match exclude patterns
24+
// Include patterns are required - this matcher does not support exclude-only filtering
2325
func (m *Matcher) Match(s string) bool {
24-
for _, p := range m.patterns {
26+
// Must match at least one include pattern first
27+
includeMatch := false
28+
for _, p := range m.includePatterns {
2529
if p.match(s) {
26-
return true
30+
includeMatch = true
31+
break
2732
}
2833
}
29-
return false
30-
}
3134

32-
// IsEmpty returns true if the Matcher has no patterns
33-
func (m *Matcher) IsEmpty() bool {
34-
if len(m.patterns) != 0 {
35+
// If no include patterns match, return false
36+
if !includeMatch {
3537
return false
3638
}
39+
40+
// Check if it matches any exclude patterns (exclude takes precedence)
41+
for _, p := range m.excludePatterns {
42+
if p.match(s) {
43+
return false
44+
}
45+
}
46+
3747
return true
3848
}
3949

50+
// IsEmpty returns true if the Matcher has no include patterns
51+
// (exclude patterns alone are not sufficient for a valid matcher)
52+
func (m *Matcher) IsEmpty() bool {
53+
return len(m.includePatterns) == 0
54+
}
55+
4056
func (p pattern) match(s string) bool {
4157
return p.regex.MatchString(s)
4258
}
4359

44-
// New create a new Matcher instance from slices of filters
60+
// New create a new Matcher instance from slices of include and exclude filters
61+
func New(includeFilters []string) Matcher {
62+
return NewWithIncludesExcludes(includeFilters, nil)
63+
}
64+
65+
// NewWithExcludes creates a new Matcher instance with both include and exclude filters
4566
// (regex) "<filter>"
46-
func New(filters []string) Matcher {
67+
func NewWithIncludesExcludes(includeFilters, excludeFilters []string) Matcher {
4768
var m Matcher
4869

70+
m.includePatterns = buildPatterns(includeFilters)
71+
m.excludePatterns = buildPatterns(excludeFilters)
72+
73+
return m
74+
}
75+
76+
// buildPatterns creates patterns from filter strings
77+
func buildPatterns(filters []string) []pattern {
78+
var patterns []pattern
4979
r, _ := regexp.Compile("(regex)?.?\"(.+)\"")
5080

5181
for _, line := range filters {
@@ -83,7 +113,7 @@ func New(filters []string) Matcher {
83113
}
84114
log.Debug("pattern added regex: %v ", filter)
85115
p.regex = reg
86-
m.patterns = append(m.patterns, p)
116+
patterns = append(patterns, p)
87117
}
88-
return m
118+
return patterns
89119
}

src/matcher/matcher_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,102 @@ func TestMatcherMatch(t *testing.T) {
3131
assert.False(t, m.Match("notimportantService"))
3232
assert.False(t, m.Match("randomService"))
3333
}
34+
35+
func TestMatcherWithExcludes(t *testing.T) {
36+
var includeFilters = []string{
37+
`regex ".*"`, // Include all services
38+
}
39+
40+
var excludeFilters = []string{
41+
`"Windows Update"`,
42+
`regex "^(Themes|Spooler)$"`,
43+
}
44+
45+
m := NewWithIncludesExcludes(includeFilters, excludeFilters)
46+
47+
// Should include services that match include but not exclude
48+
assert.True(t, m.Match("newrelic-infra"))
49+
assert.True(t, m.Match("CustomService"))
50+
51+
// Should exclude services that match exclude filters
52+
assert.False(t, m.Match("windows update"))
53+
assert.False(t, m.Match("Themes"))
54+
assert.False(t, m.Match("Spooler"))
55+
56+
// Should exclude even if matches include
57+
assert.False(t, m.Match("Windows Update"))
58+
}
59+
60+
func TestMatcherWithExcludesOnly(t *testing.T) {
61+
var includeFilters = []string{
62+
`"ServiceA"`,
63+
`"ServiceB"`,
64+
}
65+
66+
var excludeFilters = []string{
67+
`"ServiceA"`,
68+
}
69+
70+
m := NewWithIncludesExcludes(includeFilters, excludeFilters)
71+
72+
// ServiceA should be excluded even though it's in include list
73+
assert.False(t, m.Match("ServiceA"))
74+
75+
// ServiceB should be included since it's not in exclude list
76+
assert.True(t, m.Match("ServiceB"))
77+
78+
// ServiceC should not match since it's not in include list
79+
assert.False(t, m.Match("ServiceC"))
80+
}
81+
82+
func TestMatcherBothIncludeAndExclude(t *testing.T) {
83+
var includeFilters = []string{
84+
`regex "^Windows.*"`, // Include all Windows services
85+
`"CustomService"`, // Include specific custom service
86+
}
87+
88+
var excludeFilters = []string{
89+
`"Windows Update"`, // Exclude Windows Update specifically
90+
`regex ".*Audio.*"`, // Exclude any audio-related services
91+
}
92+
93+
m := NewWithIncludesExcludes(includeFilters, excludeFilters)
94+
95+
// Should include: matches include pattern and doesn't match exclude
96+
assert.True(t, m.Match("Windows Defender"))
97+
assert.True(t, m.Match("Windows Time"))
98+
assert.True(t, m.Match("CustomService"))
99+
100+
// Should exclude: matches include pattern BUT also matches exclude pattern
101+
assert.False(t, m.Match("Windows Update")) // Explicitly excluded
102+
assert.False(t, m.Match("Windows Audio")) // Matches audio exclude pattern
103+
assert.False(t, m.Match("Custom Audio Service")) // Matches audio exclude pattern
104+
105+
// Should exclude: doesn't match any include pattern
106+
assert.False(t, m.Match("Linux Service"))
107+
assert.False(t, m.Match("RandomService"))
108+
assert.False(t, m.Match("SomeOtherService"))
109+
}
110+
111+
func TestMatcherOnlyIncludeFilters(t *testing.T) {
112+
// Test scenario 1: Only include_matching_entities provided
113+
var includeFilters = []string{
114+
`"newrelic-infra"`,
115+
`regex "^CustomService.*$"`,
116+
}
117+
118+
m := NewWithIncludesExcludes(includeFilters, nil)
119+
120+
// Should match services in the include list
121+
assert.True(t, m.Match("newrelic-infra"))
122+
assert.True(t, m.Match("CustomService123"))
123+
assert.True(t, m.Match("CustomServiceABC"))
124+
125+
// Should not match services not in the include list
126+
assert.False(t, m.Match("Windows Update"))
127+
assert.False(t, m.Match("Spooler"))
128+
assert.False(t, m.Match("RandomService"))
129+
}
130+
131+
// Removed TestMatcherOnlyExcludeFilters because exclude-only filtering is not supported
132+
// Include patterns are always required for proper filtering behavior

src/nri/config.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ type Config struct {
3030
}
3131

3232
type configYml struct {
33-
FilterEntity map[string][]string `yaml:"include_matching_entities"`
33+
IncludeEntity map[string][]string `yaml:"include_matching_entities"`
34+
ExcludeEntity map[string][]string `yaml:"exclude_matching_entities"`
3435
ExporterBindAddress string `yaml:"exporter_bind_address"`
3536
ExporterBindPort string `yaml:"exporter_bind_port"`
3637
ScrapeInterval string `yaml:"scrape_interval"`
@@ -44,16 +45,34 @@ func NewConfig(filename string) (*Config, error) {
4445
return nil, fmt.Errorf("failed to open %s: %s", filename, err)
4546
}
4647
// Parse the file
47-
c := configYml{FilterEntity: make(map[string][]string)}
48+
c := configYml{
49+
IncludeEntity: make(map[string][]string),
50+
ExcludeEntity: make(map[string][]string),
51+
}
4852
if err := yaml.Unmarshal(yamlFile, &c); err != nil {
4953
return nil, fmt.Errorf("failed to parse config: %s", err)
5054
}
55+
5156
var m matcher.Matcher
52-
if val, ok := c.FilterEntity["windowsService.name"]; ok {
53-
m = matcher.New(val)
54-
} else {
55-
return nil, fmt.Errorf("failed to parse config: only filter by windowsService.name is allowed")
57+
var includeFilters, excludeFilters []string
58+
59+
// Get include filters
60+
if val, ok := c.IncludeEntity["windowsService.name"]; ok {
61+
includeFilters = val
62+
}
63+
64+
// Get exclude filters
65+
if val, ok := c.ExcludeEntity["windowsService.name"]; ok {
66+
excludeFilters = val
67+
}
68+
69+
// Must have at least include filters (exclude-only is not supported)
70+
if len(includeFilters) == 0 {
71+
return nil, fmt.Errorf("failed to parse config: include_matching_entities is required for windowsService.name (exclude-only filtering is not supported)")
5672
}
73+
74+
// Create matcher with both include and exclude filters
75+
m = matcher.NewWithIncludesExcludes(includeFilters, excludeFilters)
5776
if m.IsEmpty() {
5877
return nil, fmt.Errorf("failed to parse config: no valid filter loaded")
5978
}

src/nri/config_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,89 @@ include_matching_entities:
3232
_, err = NewConfig(tmpfile.Name())
3333
require.NoError(t, err)
3434
}
35+
36+
func TestNewConfigWithExcludes(t *testing.T) {
37+
content := []byte(`
38+
exporter_bind_address: 127.0.0.1
39+
exporter_bind_port: 9182
40+
scrape_interval: 30s
41+
include_matching_entities:
42+
windowsService.name:
43+
- regex ".*"
44+
exclude_matching_entities:
45+
windowsService.name:
46+
- "Windows Update"
47+
- regex "^(Themes|Spooler)$"`)
48+
49+
tmpfile, err := ioutil.TempFile("", "config")
50+
require.NoError(t, err)
51+
defer os.Remove(tmpfile.Name()) // clean up
52+
_, err = tmpfile.Write(content)
53+
require.NoError(t, err)
54+
55+
config, err := NewConfig(tmpfile.Name())
56+
require.NoError(t, err)
57+
58+
// Test that the matcher works correctly with excludes
59+
require.True(t, config.Matcher.Match("newrelic-infra"))
60+
require.False(t, config.Matcher.Match("Windows Update"))
61+
require.False(t, config.Matcher.Match("Themes"))
62+
}
63+
64+
func TestNewConfigBothIncludeAndExclude(t *testing.T) {
65+
content := []byte(`
66+
exporter_bind_address: 127.0.0.1
67+
exporter_bind_port: 9182
68+
scrape_interval: 30s
69+
include_matching_entities:
70+
windowsService.name:
71+
- regex "^Windows.*"
72+
- "CustomService"
73+
exclude_matching_entities:
74+
windowsService.name:
75+
- "Windows Update"
76+
- regex ".*Audio.*"`)
77+
78+
tmpfile, err := ioutil.TempFile("", "config")
79+
require.NoError(t, err)
80+
defer os.Remove(tmpfile.Name()) // clean up
81+
_, err = tmpfile.Write(content)
82+
require.NoError(t, err)
83+
84+
config, err := NewConfig(tmpfile.Name())
85+
require.NoError(t, err)
86+
87+
// Test comprehensive include/exclude logic
88+
// Should include: matches include and doesn't match exclude
89+
require.True(t, config.Matcher.Match("Windows Defender"))
90+
require.True(t, config.Matcher.Match("CustomService"))
91+
92+
// Should exclude: matches include BUT also matches exclude
93+
require.False(t, config.Matcher.Match("Windows Update"))
94+
require.False(t, config.Matcher.Match("Windows Audio"))
95+
96+
// Should exclude: doesn't match include
97+
require.False(t, config.Matcher.Match("Linux Service"))
98+
}
99+
100+
func TestNewConfigExcludeOnlyNotSupported(t *testing.T) {
101+
content := []byte(`
102+
exporter_bind_address: 127.0.0.1
103+
exporter_bind_port: 9182
104+
scrape_interval: 30s
105+
exclude_matching_entities:
106+
windowsService.name:
107+
- "Windows Update"
108+
- regex "^(Themes|Spooler)$"`)
109+
110+
tmpfile, err := ioutil.TempFile("", "config")
111+
require.NoError(t, err)
112+
defer os.Remove(tmpfile.Name()) // clean up
113+
_, err = tmpfile.Write(content)
114+
require.NoError(t, err)
115+
116+
// Should fail because exclude-only is not supported
117+
_, err = NewConfig(tmpfile.Name())
118+
require.Error(t, err)
119+
require.Contains(t, err.Error(), "include_matching_entities is required")
120+
}

test/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ include_matching_entities:
66
- regex "^*$"
77
- "ServiceNameToBeIncluded"
88
- not "ServiceNameToBeExcluded"
9+
exclude_matching_entities:
10+
windowsService.name:
11+
- "newrelic-infra"
12+
- regex "^(Themes)$"
913

winservices-config.yml.sample

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ integrations:
1818
# - regex ".*"
1919
# - "newrelic-infra"
2020

21+
# To exclude services from the included set, create a list of filters to be applied
22+
# to the service names. Services that match any exclude filter will be excluded even
23+
# if they match an include filter. This is optional and requires include_matching_entities.
24+
#
25+
# exclude_matching_entities:
26+
# windowsService.name:
27+
# - "newrelic-infra"
28+
# - regex "^(Themes|Spooler)$"
29+
2130
# Time between consecutive metric collection of the integration.
2231
# It must be a number followed by a time unit (s, m or h), without spaces.
2332
#

0 commit comments

Comments
 (0)