Skip to content

Commit

Permalink
feat: Add instances regex filters
Browse files Browse the repository at this point in the history
  • Loading branch information
maso7 committed May 11, 2023
1 parent 37d7fee commit 19c817f
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 3 deletions.
14 changes: 13 additions & 1 deletion exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exporter

import (
"context"
"regexp"
"sync"
"sync/atomic"
"time"
Expand All @@ -23,6 +24,7 @@ type Exporter struct {
totalScrapes prometheus.Counter
pricingMetrics map[string]*prometheus.GaugeVec
instances map[string]Instance
instanceRegexes []*regexp.Regexp
awsCfg aws.Config
cache int
nextScrape time.Time
Expand All @@ -45,14 +47,15 @@ type scrapeResult struct {
}

// NewExporter returns a new exporter of AWS EC2 Price metrics.
func NewExporter(pds []string, oss []string, regions []string, lifecycle []string, cache int) (*Exporter, error) {
func NewExporter(pds []string, oss []string, regions []string, lifecycle []string, cache int, instanceRegexes []*regexp.Regexp) (*Exporter, error) {

e := Exporter{
productDescriptions: pds,
operatingSystems: oss,
regions: regions,
lifecycle: lifecycle,
cache: cache,
instanceRegexes: instanceRegexes,
nextScrape: time.Now(),
duration: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "aws_pricing",
Expand Down Expand Up @@ -249,3 +252,12 @@ func contains(elems []string, v string) bool {
}
return false
}

func isMatchAny(regexList []*regexp.Regexp, text string) bool {
for _, regex := range regexList {
if regex.MatchString(text) {
return true
}
}
return false
}
6 changes: 6 additions & 0 deletions exporter/ondemand.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,17 @@ func (e *Exporter) getOnDemandPricing(region string, scrapes chan<- scrapeResult
}

for _, out := range outs {
if !isMatchAny(e.instanceRegexes, out.Product.Attributes["instanceType"]) {
log.Debugf("Skipping instance type: %s", out.Product.Attributes["instanceType"])
continue
}

sku := out.Product.Sku
skuOnDemand := fmt.Sprintf("%s.%s", sku, TermOnDemand)
skuOnDemandPerHour := fmt.Sprintf("%s.%s", skuOnDemand, TermPerHour)

value, err := strconv.ParseFloat(out.Terms.OnDemand[skuOnDemand].PriceDimensions[skuOnDemandPerHour].PricePerUnit["USD"], 64)

if err != nil {
log.WithError(err).Errorf("error while parsing spot price value from API response [region=%s, type=%s]", region, out.Product.Attributes["instanceType"])
atomic.AddUint64(&e.errorCount, 1)
Expand Down
6 changes: 6 additions & 0 deletions exporter/spot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ func (e *Exporter) getSpotPricing(region string, scrapes chan<- scrapeResult) {
break
}
for _, price := range history.SpotPriceHistory {
if !isMatchAny(e.instanceRegexes, string(price.InstanceType)) {
log.Debugf("Skipping instance type: %s", price.InstanceType)
continue
}

value, err := strconv.ParseFloat(*price.SpotPrice, 64)
if err != nil {
log.WithError(err).Errorf("error while parsing spot price value from API response [region=%s, az=%s, type=%s]", region, *price.AvailabilityZone, price.InstanceType)
atomic.AddUint64(&e.errorCount, 1)
}
log.Debugf("Creating new metric: ec2{region=%s, az=%s, instance_type=%s, product_description=%s} = %v.", region, *price.AvailabilityZone, price.InstanceType, price.ProductDescription, value)

scrapes <- scrapeResult{
Name: "ec2",
Value: value,
Expand Down
31 changes: 29 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package main
import (
"context"
"flag"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -24,6 +26,7 @@ var (
regions = flag.String("regions", "", "Comma separated list of AWS regions to get pricing for (defaults to *all*)")
lifecycle = flag.String("lifecycle", "", "Comma separated list of Lifecycles (spot or ondemand) to get pricing for (defaults to *all*)")
cache = flag.Int("cache", 0, "How long should the results be cached, in seconds (defaults to *0*)")
instanceRegexes = flag.String("instance-regexes", "", "Comma separated list of instance type regexes (defaults to *all*)")
)

func init() {
Expand All @@ -38,7 +41,7 @@ func init() {
}

func main() {
log.Infof("Starting AWS EC2 Price exporter. [log-level=%s, regions=%s, product-descriptions=%s, operating-systems=%s, cache=%d, lifecycle=%s]", *rawLevel, *regions, *productDescriptions, *operatingSystems, *cache, *lifecycle)
log.Infof("Starting AWS EC2 Price exporter. [log-level=%s, regions=%s, product-descriptions=%s, operating-systems=%s, cache=%d, lifecycle=%s, instance-regexes=%s]", *rawLevel, *regions, *productDescriptions, *operatingSystems, *cache, *lifecycle, *instanceRegexes)

var reg []string
if len(*regions) == 0 {
Expand Down Expand Up @@ -68,9 +71,20 @@ func main() {
if len(lc) == 0 {
lc = []string{"spot", "ondemand"}
}
instReg := splitAndTrim(*instanceRegexes)
if len(instReg) == 0 {
instReg = []string{".*"}
}

instRegCompiled, err := compileRegexes(instReg)
if err != nil {
fmt.Printf("Error: %s\n", err)
return
}

validateProductDesc(pds)
validateOperatingSystems(oss)
exporter, err := exporter.NewExporter(pds, oss, reg, lc, *cache)
exporter, err := exporter.NewExporter(pds, oss, reg, lc, *cache, instRegCompiled)
if err != nil {
log.Fatal(err)
}
Expand All @@ -81,6 +95,7 @@ func main() {
http.HandleFunc("/", rootHandler)
log.Fatal(http.ListenAndServe(*addr, nil))
}

func splitAndTrim(str string) []string {
if str == "" {
return []string{}
Expand Down Expand Up @@ -125,3 +140,15 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
`))

}

func compileRegexes(regexes []string) ([]*regexp.Regexp, error) {
compiledRegexes := make([]*regexp.Regexp, len(regexes))
for i, r := range regexes {
re, err := regexp.Compile(r)
if err != nil {
return nil, fmt.Errorf("invalid regex %s: %s", r, err)
}
compiledRegexes[i] = re
}
return compiledRegexes, nil
}

0 comments on commit 19c817f

Please sign in to comment.