Skip to content

Commit 8c529d6

Browse files
add region label to operational metrics (#835)
1 parent 4fd74b5 commit 8c529d6

32 files changed

Lines changed: 519 additions & 40 deletions

cmd/exporter/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Config struct {
2626
Azure struct {
2727
Services StringSliceFlag
2828
SubscriptionId string
29+
Region string
2930
}
3031
}
3132
Collector struct {

pkg/aws/aws.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,13 @@ func newWithDependencies(ctx context.Context, config *Config, awsClient client.C
138138

139139
switch service {
140140
case serviceS3:
141-
collector := s3.New(config.ScrapeInterval, awsClient)
141+
collector, err := s3.New(ctx, config.ScrapeInterval, awsClient)
142+
if err != nil {
143+
logger.LogAttrs(ctx, slog.LevelError, "Error creating collector",
144+
slog.String("service", service),
145+
slog.String("message", err.Error()))
146+
continue
147+
}
142148
collectors = append(collectors, collector)
143149
case serviceEC2:
144150
collector, err := ec2Collector.New(ctx, &ec2Collector.Config{

pkg/aws/aws_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func Test_NewWithDependencies(t *testing.T) {
7878
{RegionName: stringPtr("us-east-1")},
7979
},
8080
setupMockClient: func(m *mock_client.MockClient) {
81+
m.EXPECT().DescribeRegions(gomock.Any(), false).Return(nil, nil)
8182
},
8283
setupRegionClients: map[string]client.Client{},
8384
expectedCollectors: 1,
@@ -92,6 +93,9 @@ func Test_NewWithDependencies(t *testing.T) {
9293
{RegionName: stringPtr("us-east-1")},
9394
{RegionName: stringPtr("us-west-2")},
9495
},
96+
setupMockClient: func(m *mock_client.MockClient) {
97+
m.EXPECT().DescribeRegions(gomock.Any(), false).Return(nil, nil)
98+
},
9599
setupRegionClients: map[string]client.Client{
96100
"us-east-1": &mockRegionClient{}, // EC2 collector needs region map
97101
"us-west-2": &mockRegionClient{},
@@ -119,6 +123,9 @@ func Test_NewWithDependencies(t *testing.T) {
119123
regions: []types.Region{
120124
{RegionName: stringPtr("us-east-1")},
121125
},
126+
setupMockClient: func(m *mock_client.MockClient) {
127+
m.EXPECT().DescribeRegions(gomock.Any(), false).Return(nil, nil).Times(2)
128+
},
122129
setupRegionClients: map[string]client.Client{},
123130
expectedCollectors: 2, // Both should create collectors
124131
validateAWS: func(t *testing.T, aws *AWS) {
@@ -489,7 +496,7 @@ func Test_CollectMetrics(t *testing.T) {
489496
c.EXPECT().Describe(gomock.Any()).Return(nil).AnyTimes()
490497
}
491498
aws := &AWS{
492-
Config: nil,
499+
Config: &Config{Region: "us-east-1"},
493500
collectors: []provider.Collector{},
494501
logger: logger,
495502
ctx: t.Context(),

pkg/aws/client/billing.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/aws/aws-sdk-go-v2/service/costexplorer"
1111
"github.com/aws/aws-sdk-go-v2/service/costexplorer/types"
1212
cost "github.com/grafana/cloudcost-exporter/pkg/aws/services/costexplorer"
13+
"github.com/grafana/cloudcost-exporter/pkg/utils"
1314
)
1415

1516
type billing struct {
@@ -94,24 +95,30 @@ func parseBillingData(outputs []*costexplorer.GetCostAndUsageOutput) *BillingDat
9495
return billingData
9596
}
9697

97-
// getRegionFromKey returns the region from the key. If the key is requests, it will return an empty string because there is no region associated with it.
98+
// getRegionFromKey returns the region from the key. Keys without a recognisable
99+
// region prefix are returned as "unknown".
98100
func getRegionFromKey(key string) string {
101+
region := utils.RegionUnknown
99102
if key == "Requests-Tier1" || key == "Requests-Tier2" {
100-
return ""
103+
return region
101104
}
102105

103106
split := strings.Split(key, "-")
104107
if len(split) < 2 {
105-
slog.Warn("Could not find region in key", "key", key)
106-
return ""
108+
return region
107109
}
108110

109111
billingRegion := split[0]
110112
if region, ok := BillingToRegionMap[billingRegion]; ok {
111113
return region
112114
}
113-
slog.Warn("Could not find mapped region", "key", key, "billingRegion", billingRegion)
114-
return ""
115+
116+
// Per AWS S3 documentation, usage types for us-east-1 may appear without a
117+
// region prefix (e.g. "TimedStorage-ByteHrs" instead of "USE1-TimedStorage-ByteHrs").
118+
if billingRegion == "TimedStorage" {
119+
return BillingToRegionMap["USE1"]
120+
}
121+
return region
115122
}
116123

117124
// getComponentFromKey returns the component from the key. If the component does not contain a region, it will return
@@ -125,6 +132,10 @@ func getComponentFromKey(key string) string {
125132
if len(split) < 2 {
126133
return val
127134
}
135+
// Handle known S3 components that appear without a region prefix (no-prefix = us-east-1).
136+
if split[0] == "TimedStorage" {
137+
return "TimedStorage"
138+
}
128139
val = split[1]
129140
// Check to see if the value is a region. If so, set val to empty string to skip the dimension
130141
// Currently this is such a minor part of our bill that it's not worth it.

pkg/aws/client/billing_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,59 @@ func Test_getRegionFromKey(t *testing.T) {
5555
key, want := record[0], record[1]
5656
got := getRegionFromKey(key)
5757
mappedWant := BillingToRegionMap[want]
58+
if want == "unknown" {
59+
mappedWant = "unknown"
60+
}
5861
if mappedWant != got {
5962
t.Fatalf("getRegionFromKey(%s) = %v, want %v", key, got, want)
6063
}
6164
}
6265
}
6366

67+
func Test_getRegionFromKey_unknown(t *testing.T) {
68+
tests := map[string]struct {
69+
key string
70+
want string
71+
}{
72+
"bare Requests-Tier1 has no region": {
73+
key: "Requests-Tier1",
74+
want: "unknown",
75+
},
76+
"bare Requests-Tier2 has no region": {
77+
key: "Requests-Tier2",
78+
want: "unknown",
79+
},
80+
"Global prefix (free-tier credits) has no region": {
81+
key: "Global-Bucket-Hrs-FreeTier",
82+
want: "unknown",
83+
},
84+
"DataTransfer prefix without region code has no region": {
85+
key: "DataTransfer-In-Bytes",
86+
want: "unknown",
87+
},
88+
"DataTransfer-Out prefix without region code has no region": {
89+
key: "DataTransfer-Out-Bytes",
90+
want: "unknown",
91+
},
92+
"completely unrecognised prefix has no region": {
93+
key: "SomeNewPrefix-Bytes",
94+
want: "unknown",
95+
},
96+
"key too short to contain a region": {
97+
key: "NoHyphen",
98+
want: "unknown",
99+
},
100+
}
101+
102+
for name, tt := range tests {
103+
t.Run(name, func(t *testing.T) {
104+
if got := getRegionFromKey(tt.key); got != tt.want {
105+
t.Errorf("getRegionFromKey(%q) = %q, want %q", tt.key, got, tt.want)
106+
}
107+
})
108+
}
109+
}
110+
64111
func TestS3BillingData_AddRegion(t *testing.T) {
65112
type args struct {
66113
key string

pkg/aws/client/compute.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,12 @@ func ClusterNameFromInstance(instance types.Instance) string {
176176
}
177177
return ""
178178
}
179+
180+
// RegionsFromEC2Types converts a slice of EC2 Region types to a slice of region name strings.
181+
func RegionsFromEC2Types(regions []types.Region) []string {
182+
result := make([]string, 0, len(regions))
183+
for _, r := range regions {
184+
result = append(result, *r.RegionName)
185+
}
186+
return result
187+
}

pkg/aws/client/testdata/dimensions.csv

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ EUC1-DataTransfer-Out-Bytes,EUC1,DataTransfer,Out,Bytes,
1010
EUC1-Requests-Tier1,EUC1,Requests-Tier1,Tier1,,
1111
EUC1-Requests-Tier2,EUC1,Requests-Tier2,Tier2,,
1212
EUC1-TimedStorage-ByteHrs,EUC1,TimedStorage,ByteHrs,,
13-
Requests-Tier1,,,,,
14-
Requests-Tier2,,,,,
13+
Requests-Tier1,unknown,,,,
14+
Requests-Tier2,unknown,,,,
1515
USE1-USE2-AWS-Out-Bytes,USE1,,AWS,Out,Bytes
1616
USE2-DataTransfer-Out-Bytes,USE2,DataTransfer,Out,Bytes,
1717
USE2-Requests-Tier1,USE2,Requests-Tier1,Tier1,,
@@ -22,8 +22,8 @@ USW2-Requests-Tier1,USW2,Requests-Tier1,Tier1,,
2222
USW2-Requests-Tier2,USW2,Requests-Tier2,Tier2,,
2323
USW2-TimedStorage-ByteHrs,USW2,TimedStorage,ByteHrs,,
2424
APS1-DataTransfer-In-Bytes,APS1,DataTransfer,In,Bytes,
25-
DataTransfer-In-Bytes,DataTransfer,In,Bytes,,
26-
DataTransfer-Out-Bytes,DataTransfer,Out,Bytes,,
25+
DataTransfer-In-Bytes,unknown,In,Bytes,,
26+
DataTransfer-Out-Bytes,unknown,Out,Bytes,,
2727
USE2-DataTransfer-In-Bytes,USE2,DataTransfer,In,Bytes,
2828
USW2-DataTransfer-In-Bytes,USW2,DataTransfer,In,Bytes,
2929
APS1-USE1-AWS-Out-Bytes,APS1,,AWS,Out,Bytes
@@ -50,4 +50,6 @@ EUN1-USE1-AWS-Out-Bytes,EUN1,,AWS,Out,Bytes
5050
SAE1-USE1-AWS-Out-Bytes,SAE1,,AWS,Out,Bytes
5151
APS2-TimedStorage-ByteHrs,APS2,TimedStorage,ByteHrs,,
5252
EUN1-TimedStorage-ByteHrs,EUN1,TimedStorage,ByteHrs,,
53-
SAE1-TimedStorage-ByteHrs,SAE1,TimedStorage,ByteHrs,,
53+
SAE1-TimedStorage-ByteHrs,SAE1,TimedStorage,ByteHrs,,
54+
TimedStorage-ByteHrs,USE1,TimedStorage,ByteHrs,,
55+
Global-Bucket-Hrs-FreeTier,unknown,Bucket,Hrs,FreeTier,

pkg/aws/ec2/ec2.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ var (
6464

6565
// Collector is a prometheus collector that collects metrics from AWS EKS clusters.
6666
type Collector struct {
67-
Regions []ec2Types.Region
67+
regions []ec2Types.Region
6868
ScrapeInterval time.Duration
6969
computePricingMap *ComputePricingMap
7070
storagePricingMap *StoragePricingMap
@@ -124,7 +124,7 @@ func New(ctx context.Context, config *Config) (*Collector, error) {
124124

125125
return &Collector{
126126
ScrapeInterval: config.ScrapeInterval,
127-
Regions: config.Regions,
127+
regions: config.Regions,
128128
logger: logger,
129129
awsRegionClientMap: config.RegionMap,
130130
computePricingMap: computeMap,
@@ -136,7 +136,7 @@ func New(ctx context.Context, config *Config) (*Collector, error) {
136136
func (c *Collector) Collect(ctx context.Context, ch chan<- prometheus.Metric) error {
137137
c.logger.LogAttrs(ctx, slog.LevelInfo, "calling collect")
138138

139-
numOfRegions := len(c.Regions)
139+
numOfRegions := len(c.regions)
140140

141141
wgInstances := sync.WaitGroup{}
142142
wgInstances.Add(numOfRegions)
@@ -146,7 +146,7 @@ func (c *Collector) Collect(ctx context.Context, ch chan<- prometheus.Metric) er
146146
wgVolumes.Add(numOfRegions)
147147
volumeCh := make(chan []ec2Types.Volume, numOfRegions)
148148

149-
for _, region := range c.Regions {
149+
for _, region := range c.regions {
150150
regionName := *region.RegionName
151151

152152
regionClient, ok := c.awsRegionClientMap[regionName]
@@ -374,6 +374,10 @@ func (c *Collector) Name() string {
374374
return subsystem
375375
}
376376

377+
func (c *Collector) Regions() []string {
378+
return utils.RegionsFromMap(c.awsRegionClientMap)
379+
}
380+
377381
func (c *Collector) Register(_ provider.Registry) error {
378382
return nil
379383
}

pkg/aws/ec2/ec2_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func Test_FetchVolumesData(t *testing.T) {
394394
Times(1)
395395

396396
wg := sync.WaitGroup{}
397-
wg.Add(len(collector.Regions))
397+
wg.Add(len(collector.regions))
398398
ch := make(chan []ec2Types.Volume)
399399
go collector.fetchVolumesData(t.Context(), c, regionName, ch)
400400
go func() {

pkg/aws/elb/elb.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ var (
4040
)
4141

4242
type Collector struct {
43-
Regions []ec2Types.Region
43+
regions []ec2Types.Region
4444
ScrapeInterval time.Duration
4545
pricingMap *ELBPricingMap
4646
awsRegionClientMap map[string]client.Client
@@ -82,7 +82,7 @@ type elbProduct struct {
8282

8383
func New(config *Config) *Collector {
8484
return &Collector{
85-
Regions: config.Regions,
85+
regions: config.Regions,
8686
ScrapeInterval: config.ScrapeInterval,
8787
awsRegionClientMap: config.RegionClients,
8888
logger: config.Logger,
@@ -104,7 +104,7 @@ func (c *Collector) Collect(ctx context.Context, ch chan<- prometheus.Metric) er
104104
c.logger.Info("Starting ELB collection")
105105

106106
if c.shouldScrape() {
107-
if err := c.pricingMap.refresh(ctx, c.awsRegionClientMap, c.Regions); err != nil {
107+
if err := c.pricingMap.refresh(ctx, c.awsRegionClientMap, c.regions); err != nil {
108108
c.logger.Error("Failed to refresh pricing", "error", err)
109109
return err
110110
}
@@ -153,6 +153,10 @@ func (c *Collector) Name() string {
153153
return subsystem
154154
}
155155

156+
func (c *Collector) Regions() []string {
157+
return utils.RegionsFromMap(c.awsRegionClientMap)
158+
}
159+
156160
func (c *Collector) shouldScrape() bool {
157161
return time.Now().After(c.NextScrape)
158162
}

0 commit comments

Comments
 (0)