From 46cc9ad39cfe805524275db5efe7ebf63083a9c5 Mon Sep 17 00:00:00 2001 From: Shawn Jayasinghe <124621978+RetroJumper@users.noreply.github.com> Date: Tue, 31 Oct 2023 18:17:59 -0400 Subject: [PATCH 1/4] working version, need to test right regexs now --- pkg/config/config.go | 47 ++++++++++++---- pkg/job/discovery.go | 30 ++++++++++- pkg/job/discovery_test.go | 110 +++++++++++++++++++++++++++++++++++++- pkg/model/model.go | 6 ++- 4 files changed, 181 insertions(+), 12 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index ddccd6264..edc768b54 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,6 +6,7 @@ import ( "os" "github.com/aws/aws-sdk-go/aws" + "github.com/grafana/regexp" "gopkg.in/yaml.v2" "github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/logging" @@ -42,15 +43,16 @@ type JobLevelMetricFields struct { } type Job struct { - Regions []string `yaml:"regions"` - Type string `yaml:"type"` - Roles []Role `yaml:"roles"` - SearchTags []Tag `yaml:"searchTags"` - CustomTags []Tag `yaml:"customTags"` - DimensionNameRequirements []string `yaml:"dimensionNameRequirements"` - Metrics []*Metric `yaml:"metrics"` - RoundingPeriod *int64 `yaml:"roundingPeriod"` - RecentlyActiveOnly bool `yaml:"recentlyActiveOnly"` + Regions []string `yaml:"regions"` + Type string `yaml:"type"` + Roles []Role `yaml:"roles"` + SearchTags []Tag `yaml:"searchTags"` + CustomTags []Tag `yaml:"customTags"` + DimensionNameRequirements []string `yaml:"dimensionNameRequirements"` + DimensionValueFilter []*Dimension `yaml:"dimensionValueFilter"` + Metrics []*Metric `yaml:"metrics"` + RoundingPeriod *int64 `yaml:"roundingPeriod"` + RecentlyActiveOnly bool `yaml:"recentlyActiveOnly"` JobLevelMetricFields `yaml:",inline"` } @@ -206,6 +208,12 @@ func (j *Job) validateDiscoveryJob(jobIdx int) error { return err } } + for dimensionIdx, dimension := range j.DimensionValueFilter { + err := dimension.validateDimensionValueRegexps(dimensionIdx, parent) + if err != nil { + return err + } + } return nil } @@ -371,6 +379,7 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig { job.SearchTags = toModelTags(discoveryJob.SearchTags) job.CustomTags = toModelTags(discoveryJob.CustomTags) job.Metrics = toModelMetricConfig(discoveryJob.Metrics) + job.DimensionValueFilter = toModelDimensionValueFilterConfig(discoveryJob.DimensionValueFilter) job.ExportedTagsOnMetrics = []string{} if len(c.Discovery.ExportedTagsOnMetrics) > 0 { @@ -493,3 +502,23 @@ func logConfigErrors(cfg []byte, logger logging.Logger) { logger.Warn(`Config file error(s) detected: Yace might not work as expected. Future versions of Yace might fail to run with an invalid config file.`) } } + +// converts string to regexp for model +func toModelDimensionValueFilterConfig(dimensionValueFilter []*Dimension) []*model.DimensionFilter { + ret := make([]*model.DimensionFilter, 0, len(dimensionValueFilter)) + for _, d := range dimensionValueFilter { + ret = append(ret, &model.DimensionFilter{ + Name: d.Name, + Value: regexp.MustCompile(d.Value), + }) + } + return ret +} + +func (d *Dimension) validateDimensionValueRegexps(dimensionIdx int, parent string) error { + _, err := regexp.Compile(d.Value) + if err != nil { + return fmt.Errorf("Dimension Value filter regexp %d, %q in %v: Name should not be empty", dimensionIdx, d.Value, parent) + } + return nil +} diff --git a/pkg/job/discovery.go b/pkg/job/discovery.go index 243be1719..ad6a02403 100644 --- a/pkg/job/discovery.go +++ b/pkg/job/discovery.go @@ -9,6 +9,8 @@ import ( "strings" "sync" + "github.com/grafana/regexp" + "golang.org/x/sync/errgroup" "github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/clients/cloudwatch" @@ -192,7 +194,7 @@ func getMetricDataForQueries( assoc := maxdimassociator.NewAssociator(logger, svc.DimensionRegexps, resources) err := clientCloudwatch.ListMetrics(ctx, svc.Namespace, metric, discoveryJob.RecentlyActiveOnly, func(page []*model.Metric) { - data := getFilteredMetricDatas(logger, discoveryJob.Type, discoveryJob.ExportedTagsOnMetrics, page, discoveryJob.DimensionNameRequirements, metric, assoc) + data := getFilteredMetricDatas(logger, discoveryJob.Type, discoveryJob.ExportedTagsOnMetrics, page, discoveryJob.DimensionNameRequirements, discoveryJob.DimensionValueFilter, metric, assoc) mux.Lock() getMetricDatas = append(getMetricDatas, data...) @@ -215,6 +217,7 @@ func getFilteredMetricDatas( tagsOnMetrics []string, metricsList []*model.Metric, dimensionNameList []string, + dimensionValueFilterList []*model.DimensionFilter, m *model.MetricConfig, assoc resourceAssociator, ) []*model.CloudwatchData { @@ -223,6 +226,9 @@ func getFilteredMetricDatas( if len(dimensionNameList) > 0 && !metricDimensionsMatchNames(cwMetric, dimensionNameList) { continue } + if len(dimensionValueFilterList) > 0 && metricDimensionsFilterValue(cwMetric, dimensionValueFilterList) { + continue + } matchedResource, skip := assoc.AssociateMetricToResource(cwMetric) if skip { @@ -283,3 +289,25 @@ func metricDimensionsMatchNames(metric *model.Metric, dimensionNameRequirements } return true } + +// Modify this to look at values +func metricDimensionsFilterValue(metric *model.Metric, dimensionValueFilter []*model.DimensionFilter) bool { + for _, dimension := range metric.Dimensions { + for _, dimensionValueRegexp := range dimensionValueFilter { + if dimension.Name == dimensionValueRegexp.Name && regexpValue(dimension.Value, dimensionValueRegexp.Value) { + return true + } + } + } + return false +} + +// regex checker +func regexpValue(metricValue string, valueRegexp *regexp.Regexp) bool { + match := valueRegexp.FindStringSubmatch(metricValue) + if match == nil { + return false + } else { + return true + } +} diff --git a/pkg/job/discovery_test.go b/pkg/job/discovery_test.go index 052cd267e..7457bc32e 100644 --- a/pkg/job/discovery_test.go +++ b/pkg/job/discovery_test.go @@ -25,6 +25,7 @@ func Test_getFilteredMetricDatas(t *testing.T) { tagsOnMetrics []string dimensionRegexps []*regexp.Regexp dimensionNameRequirements []string + dimensionValueFilter []*model.DimensionFilter resources []*model.TaggedResource metricsList []*model.Metric m *model.MetricConfig @@ -397,11 +398,118 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, }, }, + { + "filter query dimension values for amq", + args{ + region: "us-east-1", + accountID: "123123123123", + namespace: "mq", + customTags: nil, + tagsOnMetrics: []string{ + "Value1", + "Value2", + }, + dimensionRegexps: config.SupportedServices.GetService("mq").DimensionRegexps, + dimensionValueFilter: []*model.DimensionFilter{ + { + Name: "FileSystemId", + Value: regexp.MustCompile("^fs-...456"), + }, + }, + resources: []*model.TaggedResource{ + { + ARN: "arn:aws:elasticfilesystem:us-east-1:123123123123:broker:test", + Tags: []model.Tag{ + { + Key: "Tag", + Value: "some-Tag", + }, + }, + Namespace: "mq", + Region: "us-east-1", + }, + }, + metricsList: []*model.Metric{ + { + MetricName: "StorageBytes", + Dimensions: []*model.Dimension{ + { + Name: "FileSystemId", + Value: "fs-abc123", + }, + { + Name: "StorageClass", + Value: "Standard", + }, + }, + Namespace: "AWS/AmazonMQ", + }, + { + MetricName: "StorageBytes", + Dimensions: []*model.Dimension{ + { + Name: "FileSystemId", + Value: "fs-abc456", + }, + { + Name: "StorageClass", + Value: "Standard", + }, + }, + Namespace: "AWS/AmazonMQ", + }, + }, + m: &model.MetricConfig{ + Name: "StorageBytes", + Statistics: []string{ + "Average", + }, + Period: 60, + Length: 600, + Delay: 120, + NilToZero: aws.Bool(false), + AddCloudwatchTimestamp: aws.Bool(false), + }, + }, + []model.CloudwatchData{ + { + AddCloudwatchTimestamp: aws.Bool(false), + Dimensions: []*model.Dimension{ + { + Name: "FileSystemId", + Value: "fs-abc123", + }, + { + Name: "StorageClass", + Value: "Standard", + }, + }, + ID: aws.String("global"), + Metric: aws.String("StorageBytes"), + Namespace: aws.String("mq"), + NilToZero: aws.Bool(false), + Period: 60, + Statistics: []string{ + "Average", + }, + Tags: []model.Tag{ + { + Key: "Value1", + Value: "", + }, + { + Key: "Value2", + Value: "", + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assoc := maxdimassociator.NewAssociator(logging.NewNopLogger(), tt.args.dimensionRegexps, tt.args.resources) - metricDatas := getFilteredMetricDatas(logging.NewNopLogger(), tt.args.namespace, tt.args.tagsOnMetrics, tt.args.metricsList, tt.args.dimensionNameRequirements, tt.args.m, assoc) + metricDatas := getFilteredMetricDatas(logging.NewNopLogger(), tt.args.namespace, tt.args.tagsOnMetrics, tt.args.metricsList, tt.args.dimensionNameRequirements, tt.args.dimensionValueFilter, tt.args.m, assoc) if len(metricDatas) != len(tt.wantGetMetricsData) { t.Errorf("len(getFilteredMetricDatas()) = %v, want %v", len(metricDatas), len(tt.wantGetMetricsData)) } diff --git a/pkg/model/model.go b/pkg/model/model.go index c7415cb75..1c6d99d63 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -26,6 +26,7 @@ type DiscoveryJob struct { SearchTags []Tag CustomTags []Tag DimensionNameRequirements []string + DimensionValueFilter []*DimensionFilter Metrics []*MetricConfig RoundingPeriod *int64 RecentlyActiveOnly bool @@ -91,7 +92,10 @@ type Dimension struct { Name string Value string } - +type DimensionFilter struct { + Name string + Value *regexp.Regexp +} type Metric struct { // The dimensions for the metric. Dimensions []*Dimension From 9c11027e1adba9d298b822a6ccfaf6394e7a4b27 Mon Sep 17 00:00:00 2001 From: Shawn Jayasinghe <124621978+RetroJumper@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:49:16 -0400 Subject: [PATCH 2/4] more representative amq unit test, understand how max associator will rejectitems as well --- pkg/job/discovery_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/job/discovery_test.go b/pkg/job/discovery_test.go index 7457bc32e..58b087e3b 100644 --- a/pkg/job/discovery_test.go +++ b/pkg/job/discovery_test.go @@ -418,7 +418,7 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, resources: []*model.TaggedResource{ { - ARN: "arn:aws:elasticfilesystem:us-east-1:123123123123:broker:test", + ARN: "arn:aws:mq:us-east-2:123456789012:broker:activemq-broker:b-000-111-222-333", Tags: []model.Tag{ { Key: "Tag", @@ -438,8 +438,8 @@ func Test_getFilteredMetricDatas(t *testing.T) { Value: "fs-abc123", }, { - Name: "StorageClass", - Value: "Standard", + Name: "Broker", + Value: "activemq-broker-1", }, }, Namespace: "AWS/AmazonMQ", @@ -452,8 +452,8 @@ func Test_getFilteredMetricDatas(t *testing.T) { Value: "fs-abc456", }, { - Name: "StorageClass", - Value: "Standard", + Name: "Broker", + Value: "activemq-broker-1", }, }, Namespace: "AWS/AmazonMQ", @@ -480,11 +480,11 @@ func Test_getFilteredMetricDatas(t *testing.T) { Value: "fs-abc123", }, { - Name: "StorageClass", - Value: "Standard", + Name: "Broker", + Value: "activemq-broker-1", }, }, - ID: aws.String("global"), + ID: aws.String("arn:aws:mq:us-east-2:123456789012:broker:activemq-broker:b-000-111-222-333"), Metric: aws.String("StorageBytes"), Namespace: aws.String("mq"), NilToZero: aws.Bool(false), From 8cc7be963dd8a4bbc867176555a616cb4956ba34 Mon Sep 17 00:00:00 2001 From: Shawn Jayasinghe <124621978+RetroJumper@users.noreply.github.com> Date: Wed, 1 Nov 2023 10:03:37 -0400 Subject: [PATCH 3/4] match main use case --- pkg/job/discovery_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/job/discovery_test.go b/pkg/job/discovery_test.go index 58b087e3b..636990613 100644 --- a/pkg/job/discovery_test.go +++ b/pkg/job/discovery_test.go @@ -412,8 +412,8 @@ func Test_getFilteredMetricDatas(t *testing.T) { dimensionRegexps: config.SupportedServices.GetService("mq").DimensionRegexps, dimensionValueFilter: []*model.DimensionFilter{ { - Name: "FileSystemId", - Value: regexp.MustCompile("^fs-...456"), + Name: "Queue", + Value: regexp.MustCompile("^ActiveMQ\\.Statistics\\.Destination\\..+"), }, }, resources: []*model.TaggedResource{ @@ -431,11 +431,11 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, metricsList: []*model.Metric{ { - MetricName: "StorageBytes", + MetricName: "QueueSize", Dimensions: []*model.Dimension{ { - Name: "FileSystemId", - Value: "fs-abc123", + Name: "Queue", + Value: "Fufillment", }, { Name: "Broker", @@ -448,8 +448,8 @@ func Test_getFilteredMetricDatas(t *testing.T) { MetricName: "StorageBytes", Dimensions: []*model.Dimension{ { - Name: "FileSystemId", - Value: "fs-abc456", + Name: "Queue", + Value: "ActiveMQ.Statistics.Destination.test", }, { Name: "Broker", @@ -460,7 +460,7 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, }, m: &model.MetricConfig{ - Name: "StorageBytes", + Name: "QueueSize", Statistics: []string{ "Average", }, @@ -476,8 +476,8 @@ func Test_getFilteredMetricDatas(t *testing.T) { AddCloudwatchTimestamp: aws.Bool(false), Dimensions: []*model.Dimension{ { - Name: "FileSystemId", - Value: "fs-abc123", + Name: "Queue", + Value: "Fufillment", }, { Name: "Broker", @@ -485,7 +485,7 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, }, ID: aws.String("arn:aws:mq:us-east-2:123456789012:broker:activemq-broker:b-000-111-222-333"), - Metric: aws.String("StorageBytes"), + Metric: aws.String("QueueSize"), Namespace: aws.String("mq"), NilToZero: aws.Bool(false), Period: 60, From 7d8b09ca553a05f3b1cbf01da8327059ce3e61f0 Mon Sep 17 00:00:00 2001 From: Shawn Jayasinghe <124621978+RetroJumper@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:02:29 -0400 Subject: [PATCH 4/4] added all scenarios, and customNamespace support --- pkg/config/config.go | 27 ++++++++++++------- pkg/job/custom.go | 3 +++ pkg/job/discovery_test.go | 56 ++++++++++++++++++++++++++++++++++++++- pkg/model/model.go | 1 + 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index edc768b54..19b3131f9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -67,15 +67,16 @@ type Static struct { } type CustomNamespace struct { - Regions []string `yaml:"regions"` - Name string `yaml:"name"` - Namespace string `yaml:"namespace"` - RecentlyActiveOnly bool `yaml:"recentlyActiveOnly"` - Roles []Role `yaml:"roles"` - Metrics []*Metric `yaml:"metrics"` - CustomTags []Tag `yaml:"customTags"` - DimensionNameRequirements []string `yaml:"dimensionNameRequirements"` - RoundingPeriod *int64 `yaml:"roundingPeriod"` + Regions []string `yaml:"regions"` + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + RecentlyActiveOnly bool `yaml:"recentlyActiveOnly"` + Roles []Role `yaml:"roles"` + Metrics []*Metric `yaml:"metrics"` + CustomTags []Tag `yaml:"customTags"` + DimensionNameRequirements []string `yaml:"dimensionNameRequirements"` + DimensionValueFilter []*Dimension `yaml:"dimensionValueFilter"` + RoundingPeriod *int64 `yaml:"roundingPeriod"` JobLevelMetricFields `yaml:",inline"` } @@ -247,7 +248,12 @@ func (j *CustomNamespace) validateCustomNamespaceJob(jobIdx int) error { return err } } - + for dimensionIdx, dimension := range j.DimensionValueFilter { + err := dimension.validateDimensionValueRegexps(dimensionIdx, parent) + if err != nil { + return err + } + } return nil } @@ -423,6 +429,7 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig { job.Roles = toModelRoles(customNamespaceJob.Roles) job.CustomTags = toModelTags(customNamespaceJob.CustomTags) job.Metrics = toModelMetricConfig(customNamespaceJob.Metrics) + job.DimensionValueFilter = toModelDimensionValueFilterConfig(customNamespaceJob.DimensionValueFilter) jobsCfg.CustomNamespaceJobs = append(jobsCfg.CustomNamespaceJobs, job) } diff --git a/pkg/job/custom.go b/pkg/job/custom.go index 23bfdc177..0ec34096a 100644 --- a/pkg/job/custom.go +++ b/pkg/job/custom.go @@ -107,6 +107,9 @@ func getMetricDataForQueriesForCustomNamespace( if len(customNamespaceJob.DimensionNameRequirements) > 0 && !metricDimensionsMatchNames(cwMetric, customNamespaceJob.DimensionNameRequirements) { continue } + if len(customNamespaceJob.DimensionValueFilter) > 0 && metricDimensionsFilterValue(cwMetric, customNamespaceJob.DimensionValueFilter) { + continue + } for _, stats := range metric.Statistics { id := fmt.Sprintf("id_%d", rand.Int()) diff --git a/pkg/job/discovery_test.go b/pkg/job/discovery_test.go index 636990613..460885f0f 100644 --- a/pkg/job/discovery_test.go +++ b/pkg/job/discovery_test.go @@ -415,6 +415,18 @@ func Test_getFilteredMetricDatas(t *testing.T) { Name: "Queue", Value: regexp.MustCompile("^ActiveMQ\\.Statistics\\.Destination\\..+"), }, + { + Name: "Queue", + Value: regexp.MustCompile("^.+\\.stats"), + }, + { + Name: "Queue", + Value: regexp.MustCompile("^Loadtesting.+"), + }, + { + Name: "Queue", + Value: regexp.MustCompile("^TEST.+"), + }, }, resources: []*model.TaggedResource{ { @@ -445,7 +457,7 @@ func Test_getFilteredMetricDatas(t *testing.T) { Namespace: "AWS/AmazonMQ", }, { - MetricName: "StorageBytes", + MetricName: "QueueSize", Dimensions: []*model.Dimension{ { Name: "Queue", @@ -458,6 +470,48 @@ func Test_getFilteredMetricDatas(t *testing.T) { }, Namespace: "AWS/AmazonMQ", }, + { + MetricName: "QueueSize", + Dimensions: []*model.Dimension{ + { + Name: "Queue", + Value: "test.stats", + }, + { + Name: "Broker", + Value: "activemq-broker-1", + }, + }, + Namespace: "AWS/AmazonMQ", + }, + { + MetricName: "QueueSize", + Dimensions: []*model.Dimension{ + { + Name: "Queue", + Value: "Loadtesting.wow", + }, + { + Name: "Broker", + Value: "activemq-broker-1", + }, + }, + Namespace: "AWS/AmazonMQ", + }, + { + MetricName: "QueueSize", + Dimensions: []*model.Dimension{ + { + Name: "Queue", + Value: "TEST.hmmm123", + }, + { + Name: "Broker", + Value: "activemq-broker-1", + }, + }, + Namespace: "AWS/AmazonMQ", + }, }, m: &model.MetricConfig{ Name: "QueueSize", diff --git a/pkg/model/model.go b/pkg/model/model.go index 1c6d99d63..fb5d7d36e 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -53,6 +53,7 @@ type CustomNamespaceJob struct { Metrics []*MetricConfig CustomTags []Tag DimensionNameRequirements []string + DimensionValueFilter []*DimensionFilter RoundingPeriod *int64 JobLevelMetricFields }