diff --git a/README.md b/README.md index 340dd95..899a99b 100644 --- a/README.md +++ b/README.md @@ -53,41 +53,21 @@ fastly-exporter -token XXX This will collect real-time stats for all Fastly services visible to your token, and make them available as Prometheus metrics on 127.0.0.1:8080/metrics. -### Advanced - -``` -USAGE - fastly-exporter [flags] - -FLAGS - -api-refresh 1m0s how often to poll api.fastly.com for updated service metadata - -api-timeout 15s HTTP client timeout for api.fastly.com requests (5–60s) - -debug false Log debug information - -endpoint http://127.0.0.1:8080/metrics Prometheus /metrics endpoint - -name-exclude-regex ... if set, ignore any service whose name matches this regex - -name-include-regex ... if set, only include services whose names match this regex - -namespace fastly Prometheus namespace - -rt-timeout 45s HTTP client timeout for rt.fastly.com requests (45–120s) - -service ... if set, only include this service ID (repeatable) - -shard ... if set, only include services whose hashed IDs modulo m equal n-1 (format 'n/m') - -subsystem rt Prometheus subsystem - -token ... Fastly API token (required; also via FASTLY_API_TOKEN) - -version false print version information and exit -``` +### Filtering services By default, all services available to your token will be exported. You can -specify an explicit set of service IDs by using the `-service xxx` flag. -(Service IDs are available at the top of your [Fastly dashboard][db].) You can -also include only those services whose name matches a regex by using the -`-name-include-regex '^Production'` flag, or reject any service whose name -matches a regex by using the `-name-exclude-regex '.*TEST.*'` flag. +specify an explicit set of service IDs to export by using the `-service xxx` +flag. (Service IDs are available at the top of your [Fastly dashboard][db].) You +can also include only those services whose name matches a regex by using the +`-service-whitelist '^Production'` flag, or skip any service whose name matches +a regex by using the `-service-blacklist '.*TEST.*'` flag. [db]: https://manage.fastly.com/services/all For tokens with access to a lot of services, it's possible to "shard" the -services among different instances of the fastly-exporter by using the `-shard` -flag. For example, to shard all services between 3 exporters, you would start -each exporter as +services among different instances of the fastly-exporter by using the +`-service-shard` flag. For example, to shard all services between 3 exporters, +you would start each exporter as ``` fastly-exporter [common flags] -shard 1/3 @@ -95,11 +75,24 @@ fastly-exporter [common flags] -shard 2/3 fastly-exporter [common flags] -shard 3/3 ``` -Flags which restrict the services that are exported combine with AND semantics. -That is, `-service A -service B -name-include-regex 'Foo'` would only export -data for service A and/or B if their names also matched "Foo". Or, specifying -`-name-include-regex 'Prod' -name-exclude-regex '^test-'` would only export data -for services whose names contained "Prod" and did not start with "test-". +### Filtering exported metrics + +By default, all metrics provided by the Fastly real-time stats API are exported +as Prometheus metrics. You can export only those metrics whose name matches a +regex by using the `-metric-whitelist bytes_total$` flag, or elide any metric +whose name matches a regex by using the `-metric-blacklist imgopto` flag. + +### Filter semantics + +All flags that restrict services or metrics are repeatable. Repeating the same +flag causes its condition to be combined with OR semantics. For example, +`-service A -service B` would include both services A and B (but not service C). +Or, `-service-blacklist Test -service-blacklist Staging` would skip any service +whose name contained Test or Staging. + +Different flags (for the same filter target) combine with AND semantics. For +example, `-metric-whitelist 'bytes_total$' -metric-blacklist imgopto` would only +export metrics whose names ended in bytes_total, but didn't include imgopto. ### Docker diff --git a/cmd/fastly-exporter/main.go b/cmd/fastly-exporter/main.go index b7e04ca..91ce05a 100644 --- a/cmd/fastly-exporter/main.go +++ b/cmd/fastly-exporter/main.go @@ -9,7 +9,6 @@ import ( "net/url" "os" "os/signal" - "regexp" "strconv" "strings" "time" @@ -18,6 +17,7 @@ import ( "github.com/go-kit/kit/log/level" "github.com/oklog/run" "github.com/peterbourgon/fastly-exporter/pkg/api" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/peterbourgon/fastly-exporter/pkg/prom" "github.com/peterbourgon/fastly-exporter/pkg/rt" "github.com/peterbourgon/usage" @@ -30,14 +30,17 @@ var programVersion = "dev" func main() { fs := flag.NewFlagSet("fastly-exporter", flag.ExitOnError) var ( - token = fs.String("token", "", "Fastly API token (required; also via FASTLY_API_TOKEN)") - addr = fs.String("endpoint", "http://127.0.0.1:8080/metrics", "Prometheus /metrics endpoint") - namespace = fs.String("namespace", "fastly", "Prometheus namespace") - subsystem = fs.String("subsystem", "rt", "Prometheus subsystem") - serviceIDs = stringslice{} - includeStr = fs.String("name-include-regex", "", "if set, only include services whose names match this regex") - excludeStr = fs.String("name-exclude-regex", "", "if set, ignore any service whose name matches this regex") - shard = fs.String("shard", "", "if set, only include services whose hashed IDs modulo m equal n-1 (format 'n/m')") + token = fs.String("token", "", "Fastly API token (required; also via FASTLY_API_TOKEN)") + addr = fs.String("endpoint", "http://127.0.0.1:8080/metrics", "Prometheus /metrics endpoint") + namespace = fs.String("namespace", "fastly", "Prometheus namespace") + subsystem = fs.String("subsystem", "rt", "Prometheus subsystem") + serviceShard = fs.String("service-shard", "", "if set, only include services whose hashed IDs modulo m equal n-1 (format 'n/m')") + serviceIDs = stringslice{} + serviceWhitelist = stringslice{} + serviceBlacklist = stringslice{} + metricWhitelist = stringslice{} + metricBlacklist = stringslice{} + apiRefresh = fs.Duration("api-refresh", time.Minute, "how often to poll api.fastly.com for updated service metadata (15s–10m)") apiTimeout = fs.Duration("api-timeout", 15*time.Second, "HTTP client timeout for api.fastly.com requests (5–60s)") rtTimeout = fs.Duration("rt-timeout", 45*time.Second, "HTTP client timeout for rt.fastly.com requests (45–120s)") @@ -45,6 +48,10 @@ func main() { versionFlag = fs.Bool("version", false, "print version information and exit") ) fs.Var(&serviceIDs, "service", "if set, only include this service ID (repeatable)") + fs.Var(&serviceWhitelist, "service-whitelist", "if set, only include services whose names match this regex (repeatable)") + fs.Var(&serviceBlacklist, "service-blacklist", "if set, don't include services whose names match this regex (repeatable)") + fs.Var(&metricWhitelist, "metric-whitelist", "if set, only export metrics whose names match this regex (repeatable)") + fs.Var(&metricBlacklist, "metric-blacklist", "if set, don't export metrics whose names match this regex (repeatable)") fs.Usage = usage.For(fs, "fastly-exporter [flags]") fs.Parse(os.Args[1:]) @@ -108,50 +115,72 @@ func main() { } } - var include, exclude *regexp.Regexp + var serviceNameFilter filter.Filter { - var err error - if *includeStr != "" { - if include, err = regexp.Compile(*includeStr); err != nil { - level.Error(logger).Log("err", "-name-include-regex invalid", "msg", err) + for _, expr := range serviceWhitelist { + if err := serviceNameFilter.Whitelist(expr); err != nil { + level.Error(logger).Log("err", "invalid -service-whitelist", "msg", err) + os.Exit(1) + } + level.Info(logger).Log("filter", "services", "type", "name whitelist", "expr", expr) + } + for _, expr := range serviceBlacklist { + if err := serviceNameFilter.Blacklist(expr); err != nil { + level.Error(logger).Log("err", "invalid -service-blacklist", "msg", err) + os.Exit(1) + } + level.Info(logger).Log("filter", "services", "type", "name blacklist", "expr", expr) + } + } + + var metricNameFilter filter.Filter + { + for _, expr := range metricWhitelist { + if err := metricNameFilter.Whitelist(expr); err != nil { + level.Error(logger).Log("err", "invalid -metric-whitelist", "msg", err) os.Exit(1) } + level.Info(logger).Log("filter", "metrics", "type", "name whitelist", "expr", expr) + } - if *excludeStr != "" { - if exclude, err = regexp.Compile(*excludeStr); err != nil { - level.Error(logger).Log("err", "-name-exclude-regex invalid", "msg", err) + for _, expr := range metricBlacklist { + if err := metricNameFilter.Blacklist(expr); err != nil { + level.Error(logger).Log("err", "invalid -metric-blacklist", "msg", err) os.Exit(1) } + level.Info(logger).Log("filter", "metrics", "type", "name blacklist", "expr", expr) } } var shardN, shardM uint64 { - if *shard != "" { - toks := strings.SplitN(*shard, "/", 2) + if *serviceShard != "" { + toks := strings.SplitN(*serviceShard, "/", 2) if len(toks) != 2 { - level.Error(logger).Log("err", "-shard must be of the format 'n/m'") + level.Error(logger).Log("err", "-service-shard must be of the format 'n/m'") os.Exit(1) } var err error shardN, err = strconv.ParseUint(toks[0], 10, 64) if err != nil { - level.Error(logger).Log("err", "-shard must be of the format 'n/m'") + level.Error(logger).Log("err", "-service-shard must be of the format 'n/m'") os.Exit(1) } if shardN <= 0 { - level.Error(logger).Log("err", "first part of -shard flag should be greater than zero") + level.Error(logger).Log("err", "first part of -service-shard flag should be greater than zero") os.Exit(1) } shardM, err = strconv.ParseUint(toks[1], 10, 64) if err != nil { - level.Error(logger).Log("err", "-shard must be of the format 'n/m'") + level.Error(logger).Log("err", "-service-shard must be of the format 'n/m'") os.Exit(1) } if shardN > shardM { - level.Error(logger).Log("err", fmt.Sprintf("-shard with n=%d m=%d is invalid: n must be less than or equal to m", shardN, shardM)) + level.Error(logger).Log("err", fmt.Sprintf("-service-shard with n=%d m=%d is invalid: n must be less than or equal to m", shardN, shardM)) os.Exit(1) } + level.Info(logger).Log("filter", "services", "type", "by shard", "n", shardN, "m", shardM) + } } @@ -163,7 +192,7 @@ func main() { var metrics *prom.Metrics { var err error - metrics, err = prom.NewMetrics(*namespace, *subsystem, registry) + metrics, err = prom.NewMetrics(*namespace, *subsystem, metricNameFilter, registry) if err != nil { level.Error(logger).Log("err", err) os.Exit(1) @@ -177,25 +206,17 @@ func main() { var apiCacheOptions []api.CacheOption { - apiCacheOptions = append(apiCacheOptions, api.WithLogger(apiLogger)) + apiCacheOptions = append(apiCacheOptions, + api.WithLogger(apiLogger), + api.WithNameFilter(serviceNameFilter), + ) if len(serviceIDs) > 0 { - level.Info(apiLogger).Log("filtering_on", "explicit service IDs", "count", len(serviceIDs)) + level.Info(logger).Log("filter", "services", "type", "explicit service IDs", "count", len(serviceIDs)) apiCacheOptions = append(apiCacheOptions, api.WithExplicitServiceIDs(serviceIDs...)) } - if include != nil { - level.Info(apiLogger).Log("filtering_on", "service name include regex", "regex", include.String()) - apiCacheOptions = append(apiCacheOptions, api.WithNameIncluding(include)) - } - - if exclude != nil { - level.Info(apiLogger).Log("filtering_on", "service name exclude regex", "regex", exclude.String()) - apiCacheOptions = append(apiCacheOptions, api.WithNameExcluding(exclude)) - } - if shardM > 0 { - level.Info(apiLogger).Log("filtering_on", "shard allocation", "shard", *shard, "n", shardN, "m", shardM) apiCacheOptions = append(apiCacheOptions, api.WithShard(shardN, shardM)) } } diff --git a/pkg/api/cache.go b/pkg/api/cache.go index 3ca88fa..e550438 100644 --- a/pkg/api/cache.go +++ b/pkg/api/cache.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "regexp" "sort" "sync" "time" @@ -12,6 +11,7 @@ import ( "github.com/cespare/xxhash" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/pkg/errors" ) @@ -32,12 +32,11 @@ type Service struct { // Cache polls api.fastly.com/service to keep metadata about // one or more service IDs up-to-date. type Cache struct { - token string - whitelist stringset - include *regexp.Regexp - exclude *regexp.Regexp - shard shardSlice - logger log.Logger + token string + serviceIDs stringSet + nameFilter filter.Filter + shard shardSlice + logger log.Logger mtx sync.RWMutex services map[string]Service @@ -65,20 +64,13 @@ type CacheOption func(*Cache) // provided service IDs. By default, all service IDs available to the provided // token are allowed. func WithExplicitServiceIDs(ids ...string) CacheOption { - return func(c *Cache) { c.whitelist = newStringSet(ids) } + return func(c *Cache) { c.serviceIDs = newStringSet(ids) } } -// WithNameIncluding restricts the cache to fetch metadata only for the services -// whose names match the provided regexp. By default, no name filtering occurs. -func WithNameIncluding(re *regexp.Regexp) CacheOption { - return func(c *Cache) { c.include = re } -} - -// WithNameExcluding restricts the cache to fetch metadata only for the services -// whose names do not match the provided regexp. By default, no name filtering -// occurs. -func WithNameExcluding(re *regexp.Regexp) CacheOption { - return func(c *Cache) { c.exclude = re } +// WithNameFilter restricts the cache to fetch metadata only for the services +// whose names pass the provided filter. By default, no name filtering occurs. +func WithNameFilter(f filter.Filter) CacheOption { + return func(c *Cache) { c.nameFilter = f } } // WithShard restricts the cache to fetch metadata only for those services whose @@ -139,18 +131,13 @@ func (c *Cache) Refresh(client HTTPClient) error { "service_version", s.Version, )) - if reject := !c.whitelist.empty() && !c.whitelist.has(s.ID); reject { - debug.Log("result", "rejected", "reason", "not in service ID whitelist") - continue - } - - if reject := c.include != nil && !c.include.MatchString(s.Name); reject { - debug.Log("result", "rejected", "reason", "service name didn't match include regex") + if reject := !c.serviceIDs.empty() && !c.serviceIDs.has(s.ID); reject { + debug.Log("result", "rejected", "reason", "service ID not explicitly allowed") continue } - if reject := c.exclude != nil && c.exclude.MatchString(s.Name); reject { - debug.Log("result", "rejected", "reason", "service name matched exclude regex") + if reject := !c.nameFilter.Allow(s.Name); reject { + debug.Log("result", "rejected", "reason", "service name rejected by name filter") continue } @@ -208,21 +195,21 @@ func (c *Cache) Metadata(id string) (name string, version int, found bool) { // // -type stringset map[string]struct{} +type stringSet map[string]struct{} -func newStringSet(initial []string) stringset { - ss := stringset{} +func newStringSet(initial []string) stringSet { + ss := stringSet{} for _, s := range initial { ss[s] = struct{}{} } return ss } -func (ss stringset) empty() bool { +func (ss stringSet) empty() bool { return len(ss) == 0 } -func (ss stringset) has(s string) bool { +func (ss stringSet) has(s string) bool { _, ok := ss[s] return ok } diff --git a/pkg/api/cache_test.go b/pkg/api/cache_test.go index bc03290..7079cb3 100644 --- a/pkg/api/cache_test.go +++ b/pkg/api/cache_test.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" "net/http/httptest" - "regexp" "testing" "github.com/google/go-cmp/cmp" "github.com/peterbourgon/fastly-exporter/pkg/api" + "github.com/peterbourgon/fastly-exporter/pkg/filter" ) func TestCache(t *testing.T) { @@ -43,47 +43,47 @@ func TestCache(t *testing.T) { }, { name: "exact name include match", - options: []api.CacheOption{api.WithNameIncluding(regexp.MustCompile(`^` + s1.Name + `$`))}, + options: []api.CacheOption{api.WithNameFilter(filterWhitelist(`^` + s1.Name + `$`))}, want: []api.Service{s1}, }, { name: "partial name include match", - options: []api.CacheOption{api.WithNameIncluding(regexp.MustCompile(`mmy`))}, + options: []api.CacheOption{api.WithNameFilter(filterWhitelist(`mmy`))}, want: []api.Service{s2}, }, { name: "generous name include match", - options: []api.CacheOption{api.WithNameIncluding(regexp.MustCompile(`.*e.*`))}, + options: []api.CacheOption{api.WithNameFilter(filterWhitelist(`.*e.*`))}, want: []api.Service{s1, s2}, }, { name: "no name include match", - options: []api.CacheOption{api.WithNameIncluding(regexp.MustCompile(`not found`))}, + options: []api.CacheOption{api.WithNameFilter(filterWhitelist(`not found`))}, want: []api.Service{}, }, { name: "exact name exclude match", - options: []api.CacheOption{api.WithNameExcluding(regexp.MustCompile(`^` + s1.Name + `$`))}, + options: []api.CacheOption{api.WithNameFilter(filterBlacklist(`^` + s1.Name + `$`))}, want: []api.Service{s2}, }, { name: "partial name exclude match", - options: []api.CacheOption{api.WithNameExcluding(regexp.MustCompile(`mmy`))}, + options: []api.CacheOption{api.WithNameFilter(filterBlacklist(`mmy`))}, want: []api.Service{s1}, }, { name: "generous name exclude match", - options: []api.CacheOption{api.WithNameExcluding(regexp.MustCompile(`.*e.*`))}, + options: []api.CacheOption{api.WithNameFilter(filterBlacklist(`.*e.*`))}, want: []api.Service{}, }, { name: "no name exclude match", - options: []api.CacheOption{api.WithNameExcluding(regexp.MustCompile(`not found`))}, + options: []api.CacheOption{api.WithNameFilter(filterBlacklist(`not found`))}, want: []api.Service{s1, s2}, }, { name: "name exclude and include", - options: []api.CacheOption{api.WithNameIncluding(regexp.MustCompile(`.*e.*`)), api.WithNameExcluding(regexp.MustCompile(`mmy`))}, + options: []api.CacheOption{api.WithNameFilter(filterWhitelistBlacklist(`.*e.*`, `mmy`))}, want: []api.Service{s1}, }, { @@ -140,6 +140,22 @@ func TestCache(t *testing.T) { } } +func filterWhitelist(w string) (f filter.Filter) { + f.Whitelist(w) + return f +} + +func filterBlacklist(b string) (f filter.Filter) { + f.Blacklist(b) + return f +} + +func filterWhitelistBlacklist(w, b string) (f filter.Filter) { + f.Whitelist(w) + f.Blacklist(b) + return f +} + type fixedResponseClient struct { code int response string diff --git a/pkg/filter/doc.go b/pkg/filter/doc.go new file mode 100644 index 0000000..7ba6a76 --- /dev/null +++ b/pkg/filter/doc.go @@ -0,0 +1,2 @@ +// Package filter provides whitelist- and blacklist-based string filtering. +package filter diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 0000000..a3fceb1 --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,66 @@ +package filter + +import "regexp" + +// Filter collects whitelist and blacklist expressions, and allows callers to +// check if a given string should be permitted. The zero value of a filter type +// is useful and permits all strings. +type Filter struct { + whitelist []*regexp.Regexp + blacklist []*regexp.Regexp +} + +// Whitelist adds a regular expression to the whitelist. If the whitelist is +// non-empty, strings must match at least one whitelist expression in order to be +// permitted. +func (f *Filter) Whitelist(expr string) error { + re, err := regexp.Compile(expr) + if err != nil { + return err + } + + f.whitelist = append(f.whitelist, re) + return nil +} + +// Blacklist adds a regular expression to the blacklist. If a string matches any +// blacklist expression, it is not permitted. +func (f *Filter) Blacklist(expr string) error { + re, err := regexp.Compile(expr) + if err != nil { + return err + } + + f.blacklist = append(f.blacklist, re) + return nil +} + +// Allow checks if the provided string is permitted, according to the current +// set of whitelist and blacklist expressions. +func (f *Filter) Allow(s string) (allowed bool) { + return f.passWhitelist(s) && f.passBlacklist(s) +} + +func (f *Filter) passWhitelist(s string) bool { + if len(f.whitelist) <= 0 { + return true // default pass + } + + for _, re := range f.whitelist { + if re.MatchString(s) { + return true + } + } + + return false +} + +func (f *Filter) passBlacklist(s string) bool { + for _, re := range f.blacklist { + if re.MatchString(s) { + return false + } + } + + return true +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go new file mode 100644 index 0000000..e03a027 --- /dev/null +++ b/pkg/filter/filter_test.go @@ -0,0 +1,113 @@ +package filter_test + +import ( + "testing" + + "github.com/peterbourgon/fastly-exporter/pkg/filter" +) + +func TestFilter(t *testing.T) { + t.Parallel() + + for _, testcase := range []struct { + name string + whitelist []string + blacklist []string + inputs map[string]bool + }{ + { + name: "default allow", + inputs: map[string]bool{ + "anything": true, + "": true, + }, + }, + { + name: "single whitelist", + whitelist: []string{"foo"}, + inputs: map[string]bool{ + "foo": true, + "bar": false, + "": false, + }, + }, + { + name: "multiple whitelist", + whitelist: []string{"foo", "bar"}, + inputs: map[string]bool{ + "foo": true, + "bar": true, + "baz": false, + "": false, + }, + }, + { + name: "single blacklist", + blacklist: []string{"foo"}, + inputs: map[string]bool{ + "foo": false, + "bar": true, + "": true, + }, + }, + { + name: "multiple blacklist", + blacklist: []string{"foo", "bar"}, + inputs: map[string]bool{ + "foo": false, + "bar": false, + "baz": true, + "": true, + }, + }, + { + name: "whitelist and blacklist", + whitelist: []string{"foo", "bar"}, + blacklist: []string{"baz", "qux"}, + inputs: map[string]bool{ + "foo": true, + "foo bar": true, + "some bar blah": true, + "foo bar baz": false, + "bar baz": false, + "baz": false, + "fo ba": false, + "": false, + }, + }, + { + name: "actual regex", + whitelist: []string{"[123]xx"}, + blacklist: []string{"bad$"}, + inputs: map[string]bool{ + "1xx": true, + "2xx_ok": true, + "3xx_bad": false, + "4xx": false, + "5xx": false, + "bad_2xx": true, + }, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + var f filter.Filter + for _, s := range testcase.whitelist { + if err := f.Whitelist(s); err != nil { + t.Fatalf("Whitelist(%s): %v", s, err) + } + } + for _, s := range testcase.blacklist { + if err := f.Blacklist(s); err != nil { + t.Fatalf("Blacklist(%s): %v", s, err) + } + } + for input, want := range testcase.inputs { + if have := f.Allow(input); want != have { + t.Errorf("Allow(%q): want %v, have %v", input, want, have) + } + } + }) + } +} diff --git a/pkg/prom/metrics.go b/pkg/prom/metrics.go index da460ca..11bd3d2 100644 --- a/pkg/prom/metrics.go +++ b/pkg/prom/metrics.go @@ -3,7 +3,9 @@ package prom import ( "fmt" "reflect" + "regexp" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" ) @@ -102,7 +104,7 @@ type Metrics struct { // NewMetrics returns a usable set of Prometheus metrics // that have been registered to the provided registerer. -func NewMetrics(namespace, subsystem string, r prometheus.Registerer) (*Metrics, error) { +func NewMetrics(namespace, subsystem string, nameFilter filter.Filter, r prometheus.Registerer) (*Metrics, error) { var m Metrics m.RealtimeAPIRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{Namespace: namespace, Subsystem: subsystem, Name: "realtime_api_requests_total", @@ -444,6 +446,9 @@ func NewMetrics(namespace, subsystem string, r prometheus.Registerer) (*Metrics, if !ok { panic(fmt.Sprintf("programmer error: field %d/%d in prom.Metrics isn't a prometheus.Collector", i+1, v.NumField())) } + if name := getName(c); !nameFilter.Allow(name) { + continue + } if err := r.Register(c); err != nil { return nil, errors.Wrapf(err, "error registering metric %d/%d", i+1, v.NumField()) } @@ -451,3 +456,16 @@ func NewMetrics(namespace, subsystem string, r prometheus.Registerer) (*Metrics, return &m, nil } + +var descNameRegex = regexp.MustCompile(`fqName: "([^"]+)"`) + +func getName(c prometheus.Collector) string { + d := make(chan *prometheus.Desc, 1) + c.Describe(d) + desc := (<-d).String() + matches := descNameRegex.FindAllStringSubmatch(desc, -1) + if len(matches) == 1 && len(matches[0]) == 2 { + return matches[0][1] + } + return "" +} diff --git a/pkg/prom/metrics_test.go b/pkg/prom/metrics_test.go index a4483c4..a16261a 100644 --- a/pkg/prom/metrics_test.go +++ b/pkg/prom/metrics_test.go @@ -3,31 +3,33 @@ package prom_test import ( "testing" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/peterbourgon/fastly-exporter/pkg/prom" "github.com/prometheus/client_golang/prometheus" ) func TestRegistration(t *testing.T) { var ( - namespace = "namespace" - subsystem = "subsystem" - registry = prometheus.NewRegistry() + namespace = "namespace" + subsystem = "subsystem" + nameFilter = filter.Filter{} // allow all + registry = prometheus.NewRegistry() ) { - _, err := prom.NewMetrics(namespace, subsystem, registry) + _, err := prom.NewMetrics(namespace, subsystem, nameFilter, registry) if err != nil { t.Errorf("unexpected error on first construction: %v", err) } } { - _, err := prom.NewMetrics(namespace, subsystem, registry) + _, err := prom.NewMetrics(namespace, subsystem, nameFilter, registry) if err == nil { t.Error("unexpected success on second construction") } } { - _, err := prom.NewMetrics("alt"+namespace, subsystem, registry) + _, err := prom.NewMetrics("alt"+namespace, subsystem, nameFilter, registry) if err != nil { t.Errorf("unexpected error on third, alt-namespace construction: %v", err) } diff --git a/pkg/rt/manager_test.go b/pkg/rt/manager_test.go index ff5ed44..6cb8831 100644 --- a/pkg/rt/manager_test.go +++ b/pkg/rt/manager_test.go @@ -9,6 +9,7 @@ import ( "github.com/go-kit/kit/log/level" "github.com/google/go-cmp/cmp" "github.com/peterbourgon/fastly-exporter/pkg/api" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/peterbourgon/fastly-exporter/pkg/prom" "github.com/peterbourgon/fastly-exporter/pkg/rt" "github.com/prometheus/client_golang/prometheus" @@ -22,7 +23,7 @@ func TestManager(t *testing.T) { s3 = api.Service{ID: "3a3b3c", Name: "service 3", Version: 3} client = newMockRealtimeClient(`{}`) token = "irrelevant-token" - metrics, _ = prom.NewMetrics("namespace", "subsystem", prometheus.NewRegistry()) + metrics, _ = prom.NewMetrics("namespace", "subsystem", filter.Filter{}, prometheus.NewRegistry()) logbuf = &bytes.Buffer{} logger = log.NewLogfmtLogger(logbuf) options = []rt.SubscriberOption{rt.WithMetadataProvider(cache)} diff --git a/pkg/rt/subscriber_test.go b/pkg/rt/subscriber_test.go index 04b7995..1fb6593 100644 --- a/pkg/rt/subscriber_test.go +++ b/pkg/rt/subscriber_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/peterbourgon/fastly-exporter/pkg/api" + "github.com/peterbourgon/fastly-exporter/pkg/filter" "github.com/peterbourgon/fastly-exporter/pkg/prom" "github.com/peterbourgon/fastly-exporter/pkg/rt" "github.com/prometheus/client_golang/prometheus" @@ -17,7 +18,8 @@ func TestSubscriberFixture(t *testing.T) { namespace = "testspace" subsystem = "testsystem" registry = prometheus.NewRegistry() - metrics, _ = prom.NewMetrics(namespace, subsystem, registry) + nameFilter = filter.Filter{} + metrics, _ = prom.NewMetrics(namespace, subsystem, nameFilter, registry) ) var ( @@ -54,7 +56,7 @@ func TestSubscriberNoData(t *testing.T) { var ( client = newMockRealtimeClient(`{"Error": "No data available, please retry"}`, `{}`) registry = prometheus.NewRegistry() - metrics, _ = prom.NewMetrics("ns", "ss", registry) + metrics, _ = prom.NewMetrics("ns", "ss", filter.Filter{}, registry) processed = make(chan struct{}, 100) postprocess = func() { processed <- struct{}{} } options = []rt.SubscriberOption{rt.WithPostprocess(postprocess)} @@ -79,7 +81,7 @@ func TestUserAgent(t *testing.T) { var ( client = newMockRealtimeClient(`{}`) userAgent = "Some user agent string" - metrics, _ = prom.NewMetrics("ns", "ss", prometheus.NewRegistry()) + metrics, _ = prom.NewMetrics("ns", "ss", filter.Filter{}, prometheus.NewRegistry()) processed = make(chan struct{}) postprocess = func() { close(processed) } options = []rt.SubscriberOption{rt.WithUserAgent(userAgent), rt.WithPostprocess(postprocess)} @@ -97,7 +99,7 @@ func TestUserAgent(t *testing.T) { func TestBadTokenNoSpam(t *testing.T) { var ( client = &countingRealtimeClient{code: 403, response: `{"Error": "unauthorized"}`} - metrics, _ = prom.NewMetrics("namespace", "subsystem", prometheus.NewRegistry()) + metrics, _ = prom.NewMetrics("namespace", "subsystem", filter.Filter{}, prometheus.NewRegistry()) subscriber = rt.NewSubscriber(client, "presumably bad token", "service ID", metrics) ) go subscriber.Run(context.Background())