diff --git a/src/matcher/matcher.go b/src/matcher/matcher.go index dbf2c04..ee1c454 100644 --- a/src/matcher/matcher.go +++ b/src/matcher/matcher.go @@ -13,39 +13,69 @@ import ( // Matcher groups the rules to validate the service name type Matcher struct { - patterns []pattern + includePatterns []pattern + excludePatterns []pattern } type pattern struct { regex *regexp.Regexp } -// Match returns true if the string matches one of the patterns +// Match returns true if the string matches include patterns and doesn't match exclude patterns +// Include patterns are required - this matcher does not support exclude-only filtering func (m *Matcher) Match(s string) bool { - for _, p := range m.patterns { + // Must match at least one include pattern first + includeMatch := false + for _, p := range m.includePatterns { if p.match(s) { - return true + includeMatch = true + break } } - return false -} -// IsEmpty returns true if the Matcher has no patterns -func (m *Matcher) IsEmpty() bool { - if len(m.patterns) != 0 { + // If no include patterns match, return false + if !includeMatch { return false } + + // Check if it matches any exclude patterns (exclude takes precedence) + for _, p := range m.excludePatterns { + if p.match(s) { + return false + } + } + return true } +// IsEmpty returns true if the Matcher has no include patterns +// (exclude patterns alone are not sufficient for a valid matcher) +func (m *Matcher) IsEmpty() bool { + return len(m.includePatterns) == 0 +} + func (p pattern) match(s string) bool { return p.regex.MatchString(s) } -// New create a new Matcher instance from slices of filters +// New create a new Matcher instance from slices of include and exclude filters +func New(includeFilters []string) Matcher { + return NewWithIncludesExcludes(includeFilters, nil) +} + +// NewWithExcludes creates a new Matcher instance with both include and exclude filters // (regex) "" -func New(filters []string) Matcher { +func NewWithIncludesExcludes(includeFilters, excludeFilters []string) Matcher { var m Matcher + m.includePatterns = buildPatterns(includeFilters) + m.excludePatterns = buildPatterns(excludeFilters) + + return m +} + +// buildPatterns creates patterns from filter strings +func buildPatterns(filters []string) []pattern { + var patterns []pattern r, _ := regexp.Compile("(regex)?.?\"(.+)\"") for _, line := range filters { @@ -83,7 +113,7 @@ func New(filters []string) Matcher { } log.Debug("pattern added regex: %v ", filter) p.regex = reg - m.patterns = append(m.patterns, p) + patterns = append(patterns, p) } - return m + return patterns } diff --git a/src/matcher/matcher_test.go b/src/matcher/matcher_test.go index b902f09..57b75b1 100644 --- a/src/matcher/matcher_test.go +++ b/src/matcher/matcher_test.go @@ -31,3 +31,102 @@ func TestMatcherMatch(t *testing.T) { assert.False(t, m.Match("notimportantService")) assert.False(t, m.Match("randomService")) } + +func TestMatcherWithExcludes(t *testing.T) { + var includeFilters = []string{ + `regex ".*"`, // Include all services + } + + var excludeFilters = []string{ + `"Windows Update"`, + `regex "^(Themes|Spooler)$"`, + } + + m := NewWithIncludesExcludes(includeFilters, excludeFilters) + + // Should include services that match include but not exclude + assert.True(t, m.Match("newrelic-infra")) + assert.True(t, m.Match("CustomService")) + + // Should exclude services that match exclude filters + assert.False(t, m.Match("windows update")) + assert.False(t, m.Match("Themes")) + assert.False(t, m.Match("Spooler")) + + // Should exclude even if matches include + assert.False(t, m.Match("Windows Update")) +} + +func TestMatcherWithExcludesOnly(t *testing.T) { + var includeFilters = []string{ + `"ServiceA"`, + `"ServiceB"`, + } + + var excludeFilters = []string{ + `"ServiceA"`, + } + + m := NewWithIncludesExcludes(includeFilters, excludeFilters) + + // ServiceA should be excluded even though it's in include list + assert.False(t, m.Match("ServiceA")) + + // ServiceB should be included since it's not in exclude list + assert.True(t, m.Match("ServiceB")) + + // ServiceC should not match since it's not in include list + assert.False(t, m.Match("ServiceC")) +} + +func TestMatcherBothIncludeAndExclude(t *testing.T) { + var includeFilters = []string{ + `regex "^Windows.*"`, // Include all Windows services + `"CustomService"`, // Include specific custom service + } + + var excludeFilters = []string{ + `"Windows Update"`, // Exclude Windows Update specifically + `regex ".*Audio.*"`, // Exclude any audio-related services + } + + m := NewWithIncludesExcludes(includeFilters, excludeFilters) + + // Should include: matches include pattern and doesn't match exclude + assert.True(t, m.Match("Windows Defender")) + assert.True(t, m.Match("Windows Time")) + assert.True(t, m.Match("CustomService")) + + // Should exclude: matches include pattern BUT also matches exclude pattern + assert.False(t, m.Match("Windows Update")) // Explicitly excluded + assert.False(t, m.Match("Windows Audio")) // Matches audio exclude pattern + assert.False(t, m.Match("Custom Audio Service")) // Matches audio exclude pattern + + // Should exclude: doesn't match any include pattern + assert.False(t, m.Match("Linux Service")) + assert.False(t, m.Match("RandomService")) + assert.False(t, m.Match("SomeOtherService")) +} + +func TestMatcherOnlyIncludeFilters(t *testing.T) { + // Test scenario 1: Only include_matching_entities provided + var includeFilters = []string{ + `"newrelic-infra"`, + `regex "^CustomService.*$"`, + } + + m := NewWithIncludesExcludes(includeFilters, nil) + + // Should match services in the include list + assert.True(t, m.Match("newrelic-infra")) + assert.True(t, m.Match("CustomService123")) + assert.True(t, m.Match("CustomServiceABC")) + + // Should not match services not in the include list + assert.False(t, m.Match("Windows Update")) + assert.False(t, m.Match("Spooler")) + assert.False(t, m.Match("RandomService")) +} + +// Removed TestMatcherOnlyExcludeFilters because exclude-only filtering is not supported +// Include patterns are always required for proper filtering behavior diff --git a/src/nri/config.go b/src/nri/config.go index 4d71461..c4ba708 100644 --- a/src/nri/config.go +++ b/src/nri/config.go @@ -30,7 +30,8 @@ type Config struct { } type configYml struct { - FilterEntity map[string][]string `yaml:"include_matching_entities"` + IncludeEntity map[string][]string `yaml:"include_matching_entities"` + ExcludeEntity map[string][]string `yaml:"exclude_matching_entities"` ExporterBindAddress string `yaml:"exporter_bind_address"` ExporterBindPort string `yaml:"exporter_bind_port"` ScrapeInterval string `yaml:"scrape_interval"` @@ -44,16 +45,34 @@ func NewConfig(filename string) (*Config, error) { return nil, fmt.Errorf("failed to open %s: %s", filename, err) } // Parse the file - c := configYml{FilterEntity: make(map[string][]string)} + c := configYml{ + IncludeEntity: make(map[string][]string), + ExcludeEntity: make(map[string][]string), + } if err := yaml.Unmarshal(yamlFile, &c); err != nil { return nil, fmt.Errorf("failed to parse config: %s", err) } + var m matcher.Matcher - if val, ok := c.FilterEntity["windowsService.name"]; ok { - m = matcher.New(val) - } else { - return nil, fmt.Errorf("failed to parse config: only filter by windowsService.name is allowed") + var includeFilters, excludeFilters []string + + // Get include filters + if val, ok := c.IncludeEntity["windowsService.name"]; ok { + includeFilters = val + } + + // Get exclude filters + if val, ok := c.ExcludeEntity["windowsService.name"]; ok { + excludeFilters = val + } + + // Must have at least include filters (exclude-only is not supported) + if len(includeFilters) == 0 { + return nil, fmt.Errorf("failed to parse config: include_matching_entities is required for windowsService.name (exclude-only filtering is not supported)") } + + // Create matcher with both include and exclude filters + m = matcher.NewWithIncludesExcludes(includeFilters, excludeFilters) if m.IsEmpty() { return nil, fmt.Errorf("failed to parse config: no valid filter loaded") } diff --git a/src/nri/config_test.go b/src/nri/config_test.go index c6d8dbb..9d33156 100644 --- a/src/nri/config_test.go +++ b/src/nri/config_test.go @@ -32,3 +32,89 @@ include_matching_entities: _, err = NewConfig(tmpfile.Name()) require.NoError(t, err) } + +func TestNewConfigWithExcludes(t *testing.T) { + content := []byte(` +exporter_bind_address: 127.0.0.1 +exporter_bind_port: 9182 +scrape_interval: 30s +include_matching_entities: + windowsService.name: + - regex ".*" +exclude_matching_entities: + windowsService.name: + - "Windows Update" + - regex "^(Themes|Spooler)$"`) + + tmpfile, err := ioutil.TempFile("", "config") + require.NoError(t, err) + defer os.Remove(tmpfile.Name()) // clean up + _, err = tmpfile.Write(content) + require.NoError(t, err) + + config, err := NewConfig(tmpfile.Name()) + require.NoError(t, err) + + // Test that the matcher works correctly with excludes + require.True(t, config.Matcher.Match("newrelic-infra")) + require.False(t, config.Matcher.Match("Windows Update")) + require.False(t, config.Matcher.Match("Themes")) +} + +func TestNewConfigBothIncludeAndExclude(t *testing.T) { + content := []byte(` +exporter_bind_address: 127.0.0.1 +exporter_bind_port: 9182 +scrape_interval: 30s +include_matching_entities: + windowsService.name: + - regex "^Windows.*" + - "CustomService" +exclude_matching_entities: + windowsService.name: + - "Windows Update" + - regex ".*Audio.*"`) + + tmpfile, err := ioutil.TempFile("", "config") + require.NoError(t, err) + defer os.Remove(tmpfile.Name()) // clean up + _, err = tmpfile.Write(content) + require.NoError(t, err) + + config, err := NewConfig(tmpfile.Name()) + require.NoError(t, err) + + // Test comprehensive include/exclude logic + // Should include: matches include and doesn't match exclude + require.True(t, config.Matcher.Match("Windows Defender")) + require.True(t, config.Matcher.Match("CustomService")) + + // Should exclude: matches include BUT also matches exclude + require.False(t, config.Matcher.Match("Windows Update")) + require.False(t, config.Matcher.Match("Windows Audio")) + + // Should exclude: doesn't match include + require.False(t, config.Matcher.Match("Linux Service")) +} + +func TestNewConfigExcludeOnlyNotSupported(t *testing.T) { + content := []byte(` +exporter_bind_address: 127.0.0.1 +exporter_bind_port: 9182 +scrape_interval: 30s +exclude_matching_entities: + windowsService.name: + - "Windows Update" + - regex "^(Themes|Spooler)$"`) + + tmpfile, err := ioutil.TempFile("", "config") + require.NoError(t, err) + defer os.Remove(tmpfile.Name()) // clean up + _, err = tmpfile.Write(content) + require.NoError(t, err) + + // Should fail because exclude-only is not supported + _, err = NewConfig(tmpfile.Name()) + require.Error(t, err) + require.Contains(t, err.Error(), "include_matching_entities is required") +} diff --git a/test/config.yml b/test/config.yml index b7547ea..721121d 100644 --- a/test/config.yml +++ b/test/config.yml @@ -6,4 +6,8 @@ include_matching_entities: - regex "^*$" - "ServiceNameToBeIncluded" - not "ServiceNameToBeExcluded" +exclude_matching_entities: + windowsService.name: + - "newrelic-infra" + - regex "^(Themes)$" \ No newline at end of file diff --git a/winservices-config.yml.sample b/winservices-config.yml.sample index 013b839..539834b 100644 --- a/winservices-config.yml.sample +++ b/winservices-config.yml.sample @@ -18,6 +18,15 @@ integrations: # - regex ".*" # - "newrelic-infra" + # To exclude services from the included set, create a list of filters to be applied + # to the service names. Services that match any exclude filter will be excluded even + # if they match an include filter. This is optional and requires include_matching_entities. + # + # exclude_matching_entities: + # windowsService.name: + # - "newrelic-infra" + # - regex "^(Themes|Spooler)$" + # Time between consecutive metric collection of the integration. # It must be a number followed by a time unit (s, m or h), without spaces. #