Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions src/matcher/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) "<filter>"
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 {
Expand Down Expand Up @@ -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
}
99 changes: 99 additions & 0 deletions src/matcher/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 25 additions & 6 deletions src/nri/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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")
}
Expand Down
86 changes: 86 additions & 0 deletions src/nri/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
4 changes: 4 additions & 0 deletions test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ include_matching_entities:
- regex "^*$"
- "ServiceNameToBeIncluded"
- not "ServiceNameToBeExcluded"
exclude_matching_entities:
windowsService.name:
- "newrelic-infra"
- regex "^(Themes)$"

9 changes: 9 additions & 0 deletions winservices-config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
Loading