diff --git a/config/config.go b/config/config.go index 510de3136..dda9d61c1 100644 --- a/config/config.go +++ b/config/config.go @@ -156,6 +156,13 @@ var ( RefreshInterval: model.Duration(5 * time.Minute), } + // DefaultTritonSDConfig is the default Triton SD configuration. + DefaultTritonSDConfig = TritonSDConfig{ + Port: 9163, + RefreshInterval: model.Duration(60 * time.Second), + Version: 1, + } + // DefaultRemoteWriteConfig is the default remote write configuration. DefaultRemoteWriteConfig = RemoteWriteConfig{ RemoteTimeout: model.Duration(30 * time.Second), @@ -437,6 +444,8 @@ type ServiceDiscoveryConfig struct { EC2SDConfigs []*EC2SDConfig `yaml:"ec2_sd_configs,omitempty"` // List of Azure service discovery configurations. AzureSDConfigs []*AzureSDConfig `yaml:"azure_sd_configs,omitempty"` + // List of Triton service discovery configurations. + TritonSDConfigs []*TritonSDConfig `yaml:"triton_sd_configs,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` @@ -1086,6 +1095,42 @@ func (c *AzureSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return checkOverflow(c.XXX, "azure_sd_config") } +// TritonSDConfig is the configuration for Triton based service discovery. +type TritonSDConfig struct { + Account string `yaml:"account"` + DNSSuffix string `yaml:"dns_suffix"` + Endpoint string `yaml:"endpoint"` + Port int `yaml:"port"` + RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` + Version int `yaml:"version"` + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *TritonSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultTritonSDConfig + type plain TritonSDConfig + err := unmarshal((*plain)(c)) + if err != nil { + return err + } + if c.Account == "" { + return fmt.Errorf("Triton SD configuration requires an account") + } + if c.DNSSuffix == "" { + return fmt.Errorf("Triton SD configuration requires a dns_suffix") + } + if c.Endpoint == "" { + return fmt.Errorf("Triton SD configuration requires an endpoint") + } + if c.RefreshInterval <= 0 { + return fmt.Errorf("Triton SD configuration requires RefreshInterval to be a positive integer") + } + return checkOverflow(c.XXX, "triton_sd_config") +} + // RelabelAction is the action to be performed on relabeling. type RelabelAction string diff --git a/config/config_test.go b/config/config_test.go index 31d678622..f772eeb31 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -413,6 +413,33 @@ var expectedConf = &Config{ }, }, }, + { + JobName: "service-triton", + + ScrapeInterval: model.Duration(15 * time.Second), + ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout, + + MetricsPath: DefaultScrapeConfig.MetricsPath, + Scheme: DefaultScrapeConfig.Scheme, + + ServiceDiscoveryConfig: ServiceDiscoveryConfig{ + TritonSDConfigs: []*TritonSDConfig{ + { + + Account: "testAccount", + DNSSuffix: "triton.example.com", + Endpoint: "triton.example.com", + Port: 9163, + RefreshInterval: model.Duration(60 * time.Second), + Version: 1, + TLSConfig: TLSConfig{ + CertFile: "testdata/valid_cert_file", + KeyFile: "testdata/valid_key_file", + }, + }, + }, + }, + }, }, AlertingConfig: AlertingConfig{ AlertmanagerConfigs: []*AlertmanagerConfig{ diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 9f62cb4ca..bcea23faf 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -179,6 +179,18 @@ scrape_configs: - targets: - localhost:9090 +- job_name: service-triton + triton_sd_configs: + - account: 'testAccount' + dns_suffix: 'triton.example.com' + endpoint: 'triton.example.com' + port: 9163 + refresh_interval: 1m + version: 1 + tls_config: + cert_file: testdata/valid_cert_file + key_file: testdata/valid_key_file + alerting: alertmanagers: - scheme: https diff --git a/discovery/discovery.go b/discovery/discovery.go index d81ae6391..883b27b86 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -28,6 +28,7 @@ import ( "github.com/prometheus/prometheus/discovery/gce" "github.com/prometheus/prometheus/discovery/kubernetes" "github.com/prometheus/prometheus/discovery/marathon" + "github.com/prometheus/prometheus/discovery/triton" "github.com/prometheus/prometheus/discovery/zookeeper" "golang.org/x/net/context" ) @@ -106,6 +107,14 @@ func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig) map[string]TargetPro for i, c := range cfg.AzureSDConfigs { app("azure", i, azure.NewDiscovery(c)) } + for i, c := range cfg.TritonSDConfigs { + t, err := triton.New(log.With("sd", "triton"), c) + if err != nil { + log.Errorf("Cannot create Triton discovery: %s", err) + continue + } + app("triton", i, t) + } if len(cfg.StaticConfigs) > 0 { app("static", 0, NewStaticProvider(cfg.StaticConfigs)) } diff --git a/discovery/triton/triton.go b/discovery/triton/triton.go new file mode 100644 index 000000000..b2c0bd916 --- /dev/null +++ b/discovery/triton/triton.go @@ -0,0 +1,169 @@ +// Copyright 2017 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 triton + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/util/httputil" + "golang.org/x/net/context" +) + +const ( + tritonLabel = model.MetaLabelPrefix + "triton_" + tritonLabelMachineId = tritonLabel + "machine_id" + tritonLabelMachineAlias = tritonLabel + "machine_alias" + tritonLabelMachineImage = tritonLabel + "machine_image" + tritonLabelServerId = tritonLabel + "server_id" + namespace = "prometheus" +) + +var ( + refreshFailuresCount = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "prometheus_sd_triton_refresh_failures_total", + Help: "The number of Triton-SD scrape failures.", + }) + refreshDuration = prometheus.NewSummary( + prometheus.SummaryOpts{ + Name: "prometheus_sd_triton_refresh_duration_seconds", + Help: "The duration of a Triton-SD refresh in seconds.", + }) +) + +func init() { + prometheus.MustRegister(refreshFailuresCount) + prometheus.MustRegister(refreshDuration) +} + +type DiscoveryResponse struct { + Containers []struct { + ServerUUID string `json:"server_uuid"` + VMAlias string `json:"vm_alias"` + VMImageUUID string `json:"vm_image_uuid"` + VMUUID string `json:"vm_uuid"` + } `json:"containers"` +} + +// Discovery periodically performs Triton-SD requests. It implements +// the TargetProvider interface. +type Discovery struct { + client *http.Client + interval time.Duration + logger log.Logger + sdConfig *config.TritonSDConfig +} + +// New returns a new Discovery which periodically refreshes its targets. +func New(logger log.Logger, conf *config.TritonSDConfig) (*Discovery, error) { + tls, err := httputil.NewTLSConfig(conf.TLSConfig) + if err != nil { + return nil, err + } + + transport := &http.Transport{TLSClientConfig: tls} + client := &http.Client{Transport: transport} + + return &Discovery{ + client: client, + interval: time.Duration(conf.RefreshInterval), + logger: logger, + sdConfig: conf, + }, nil +} + +// Run implements the TargetProvider interface. +func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { + defer close(ch) + + ticker := time.NewTicker(d.interval) + defer ticker.Stop() + + // Get an initial set right away. + tg, err := d.refresh() + if err != nil { + d.logger.With("err", err).Error("Refreshing targets failed") + } else { + ch <- []*config.TargetGroup{tg} + } + + for { + select { + case <-ticker.C: + tg, err := d.refresh() + if err != nil { + d.logger.With("err", err).Error("Refreshing targets failed") + } else { + ch <- []*config.TargetGroup{tg} + } + case <-ctx.Done(): + return + } + } +} + +func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { + t0 := time.Now() + defer func() { + refreshDuration.Observe(time.Since(t0).Seconds()) + if err != nil { + refreshFailuresCount.Inc() + } + }() + + var endpoint = fmt.Sprintf("https://%s:%d/v%d/discover", d.sdConfig.Endpoint, d.sdConfig.Port, d.sdConfig.Version) + tg = &config.TargetGroup{ + Source: endpoint, + } + + resp, err := d.client.Get(endpoint) + if err != nil { + return tg, fmt.Errorf("an error occurred when requesting targets from the discovery endpoint. %s", err) + } + + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return tg, fmt.Errorf("an error occurred when reading the response body. %s", err) + } + + dr := DiscoveryResponse{} + err = json.Unmarshal(data, &dr) + if err != nil { + return tg, fmt.Errorf("an error occurred unmarshaling the disovery response json. %s", err) + } + + for _, container := range dr.Containers { + labels := model.LabelSet{ + tritonLabelMachineId: model.LabelValue(container.VMUUID), + tritonLabelMachineAlias: model.LabelValue(container.VMAlias), + tritonLabelMachineImage: model.LabelValue(container.VMImageUUID), + tritonLabelServerId: model.LabelValue(container.ServerUUID), + } + addr := fmt.Sprintf("%s.%s:%d", container.VMUUID, d.sdConfig.DNSSuffix, d.sdConfig.Port) + labels[model.AddressLabel] = model.LabelValue(addr) + tg.Targets = append(tg.Targets, labels) + } + + return tg, nil +} diff --git a/discovery/triton/triton_test.go b/discovery/triton/triton_test.go new file mode 100644 index 000000000..bf89e7806 --- /dev/null +++ b/discovery/triton/triton_test.go @@ -0,0 +1,178 @@ +// Copyright 2016 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 triton + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + "time" + + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +var ( + conf = config.TritonSDConfig{ + Account: "testAccount", + DNSSuffix: "triton.example.com", + Endpoint: "127.0.0.1", + Port: 443, + Version: 1, + RefreshInterval: 1, + TLSConfig: config.TLSConfig{InsecureSkipVerify: true}, + } + badconf = config.TritonSDConfig{ + Account: "badTestAccount", + DNSSuffix: "bad.triton.example.com", + Endpoint: "127.0.0.1", + Port: 443, + Version: 1, + RefreshInterval: 1, + TLSConfig: config.TLSConfig{ + InsecureSkipVerify: false, + KeyFile: "shouldnotexist.key", + CAFile: "shouldnotexist.ca", + CertFile: "shouldnotexist.cert", + }, + } + logger = log.Base() +) + +func TestTritonSDNew(t *testing.T) { + td, err := New(logger, &conf) + assert.Nil(t, err) + assert.NotNil(t, td) + assert.NotNil(t, td.client) + assert.NotNil(t, td.interval) + assert.NotNil(t, td.logger) + assert.Equal(t, logger, td.logger, "td.logger equals logger") + assert.NotNil(t, td.sdConfig) + assert.Equal(t, conf.Account, td.sdConfig.Account) + assert.Equal(t, conf.DNSSuffix, td.sdConfig.DNSSuffix) + assert.Equal(t, conf.Endpoint, td.sdConfig.Endpoint) + assert.Equal(t, conf.Port, td.sdConfig.Port) +} + +func TestTritonSDNewBadConfig(t *testing.T) { + td, err := New(logger, &badconf) + assert.NotNil(t, err) + assert.Nil(t, td) +} + +func TestTritonSDRun(t *testing.T) { + var ( + td, err = New(logger, &conf) + ch = make(chan []*config.TargetGroup) + ctx, cancel = context.WithCancel(context.Background()) + ) + + assert.Nil(t, err) + assert.NotNil(t, td) + + go td.Run(ctx, ch) + + select { + case <-time.After(60 * time.Millisecond): + // Expected. + case tgs := <-ch: + t.Fatalf("Unexpected target groups in triton discovery: %s", tgs) + } + + cancel() +} + +func TestTritonSDRefreshNoTargets(t *testing.T) { + tgts := testTritonSDRefresh(t, "{\"containers\":[]}") + assert.Nil(t, tgts) +} + +func TestTritonSDRefreshMultipleTargets(t *testing.T) { + var ( + dstr = `{"containers":[ + { + "server_uuid":"44454c4c-5000-104d-8037-b7c04f5a5131", + "vm_alias":"server01", + "vm_image_uuid":"7b27a514-89d7-11e6-bee6-3f96f367bee7", + "vm_uuid":"ad466fbf-46a2-4027-9b64-8d3cdb7e9072" + }, + { + "server_uuid":"a5894692-bd32-4ca1-908a-e2dda3c3a5e6", + "vm_alias":"server02", + "vm_image_uuid":"a5894692-bd32-4ca1-908a-e2dda3c3a5e6", + "vm_uuid":"7b27a514-89d7-11e6-bee6-3f96f367bee7" + }] + }` + ) + + tgts := testTritonSDRefresh(t, dstr) + assert.NotNil(t, tgts) + assert.Equal(t, 2, len(tgts)) +} + +func TestTritonSDRefreshNoServer(t *testing.T) { + var ( + td, err = New(logger, &conf) + ) + assert.Nil(t, err) + assert.NotNil(t, td) + + tg, rerr := td.refresh() + assert.NotNil(t, rerr) + assert.Contains(t, rerr.Error(), "an error occurred when requesting targets from the discovery endpoint.") + assert.NotNil(t, tg) + assert.Nil(t, tg.Targets) +} + +func testTritonSDRefresh(t *testing.T, dstr string) []model.LabelSet { + var ( + td, err = New(logger, &conf) + s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, dstr) + })) + ) + + defer s.Close() + + u, uperr := url.Parse(s.URL) + assert.Nil(t, uperr) + assert.NotNil(t, u) + + host, strport, sherr := net.SplitHostPort(u.Host) + assert.Nil(t, sherr) + assert.NotNil(t, host) + assert.NotNil(t, strport) + + port, atoierr := strconv.Atoi(strport) + assert.Nil(t, atoierr) + assert.NotNil(t, port) + + td.sdConfig.Port = port + + assert.Nil(t, err) + assert.NotNil(t, td) + + tg, err := td.refresh() + assert.Nil(t, err) + assert.NotNil(t, tg) + + return tg.Targets +}