From 3b546d061f924232f840416e54761c3ec50f26a6 Mon Sep 17 00:00:00 2001 From: Ingo Gottwald Date: Tue, 28 Jun 2016 15:15:37 +0200 Subject: [PATCH] Add support for GCE discovery --- config/config.go | 51 ++++++++++ retrieval/discovery/gce.go | 199 +++++++++++++++++++++++++++++++++++++ retrieval/targetmanager.go | 8 ++ 3 files changed, 258 insertions(+) create mode 100644 retrieval/discovery/gce.go diff --git a/config/config.go b/config/config.go index 3141ca8e69..2fb6f4dd4e 100644 --- a/config/config.go +++ b/config/config.go @@ -133,6 +133,13 @@ var ( RetryInterval: model.Duration(1 * time.Second), } + // DefaultGCESDConfig is the default EC2 SD configuration. + DefaultGCESDConfig = GCESDConfig{ + Port: 80, + TagSeparator: ",", + RefreshInterval: model.Duration(60 * time.Second), + } + // DefaultEC2SDConfig is the default EC2 SD configuration. DefaultEC2SDConfig = EC2SDConfig{ Port: 80, @@ -429,6 +436,8 @@ type ScrapeConfig struct { MarathonSDConfigs []*MarathonSDConfig `yaml:"marathon_sd_configs,omitempty"` // List of Kubernetes service discovery configurations. KubernetesSDConfigs []*KubernetesSDConfig `yaml:"kubernetes_sd_configs,omitempty"` + // List of GCE service discovery configurations. + GCESDConfigs []*GCESDConfig `yaml:"gce_sd_configs,omitempty"` // List of EC2 service discovery configurations. EC2SDConfigs []*EC2SDConfig `yaml:"ec2_sd_configs,omitempty"` // List of Azure service discovery configurations. @@ -846,6 +855,48 @@ func (c *KubernetesSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) er return nil } +// GCESDConfig is the configuration for GCE based service discovery. +type GCESDConfig struct { + // Project: The Google Cloud Project ID + Project string `yaml:"project"` + + // Zone: The zone of the scrape targets. + // If you need to configure multiple zones use multiple gce_sd_configs + Zone string `yaml:"zone"` + + // Filter: Can be used optionally to filter the instance list by other criteria. + // Syntax of this filter string is described here in the filter query parameter section: + // https://cloud.google.com/compute/docs/reference/latest/instances/list + Filter string `yaml:"filter,omitempty"` + + RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` + Port int `yaml:"port"` + TagSeparator string `yaml:"tag_separator,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *GCESDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultGCESDConfig + type plain GCESDConfig + err := unmarshal((*plain)(c)) + if err != nil { + return err + } + if err := checkOverflow(c.XXX, "ec2_sd_config"); err != nil { + return err + } + if c.Project == "" { + return fmt.Errorf("GCE SD configuration requires a project") + } + if c.Zone == "" { + return fmt.Errorf("GCE SD configuration requires a zone") + } + return nil +} + // EC2SDConfig is the configuration for EC2 based service discovery. type EC2SDConfig struct { Region string `yaml:"region"` diff --git a/retrieval/discovery/gce.go b/retrieval/discovery/gce.go new file mode 100644 index 0000000000..0b9116bfad --- /dev/null +++ b/retrieval/discovery/gce.go @@ -0,0 +1,199 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package discovery + +import ( + "fmt" + "net/http" + "strings" + "time" + + "google.golang.org/api/compute/v1" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + + "github.com/prometheus/prometheus/config" +) + +const ( + gceLabel = model.MetaLabelPrefix + "gce_" + gceLabelProject = gceLabel + "project" + gceLabelZone = gceLabel + "zone" + gceLabelNetwork = gceLabel + "network" + gceLabelSubnetwork = gceLabel + "subnetwork" + gceLabelPublicIP = gceLabel + "public_ip" + gceLabelPrivateIP = gceLabel + "private_ip" + gceLabelInstanceName = gceLabel + "instance_name" + gceLabelTags = gceLabel + "tags" + + // Constants for instrumentation. + namespace = "prometheus" +) + +var ( + gceSDScrapesCount = prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "gce_sd_scrapes_total", + Help: "The number of GCE-SD scrapes.", + }) + gceSDScrapeFailuresCount = prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "gce_sd_scrape_failures_total", + Help: "The number of GCE-SD scrape failures.", + }) + gceSDScrapeDuration = prometheus.NewSummary( + prometheus.SummaryOpts{ + Namespace: namespace, + Name: "gce_sd_scrape_duration", + Help: "The duration of a GCE-SD scrape in seconds.", + }) +) + +func init() { + prometheus.MustRegister(gceSDScrapesCount) + prometheus.MustRegister(gceSDScrapeFailuresCount) + prometheus.MustRegister(gceSDScrapeDuration) +} + +// GCEDiscovery periodically performs GCE-SD requests. It implements +// the TargetProvider interface. +type GCEDiscovery struct { + project string + zone string + filter string + client *http.Client + svc *compute.Service + isvc *compute.InstancesService + interval time.Duration + port int + tagSeparator string +} + +// NewGCEDiscovery returns a new GCEDiscovery which periodically refreshes its targets. +func NewGCEDiscovery(conf *config.GCESDConfig) (*GCEDiscovery, error) { + gd := &GCEDiscovery{ + project: conf.Project, + zone: conf.Zone, + filter: conf.Filter, + interval: time.Duration(conf.RefreshInterval), + port: conf.Port, + tagSeparator: conf.TagSeparator, + } + var err error + gd.client, err = google.DefaultClient(oauth2.NoContext, compute.ComputeReadonlyScope) + if err != nil { + return nil, fmt.Errorf("error setting up communication with GCE service: %s", err) + } + gd.svc, err = compute.New(gd.client) + if err != nil { + return nil, fmt.Errorf("error setting up communication with GCE service: %s", err) + } + gd.isvc = compute.NewInstancesService(gd.svc) + return gd, nil +} + +// Run implements the TargetProvider interface. +func (gd *GCEDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { + defer close(ch) + + // Get an initial set right away. + tg, err := gd.refresh() + if err != nil { + log.Error(err) + } else { + ch <- []*config.TargetGroup{tg} + } + + ticker := time.NewTicker(gd.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + tg, err := gd.refresh() + if err != nil { + log.Error(err) + } else { + ch <- []*config.TargetGroup{tg} + } + case <-ctx.Done(): + return + } + } +} + +func (gd *GCEDiscovery) refresh() (tg *config.TargetGroup, err error) { + t0 := time.Now() + defer func() { + gceSDScrapeDuration.Observe(time.Since(t0).Seconds()) + gceSDScrapesCount.Inc() + if err != nil { + gceSDScrapeFailuresCount.Inc() + } + }() + + tg = &config.TargetGroup{ + Source: fmt.Sprintf("GCE_%s_%s", gd.project, gd.zone), + } + + ilc := gd.isvc.List(gd.project, gd.zone) + if len(gd.filter) > 0 { + ilc = ilc.Filter(gd.filter) + } + err = ilc.Pages(nil, func(l *compute.InstanceList) error { + for _, inst := range l.Items { + if len(inst.NetworkInterfaces) == 0 { + continue + } + labels := model.LabelSet{ + gceLabelProject: model.LabelValue(gd.project), + gceLabelZone: model.LabelValue(inst.Zone), + gceLabelInstanceName: model.LabelValue(inst.Name), + } + priIface := inst.NetworkInterfaces[0] + labels[gceLabelNetwork] = model.LabelValue(priIface.Network) + labels[gceLabelSubnetwork] = model.LabelValue(priIface.Subnetwork) + labels[gceLabelPrivateIP] = model.LabelValue(priIface.NetworkIP) + addr := fmt.Sprintf("%s:%d", priIface.NetworkIP, gd.port) + labels[model.AddressLabel] = model.LabelValue(addr) + + if inst.Tags != nil && len(inst.Tags.Items) > 0 { + // We surround the separated list with the separator as well. This way regular expressions + // in relabeling rules don't have to consider tag positions. + tags := gd.tagSeparator + strings.Join(inst.Tags.Items, gd.tagSeparator) + gd.tagSeparator + labels[gceLabelTags] = model.LabelValue(tags) + } + + if len(priIface.AccessConfigs) > 0 { + ac := priIface.AccessConfigs[0] + if ac.Type == "ONE_TO_ONE_NAT" { + labels[gceLabelPublicIP] = model.LabelValue(ac.NatIP) + } + } + tg.Targets = append(tg.Targets, labels) + } + return nil + }) + if err != nil { + return tg, fmt.Errorf("error retrieving scrape targets from gce: %s", err) + } + return tg, nil +} diff --git a/retrieval/targetmanager.go b/retrieval/targetmanager.go index 1aa7a20f4c..30dfbe8ab1 100644 --- a/retrieval/targetmanager.go +++ b/retrieval/targetmanager.go @@ -408,6 +408,14 @@ func providersFromConfig(cfg *config.ScrapeConfig) map[string]TargetProvider { for i, c := range cfg.EC2SDConfigs { app("ec2", i, discovery.NewEC2Discovery(c)) } + for i, c := range cfg.GCESDConfigs { + gced, err := discovery.NewGCEDiscovery(c) + if err != nil { + log.Errorf("Cannot initialize GCE discovery: %s", err) + continue + } + app("gce", i, gced) + } for i, c := range cfg.AzureSDConfigs { app("azure", i, discovery.NewAzureDiscovery(c)) }