Skip to content

Commit 1092b6c

Browse files
Improve usability and performance of searchTags (prometheus-community#1270)
Co-authored-by: Cristian Greco <[email protected]>
1 parent ad5406f commit 1092b6c

File tree

6 files changed

+93
-29
lines changed

6 files changed

+93
-29
lines changed

docs/configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ type: <string>
9191
roles:
9292
[ - <role_config> ... ]
9393
94-
# List of Key/Value pairs to use for tag filtering (all must match). Value can be a regex.
94+
# List of Key/Value pairs to use for tag filtering (all must match).
95+
# The key is the AWS Tag key and is case-sensitive
96+
# The value will be treated as a regex
9597
searchTags:
9698
[ - <search_tags_config> ... ]
9799

pkg/clients/tagging/v1/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,27 @@ func (c client) GetResources(ctx context.Context, job model.DiscoveryJob, region
6969

7070
if len(svc.ResourceFilters) > 0 {
7171
shouldHaveDiscoveredResources = true
72+
73+
var tagFilters []*resourcegroupstaggingapi.TagFilter
74+
if len(job.SearchTags) > 0 {
75+
for i := range job.SearchTags {
76+
// Because everything with the AWS APIs is pointers we need a pointer to the `Key` field from the SearchTag.
77+
// We can't take a pointer to any fields from loop variable or the pointer will always be the same and this logic will be broken.
78+
st := job.SearchTags[i]
79+
80+
// AWS's GetResources has a TagFilter option which matches the semantics of our SearchTags where all filters must match
81+
// Their value matching implementation is different though so instead of mapping the Key and Value we only map the Keys.
82+
// Their API docs say, "If you don't specify a value for a key, the response returns all resources that are tagged with that key, with any or no value."
83+
// which makes this a safe way to reduce the amount of data we need to filter out.
84+
// https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters
85+
tagFilters = append(tagFilters, &resourcegroupstaggingapi.TagFilter{Key: &st.Key})
86+
}
87+
}
88+
7289
inputparams := &resourcegroupstaggingapi.GetResourcesInput{
7390
ResourceTypeFilters: svc.ResourceFilters,
7491
ResourcesPerPage: aws.Int64(100), // max allowed value according to API docs
92+
TagFilters: tagFilters,
7593
}
7694
pageNum := 0
7795

pkg/clients/tagging/v2/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/aws/aws-sdk-go-v2/service/databasemigrationservice"
1313
"github.com/aws/aws-sdk-go-v2/service/ec2"
1414
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
15+
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
1516
"github.com/aws/aws-sdk-go-v2/service/shield"
1617
"github.com/aws/aws-sdk-go-v2/service/storagegateway"
1718

@@ -72,9 +73,25 @@ func (c client) GetResources(ctx context.Context, job model.DiscoveryJob, region
7273
for _, filter := range svc.ResourceFilters {
7374
filters = append(filters, *filter)
7475
}
76+
var tagFilters []types.TagFilter
77+
if len(job.SearchTags) > 0 {
78+
for i := range job.SearchTags {
79+
// Because everything with the AWS APIs is pointers we need a pointer to the `Key` field from the SearchTag.
80+
// We can't take a pointer to any fields from loop variable or the pointer will always be the same and this logic will be broken.
81+
st := job.SearchTags[i]
82+
83+
// AWS's GetResources has a TagFilter option which matches the semantics of our SearchTags where all filters must match
84+
// Their value matching implementation is different though so instead of mapping the Key and Value we only map the Keys.
85+
// Their API docs say, "If you don't specify a value for a key, the response returns all resources that are tagged with that key, with any or no value."
86+
// which makes this a safe way to reduce the amount of data we need to filter out.
87+
// https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html#resourcegrouptagging-GetResources-request-TagFilters
88+
tagFilters = append(tagFilters, types.TagFilter{Key: &st.Key})
89+
}
90+
}
7591
inputparams := &resourcegroupstaggingapi.GetResourcesInput{
7692
ResourceTypeFilters: filters,
7793
ResourcesPerPage: aws.Int32(int32(100)), // max allowed value according to API docs
94+
TagFilters: tagFilters,
7895
}
7996

8097
paginator := resourcegroupstaggingapi.NewGetResourcesPaginator(c.taggingAPI, inputparams, func(options *resourcegroupstaggingapi.GetResourcesPaginatorOptions) {

pkg/config/config.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77

88
"github.com/aws/aws-sdk-go/aws"
9+
"github.com/grafana/regexp"
910
"gopkg.in/yaml.v2"
1011

1112
"github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/logging"
@@ -208,6 +209,12 @@ func (j *Job) validateDiscoveryJob(jobIdx int) error {
208209
}
209210
}
210211

212+
for _, st := range j.SearchTags {
213+
if _, err := regexp.Compile(st.Value); err != nil {
214+
return fmt.Errorf("Discovery job [%s/%d]: search tag value for %s has invalid regex value %s: %w", j.Type, jobIdx, st.Key, st.Value, err)
215+
}
216+
}
217+
211218
return nil
212219
}
213220

@@ -371,7 +378,7 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig {
371378
job.NilToZero = discoveryJob.NilToZero
372379
job.AddCloudwatchTimestamp = discoveryJob.AddCloudwatchTimestamp
373380
job.Roles = toModelRoles(discoveryJob.Roles)
374-
job.SearchTags = toModelTags(discoveryJob.SearchTags)
381+
job.SearchTags = toModelSearchTags(discoveryJob.SearchTags)
375382
job.CustomTags = toModelTags(discoveryJob.CustomTags)
376383
job.Metrics = toModelMetricConfig(discoveryJob.Metrics)
377384
job.IncludeContextOnInfoMetrics = discoveryJob.IncludeContextOnInfoMetrics
@@ -435,6 +442,19 @@ func toModelTags(tags []Tag) []model.Tag {
435442
return ret
436443
}
437444

445+
func toModelSearchTags(tags []Tag) []model.SearchTag {
446+
ret := make([]model.SearchTag, 0, len(tags))
447+
for _, t := range tags {
448+
// This should never panic as long as regex validation continues to happen before model mapping
449+
r := regexp.MustCompile(t.Value)
450+
ret = append(ret, model.SearchTag{
451+
Key: t.Key,
452+
Value: r,
453+
})
454+
}
455+
return ret
456+
}
457+
438458
func toModelRoles(roles []Role) []model.Role {
439459
ret := make([]model.Role, 0, len(roles))
440460
for _, r := range roles {

pkg/model/model.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type DiscoveryJob struct {
2323
Regions []string
2424
Type string
2525
Roles []Role
26-
SearchTags []Tag
26+
SearchTags []SearchTag
2727
CustomTags []Tag
2828
DimensionNameRequirements []string
2929
Metrics []*MetricConfig
@@ -94,6 +94,11 @@ type Tag struct {
9494
Value string
9595
}
9696

97+
type SearchTag struct {
98+
Key string
99+
Value *regexp.Regexp
100+
}
101+
97102
type Dimension struct {
98103
Name string
99104
Value string
@@ -181,25 +186,27 @@ type TaggedResource struct {
181186

182187
// filterThroughTags returns true if all filterTags match
183188
// with tags of the TaggedResource, returns false otherwise.
184-
func (r TaggedResource) FilterThroughTags(filterTags []Tag) bool {
189+
func (r TaggedResource) FilterThroughTags(filterTags []SearchTag) bool {
185190
if len(filterTags) == 0 {
186191
return true
187192
}
188193

189-
tagMatches := 0
194+
tagFilterMatches := 0
190195

191196
for _, resourceTag := range r.Tags {
192197
for _, filterTag := range filterTags {
193198
if resourceTag.Key == filterTag.Key {
194-
r, _ := regexp.Compile(filterTag.Value)
195-
if r.MatchString(resourceTag.Value) {
196-
tagMatches++
199+
if !filterTag.Value.MatchString(resourceTag.Value) {
200+
return false
197201
}
202+
// A resource needs to match all SearchTags to be returned, so we track the number of tag filter
203+
// matches to ensure it matches the number of tag filters at the end
204+
tagFilterMatches++
198205
}
199206
}
200207
}
201208

202-
return tagMatches == len(filterTags)
209+
return tagFilterMatches == len(filterTags)
203210
}
204211

205212
// MetricTags returns a list of tags built from the tags of

pkg/model/model_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package model
33
import (
44
"testing"
55

6+
"github.com/grafana/regexp"
67
"github.com/stretchr/testify/require"
78
)
89

910
func Test_FilterThroughTags(t *testing.T) {
1011
testCases := []struct {
1112
testName string
1213
resourceTags []Tag
13-
filterTags []Tag
14+
filterTags []SearchTag
1415
result bool
1516
}{
1617
{
@@ -21,10 +22,10 @@ func Test_FilterThroughTags(t *testing.T) {
2122
Value: "v1",
2223
},
2324
},
24-
filterTags: []Tag{
25+
filterTags: []SearchTag{
2526
{
2627
Key: "k1",
27-
Value: "v1",
28+
Value: regexp.MustCompile("v1"),
2829
},
2930
},
3031
result: true,
@@ -37,10 +38,10 @@ func Test_FilterThroughTags(t *testing.T) {
3738
Value: "v1",
3839
},
3940
},
40-
filterTags: []Tag{
41+
filterTags: []SearchTag{
4142
{
4243
Key: "k2",
43-
Value: "v2",
44+
Value: regexp.MustCompile("v2"),
4445
},
4546
},
4647
result: false,
@@ -57,10 +58,10 @@ func Test_FilterThroughTags(t *testing.T) {
5758
Value: "v2",
5859
},
5960
},
60-
filterTags: []Tag{
61+
filterTags: []SearchTag{
6162
{
6263
Key: "k1",
63-
Value: "v1",
64+
Value: regexp.MustCompile("v1"),
6465
},
6566
},
6667
result: true,
@@ -73,14 +74,14 @@ func Test_FilterThroughTags(t *testing.T) {
7374
Value: "v1",
7475
},
7576
},
76-
filterTags: []Tag{
77+
filterTags: []SearchTag{
7778
{
7879
Key: "k1",
79-
Value: "v1",
80+
Value: regexp.MustCompile("v1"),
8081
},
8182
{
8283
Key: "k2",
83-
Value: "v2",
84+
Value: regexp.MustCompile("v2"),
8485
},
8586
},
8687
result: false,
@@ -93,10 +94,10 @@ func Test_FilterThroughTags(t *testing.T) {
9394
Value: "v1",
9495
},
9596
},
96-
filterTags: []Tag{
97+
filterTags: []SearchTag{
9798
{
9899
Key: "k2",
99-
Value: "v1",
100+
Value: regexp.MustCompile("v1"),
100101
},
101102
},
102103
result: false,
@@ -109,21 +110,21 @@ func Test_FilterThroughTags(t *testing.T) {
109110
Value: "v1",
110111
},
111112
},
112-
filterTags: []Tag{
113+
filterTags: []SearchTag{
113114
{
114115
Key: "k1",
115-
Value: "v2",
116+
Value: regexp.MustCompile("v2"),
116117
},
117118
},
118119
result: false,
119120
},
120121
{
121122
testName: "resource without tags",
122123
resourceTags: []Tag{},
123-
filterTags: []Tag{
124+
filterTags: []SearchTag{
124125
{
125126
Key: "k1",
126-
Value: "v2",
127+
Value: regexp.MustCompile("v2"),
127128
},
128129
},
129130
result: false,
@@ -136,7 +137,7 @@ func Test_FilterThroughTags(t *testing.T) {
136137
Value: "v1",
137138
},
138139
},
139-
filterTags: []Tag{},
140+
filterTags: []SearchTag{},
140141
result: true,
141142
},
142143
{
@@ -147,10 +148,10 @@ func Test_FilterThroughTags(t *testing.T) {
147148
Value: "v1",
148149
},
149150
},
150-
filterTags: []Tag{
151+
filterTags: []SearchTag{
151152
{
152153
Key: "k1",
153-
Value: "v.*",
154+
Value: regexp.MustCompile("v.*"),
154155
},
155156
},
156157
result: true,
@@ -165,7 +166,6 @@ func Test_FilterThroughTags(t *testing.T) {
165166
Region: "us-east-1",
166167
Tags: tc.resourceTags,
167168
}
168-
169169
require.Equal(t, tc.result, res.FilterThroughTags(tc.filterTags))
170170
})
171171
}

0 commit comments

Comments
 (0)