diff --git a/config/config_test.go b/config/config_test.go index 70ed634fa..85234dd27 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -34,6 +34,7 @@ import ( "github.com/prometheus/prometheus/discovery/dns" "github.com/prometheus/prometheus/discovery/dockerswarm" "github.com/prometheus/prometheus/discovery/ec2" + "github.com/prometheus/prometheus/discovery/eureka" "github.com/prometheus/prometheus/discovery/file" "github.com/prometheus/prometheus/discovery/hetzner" "github.com/prometheus/prometheus/discovery/kubernetes" @@ -677,6 +678,22 @@ var expectedConf = &Config{ }, }, }, + { + JobName: "service-eureka", + + HonorTimestamps: true, + ScrapeInterval: model.Duration(15 * time.Second), + ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout, + + MetricsPath: DefaultScrapeConfig.MetricsPath, + Scheme: DefaultScrapeConfig.Scheme, + + ServiceDiscoveryConfigs: discovery.Configs{&eureka.SDConfig{ + Server: "http://eureka.example.com:8761/eureka", + RefreshInterval: model.Duration(30 * time.Second), + }, + }, + }, }, AlertingConfig: AlertingConfig{ AlertmanagerConfigs: []*AlertmanagerConfig{ @@ -996,6 +1013,14 @@ var expectedErrors = []struct { filename: "hetzner_role.bad.yml", errMsg: "unknown role", }, + { + filename: "eureka_no_server.bad.yml", + errMsg: "empty or null eureka server", + }, + { + filename: "eureka_invalid_server.bad.yml", + errMsg: "invalid eureka server URL", + }, } func TestBadConfigs(t *testing.T) { diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index fc2db1c06..b45541fbd 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -288,6 +288,10 @@ scrape_configs: username: abcdef password: abcdef +- job_name: service-eureka + eureka_sd_configs: + - server: 'http://eureka.example.com:8761/eureka' + alerting: alertmanagers: - scheme: https diff --git a/config/testdata/eureka_invalid_server.bad.yml b/config/testdata/eureka_invalid_server.bad.yml new file mode 100644 index 000000000..0c8ae428a --- /dev/null +++ b/config/testdata/eureka_invalid_server.bad.yml @@ -0,0 +1,5 @@ +scrape_configs: + +- job_name: eureka + eureka_sd_configs: + - server: eureka.com diff --git a/config/testdata/eureka_no_server.bad.yml b/config/testdata/eureka_no_server.bad.yml new file mode 100644 index 000000000..35c578a6c --- /dev/null +++ b/config/testdata/eureka_no_server.bad.yml @@ -0,0 +1,5 @@ +scrape_configs: + +- job_name: eureka + eureka_sd_configs: + - server: diff --git a/discovery/eureka/client.go b/discovery/eureka/client.go new file mode 100644 index 000000000..6d45fb77e --- /dev/null +++ b/discovery/eureka/client.go @@ -0,0 +1,110 @@ +// Copyright 2020 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 eureka + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" +) + +type Applications struct { + VersionsDelta int `xml:"versions__delta"` + AppsHashcode string `xml:"apps__hashcode"` + Applications []Application `xml:"application"` +} + +type Application struct { + Name string `xml:"name"` + Instances []Instance `xml:"instance"` +} + +type Port struct { + Port int `xml:",chardata"` + Enabled bool `xml:"enabled,attr"` +} + +type Instance struct { + HostName string `xml:"hostName"` + HomePageURL string `xml:"homePageUrl"` + StatusPageURL string `xml:"statusPageUrl"` + HealthCheckURL string `xml:"healthCheckUrl"` + App string `xml:"app"` + IPAddr string `xml:"ipAddr"` + VipAddress string `xml:"vipAddress"` + SecureVipAddress string `xml:"secureVipAddress"` + Status string `xml:"status"` + Port *Port `xml:"port"` + SecurePort *Port `xml:"securePort"` + DataCenterInfo *DataCenterInfo `xml:"dataCenterInfo"` + Metadata *MetaData `xml:"metadata"` + IsCoordinatingDiscoveryServer bool `xml:"isCoordinatingDiscoveryServer"` + LastUpdatedTimestamp int `xml:"lastUpdatedTimestamp"` + LastDirtyTimestamp int `xml:"lastDirtyTimestamp"` + ActionType string `xml:"actionType"` + CountryID int `xml:"countryId"` + InstanceID string `xml:"instanceId"` +} + +type MetaData struct { + Items []Tag `xml:",any"` +} + +type Tag struct { + XMLName xml.Name + Content string `xml:",innerxml"` +} + +type DataCenterInfo struct { + Name string `xml:"name"` + Class string `xml:"class,attr"` + Metadata *MetaData `xml:"metadata"` +} + +const appListPath string = "/apps" + +func fetchApps(ctx context.Context, server string, client *http.Client) (*Applications, error) { + url := fmt.Sprintf("%s%s", server, appListPath) + + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + request = request.WithContext(ctx) + + resp, err := client.Do(request) + if err != nil { + return nil, err + } + defer func() { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode/100 != 2 { + return nil, errors.Errorf("non 2xx status '%d' response during eureka service discovery", resp.StatusCode) + } + + var apps Applications + err = xml.NewDecoder(resp.Body).Decode(&apps) + if err != nil { + return nil, errors.Wrapf(err, "%q", url) + } + return &apps, nil +} diff --git a/discovery/eureka/client_test.go b/discovery/eureka/client_test.go new file mode 100644 index 000000000..9b7305a82 --- /dev/null +++ b/discovery/eureka/client_test.go @@ -0,0 +1,213 @@ +// Copyright 2020 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 eureka + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prometheus/prometheus/util/testutil" +) + +func TestFetchApps(t *testing.T) { + appsXML := ` + 1 + UP_4_ + + CONFIG-SERVICE + + config-service001.test.com:config-service:8080 + config-service001.test.com + CONFIG-SERVICE + 192.133.83.31 + UP + UNKNOWN + 8080 + 8080 + 1 + + MyOwn + + + 30 + 90 + 1596003469304 + 1596110179310 + 0 + 1547190033103 + + + config-service001.test.com:config-service:8080 + + http://config-service001.test.com:8080/ + http://config-service001.test.com:8080/info + http://config-service001.test.com 8080/health + config-service + false + 1596003469304 + 1596003469304 + ADDED + + + config-service002.test.com:config-service:8080 + config-service002.test.com + CONFIG-SERVICE + 192.133.83.31 + UP + UNKNOWN + 8080 + 8080 + 1 + + MyOwn + + + 30 + 90 + 1596003469304 + 1596110179310 + 0 + 1547190033103 + + + config-service002.test.com:config-service:8080 + + http://config-service002.test.com:8080/ + http://config-service002.test.com:8080/info + http://config-service002.test.com:8080/health + config-service + false + 1596003469304 + 1596003469304 + ADDED + + + + META-SERVICE + + meta-service002.test.com:meta-service:8080 + meta-service002.test.com + META-SERVICE + 192.133.87.237 + UP + UNKNOWN + 8080 + 443 + 1 + + MyOwn + + + 30 + 90 + 1535444352472 + 1596110168846 + 0 + 1535444352472 + + + meta-service + 8090 + + http://meta-service002.test.com:8080/ + http://meta-service002.test.com:8080/info + http://meta-service002.test.com:8080/health + meta-service + meta-service + false + 1535444352472 + 1535444352398 + ADDED + + + meta-service001.test.com:meta-service:8080 + meta-service001.test.com + META-SERVICE + 192.133.87.236 + UP + UNKNOWN + 8080 + 443 + 1 + + MyOwn + + + 30 + 90 + 1535444352472 + 1596110168846 + 0 + 1535444352472 + + + meta-service + 8090 + + http://meta-service001.test.com:8080/ + http://meta-service001.test.com:8080/info + http://meta-service001.test.com:8080/health + meta-service + meta-service + false + 1535444352472 + 1535444352398 + ADDED + + +` + + // Simulate apps with a valid XML response. + respHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/xml") + io.WriteString(w, appsXML) + } + // Create a test server with mock HTTP handler. + ts := httptest.NewServer(http.HandlerFunc(respHandler)) + defer ts.Close() + + apps, err := fetchApps(context.TODO(), ts.URL, &http.Client{}) + testutil.Ok(t, err) + + testutil.Equals(t, len(apps.Applications), 2) + testutil.Equals(t, apps.Applications[0].Name, "CONFIG-SERVICE") + testutil.Equals(t, apps.Applications[1].Name, "META-SERVICE") + + testutil.Equals(t, len(apps.Applications[1].Instances), 2) + testutil.Equals(t, apps.Applications[1].Instances[0].InstanceID, "meta-service002.test.com:meta-service:8080") + testutil.Equals(t, apps.Applications[1].Instances[0].Metadata.Items[0].XMLName.Local, "project") + testutil.Equals(t, apps.Applications[1].Instances[0].Metadata.Items[0].Content, "meta-service") + testutil.Equals(t, apps.Applications[1].Instances[0].Metadata.Items[1].XMLName.Local, "management.port") + testutil.Equals(t, apps.Applications[1].Instances[0].Metadata.Items[1].Content, "8090") + testutil.Equals(t, apps.Applications[1].Instances[1].InstanceID, "meta-service001.test.com:meta-service:8080") +} + +func Test500ErrorHttpResponse(t *testing.T) { + // Simulate 500 error. + respHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", "application/xml") + io.WriteString(w, ``) + } + // Create a test server with mock HTTP handler. + ts := httptest.NewServer(http.HandlerFunc(respHandler)) + defer ts.Close() + + _, err := fetchApps(context.TODO(), ts.URL, &http.Client{}) + testutil.NotOk(t, err, "5xx HTTP response") +} diff --git a/discovery/eureka/eureka.go b/discovery/eureka/eureka.go new file mode 100644 index 000000000..5cc3b5b50 --- /dev/null +++ b/discovery/eureka/eureka.go @@ -0,0 +1,220 @@ +// Copyright 2020 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 eureka + +import ( + "context" + "net" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-kit/kit/log" + "github.com/pkg/errors" + "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/discovery" + "github.com/prometheus/prometheus/discovery/refresh" + "github.com/prometheus/prometheus/discovery/targetgroup" + "github.com/prometheus/prometheus/util/strutil" +) + +const ( + // metaLabelPrefix is the meta prefix used for all meta labels. + // in this discovery. + metaLabelPrefix = model.MetaLabelPrefix + "eureka_" + metaAppInstanceLabelPrefix = metaLabelPrefix + "app_instance_" + + appNameLabel = metaLabelPrefix + "app_name" + appInstanceHostNameLabel = metaAppInstanceLabelPrefix + "hostname" + appInstanceHomePageURLLabel = metaAppInstanceLabelPrefix + "homepage_url" + appInstanceStatusPageURLLabel = metaAppInstanceLabelPrefix + "statuspage_url" + appInstanceHealthCheckURLLabel = metaAppInstanceLabelPrefix + "healthcheck_url" + appInstanceIPAddrLabel = metaAppInstanceLabelPrefix + "ip_addr" + appInstanceVipAddressLabel = metaAppInstanceLabelPrefix + "vip_address" + appInstanceSecureVipAddressLabel = metaAppInstanceLabelPrefix + "secure_vip_address" + appInstanceStatusLabel = metaAppInstanceLabelPrefix + "status" + appInstancePortLabel = metaAppInstanceLabelPrefix + "port" + appInstancePortEnabledLabel = metaAppInstanceLabelPrefix + "port_enabled" + appInstanceSecurePortLabel = metaAppInstanceLabelPrefix + "secure_port" + appInstanceSecurePortEnabledLabel = metaAppInstanceLabelPrefix + "secure_port_enabled" + appInstanceDataCenterInfoNameLabel = metaAppInstanceLabelPrefix + "datacenterinfo_name" + appInstanceDataCenterInfoMetadataPrefix = metaAppInstanceLabelPrefix + "datacenterinfo_metadata_" + appInstanceCountryIDLabel = metaAppInstanceLabelPrefix + "country_id" + appInstanceIDLabel = metaAppInstanceLabelPrefix + "id" + appInstanceMetadataPrefix = metaAppInstanceLabelPrefix + "metadata_" +) + +// DefaultSDConfig is the default Eureka SD configuration. +var DefaultSDConfig = SDConfig{ + RefreshInterval: model.Duration(30 * time.Second), +} + +func init() { + discovery.RegisterConfig(&SDConfig{}) +} + +// SDConfig is the configuration for applications running on Eureka. +type SDConfig struct { + Server string `yaml:"server,omitempty"` + HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` + RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` +} + +// Name returns the name of the Config. +func (*SDConfig) Name() string { return "eureka" } + +// NewDiscoverer returns a Discoverer for the Config. +func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { + return NewDiscovery(c, opts.Logger) +} + +// SetDirectory joins any relative file paths with dir. +func (c *SDConfig) SetDirectory(dir string) { + c.HTTPClientConfig.SetDirectory(dir) +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultSDConfig + type plain SDConfig + err := unmarshal((*plain)(c)) + if err != nil { + return err + } + if len(c.Server) == 0 { + return errors.New("eureka_sd: empty or null eureka server") + } + url, err := url.Parse(c.Server) + if err != nil { + return err + } + if len(url.Scheme) == 0 || len(url.Host) == 0 { + return errors.New("eureka_sd: invalid eureka server URL") + } + return c.HTTPClientConfig.Validate() +} + +// Discovery provides service discovery based on a Eureka instance. +type Discovery struct { + *refresh.Discovery + client *http.Client + server string +} + +// New creates a new Eureka discovery for the given role. +func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) { + rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "eureka_sd", false, false) + if err != nil { + return nil, err + } + + d := &Discovery{ + client: &http.Client{Transport: rt}, + server: conf.Server, + } + d.Discovery = refresh.NewDiscovery( + logger, + "eureka", + time.Duration(conf.RefreshInterval), + d.refresh, + ) + return d, nil +} + +func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { + apps, err := fetchApps(ctx, d.server, d.client) + if err != nil { + return nil, err + } + + tg := &targetgroup.Group{ + Source: "eureka", + } + + for _, app := range apps.Applications { + targets := targetsForApp(&app) + tg.Targets = append(tg.Targets, targets...) + } + return []*targetgroup.Group{tg}, nil +} + +func targetsForApp(app *Application) []model.LabelSet { + targets := make([]model.LabelSet, 0, len(app.Instances)) + + // Gather info about the app's 'instances'. Each instance is considered a task. + for _, t := range app.Instances { + var targetAddress string + if t.Port != nil { + targetAddress = net.JoinHostPort(t.HostName, strconv.Itoa(t.Port.Port)) + } else { + targetAddress = net.JoinHostPort(t.HostName, "80") + } + + target := model.LabelSet{ + model.AddressLabel: lv(targetAddress), + model.InstanceLabel: lv(t.InstanceID), + + appNameLabel: lv(app.Name), + appInstanceHostNameLabel: lv(t.HostName), + appInstanceHomePageURLLabel: lv(t.HomePageURL), + appInstanceStatusPageURLLabel: lv(t.StatusPageURL), + appInstanceHealthCheckURLLabel: lv(t.HealthCheckURL), + appInstanceIPAddrLabel: lv(t.IPAddr), + appInstanceVipAddressLabel: lv(t.VipAddress), + appInstanceSecureVipAddressLabel: lv(t.SecureVipAddress), + appInstanceStatusLabel: lv(t.Status), + appInstanceCountryIDLabel: lv(strconv.Itoa(t.CountryID)), + appInstanceIDLabel: lv(t.InstanceID), + } + + if t.Port != nil { + target[appInstancePortLabel] = lv(strconv.Itoa(t.Port.Port)) + target[appInstancePortEnabledLabel] = lv(strconv.FormatBool(t.Port.Enabled)) + } + + if t.SecurePort != nil { + target[appInstanceSecurePortLabel] = lv(strconv.Itoa(t.SecurePort.Port)) + target[appInstanceSecurePortEnabledLabel] = lv(strconv.FormatBool(t.SecurePort.Enabled)) + } + + if t.DataCenterInfo != nil { + target[appInstanceDataCenterInfoNameLabel] = lv(t.DataCenterInfo.Name) + + if t.DataCenterInfo.Metadata != nil { + for _, m := range t.DataCenterInfo.Metadata.Items { + ln := strutil.SanitizeLabelName(m.XMLName.Local) + target[model.LabelName(appInstanceDataCenterInfoMetadataPrefix+ln)] = lv(m.Content) + } + } + } + + if t.Metadata != nil { + for _, m := range t.Metadata.Items { + ln := strutil.SanitizeLabelName(m.XMLName.Local) + target[model.LabelName(appInstanceMetadataPrefix+ln)] = lv(m.Content) + } + } + + targets = append(targets, target) + + } + return targets +} + +func lv(s string) model.LabelValue { + return model.LabelValue(s) +} diff --git a/discovery/eureka/eureka_test.go b/discovery/eureka/eureka_test.go new file mode 100644 index 000000000..c2c67158a --- /dev/null +++ b/discovery/eureka/eureka_test.go @@ -0,0 +1,246 @@ +// Copyright 2020 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 eureka + +import ( + "context" + "github.com/prometheus/prometheus/util/testutil" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pkg/errors" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/discovery/targetgroup" +) + +func testUpdateServices(respHandler http.HandlerFunc) ([]*targetgroup.Group, error) { + // Create a test server with mock HTTP handler. + ts := httptest.NewServer(respHandler) + defer ts.Close() + + conf := SDConfig{ + Server: ts.URL, + } + + md, err := NewDiscovery(&conf, nil) + if err != nil { + return nil, err + } + + return md.refresh(context.Background()) +} + +func TestEurekaSDHandleError(t *testing.T) { + var ( + errTesting = errors.Errorf("non 2xx status '%d' response during eureka service discovery", http.StatusInternalServerError) + respHandler = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", "application/xml") + io.WriteString(w, ``) + } + ) + tgs, err := testUpdateServices(respHandler) + + testutil.ErrorEqual(t, err, errTesting) + testutil.Equals(t, len(tgs), 0) +} + +func TestEurekaSDEmptyList(t *testing.T) { + var ( + appsXML = ` +1 + +` + respHandler = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/xml") + io.WriteString(w, appsXML) + } + ) + tgs, err := testUpdateServices(respHandler) + testutil.Ok(t, err) + testutil.Equals(t, len(tgs), 1) +} + +func TestEurekaSDSendGroup(t *testing.T) { + var ( + appsXML = ` + 1 + UP_4_ + + CONFIG-SERVICE + + config-service001.test.com:config-service:8080 + config-service001.test.com + CONFIG-SERVICE + 192.133.83.31 + UP + UNKNOWN + 8080 + 8080 + 1 + + MyOwn + + + 30 + 90 + 1596003469304 + 1596110179310 + 0 + 1547190033103 + + + config-service001.test.com:config-service:8080 + + http://config-service001.test.com:8080/ + http://config-service001.test.com:8080/info + http://config-service001.test.com 8080/health + config-service + false + 1596003469304 + 1596003469304 + ADDED + + + config-service002.test.com:config-service:8080 + config-service002.test.com + CONFIG-SERVICE + 192.133.83.31 + UP + UNKNOWN + 8080 + 8080 + 1 + + MyOwn + + + 30 + 90 + 1596003469304 + 1596110179310 + 0 + 1547190033103 + + + config-service002.test.com:config-service:8080 + + http://config-service002.test.com:8080/ + http://config-service002.test.com:8080/info + http://config-service002.test.com:8080/health + config-service + false + 1596003469304 + 1596003469304 + ADDED + + + + META-SERVICE + + meta-service002.test.com:meta-service:8080 + meta-service002.test.com + META-SERVICE + 192.133.87.237 + UP + UNKNOWN + 8080 + 443 + 1 + + MyOwn + + + 30 + 90 + 1535444352472 + 1596110168846 + 0 + 1535444352472 + + + meta-service + 8090 + + http://meta-service002.test.com:8080/ + http://meta-service002.test.com:8080/info + http://meta-service002.test.com:8080/health + meta-service + meta-service + false + 1535444352472 + 1535444352398 + ADDED + + + meta-service001.test.com:meta-service:8080 + meta-service001.test.com + META-SERVICE + 192.133.87.236 + UP + UNKNOWN + 8080 + 443 + 1 + + MyOwn + + + 30 + 90 + 1535444352472 + 1596110168846 + 0 + 1535444352472 + + + meta-service + 8090 + + http://meta-service001.test.com:8080/ + http://meta-service001.test.com:8080/info + http://meta-service001.test.com:8080/health + meta-service + meta-service + false + 1535444352472 + 1535444352398 + ADDED + + +` + respHandler = func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/xml") + io.WriteString(w, appsXML) + } + ) + + tgs, err := testUpdateServices(respHandler) + testutil.Ok(t, err) + testutil.Equals(t, len(tgs), 1) + + tg := tgs[0] + testutil.Equals(t, tg.Source, "eureka") + testutil.Equals(t, len(tg.Targets), 4) + + tgt := tg.Targets[0] + testutil.Equals(t, tgt[model.AddressLabel], model.LabelValue("config-service001.test.com:8080")) + + tgt = tg.Targets[2] + testutil.Equals(t, tgt[model.AddressLabel], model.LabelValue("meta-service002.test.com:8080")) +} diff --git a/discovery/install/install.go b/discovery/install/install.go index aa07b26ea..d9394f270 100644 --- a/discovery/install/install.go +++ b/discovery/install/install.go @@ -22,6 +22,7 @@ import ( _ "github.com/prometheus/prometheus/discovery/dns" // register dns _ "github.com/prometheus/prometheus/discovery/dockerswarm" // register dockerswarm _ "github.com/prometheus/prometheus/discovery/ec2" // register ec2 + _ "github.com/prometheus/prometheus/discovery/eureka" // register eureka _ "github.com/prometheus/prometheus/discovery/file" // register file _ "github.com/prometheus/prometheus/discovery/gce" // register gce _ "github.com/prometheus/prometheus/discovery/hetzner" // register hetzner diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 4cd588c26..6f86e0832 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -207,6 +207,10 @@ dns_sd_configs: ec2_sd_configs: [ - ... ] +# List of Eureka service discovery configurations. +eureka_sd_configs: + [ - ... ] + # List of file service discovery configurations. file_sd_configs: [ - ... ] @@ -1381,6 +1385,72 @@ tls_config: [ ] ``` +### `` + +Eureka SD configurations allow retrieving scrape targets using the +[Eureka](https://github.com/Netflix/eureka) REST API. Prometheus +will periodically check the REST endpoint and +create a target for every app instance. + +The following meta labels are available on targets during [relabeling](#relabel_config): + +* `__meta_eureka_app_name`: the name of the app +* `__meta_eureka_app_instance_id`: the ID of the app instance +* `__meta_eureka_app_instance_hostname`: the hostname of the instance +* `__meta_eureka_app_instance_homepage_url`: the homepage url of the app instance +* `__meta_eureka_app_instance_statuspage_url`: the status page url of the app instance +* `__meta_eureka_app_instance_healthcheck_url`: the health check url of the app instance +* `__meta_eureka_app_instance_ip_addr`: the IP address of the app instance +* `__meta_eureka_app_instance_vip_address`: the VIP address of the app instance +* `__meta_eureka_app_instance_secure_vip_address`: the secure VIP address of the app instance +* `__meta_eureka_app_instance_status`: the status of the app instance +* `__meta_eureka_app_instance_port`: the port of the app instance +* `__meta_eureka_app_instance_port_enabled`: the port enabled of the app instance +* `__meta_eureka_app_instance_secure_port`: the secure port address of the app instance +* `__meta_eureka_app_instance_secure_port_enabled`: the secure port of the app instance +* `__meta_eureka_app_instance_country_id`: the country ID of the app instance +* `__meta_eureka_app_instance_metadata_`: app instance metadata +* `__meta_eureka_app_instance_datacenterinfo_name`: the datacenter name of the app instance +* `__meta_eureka_app_instance_datacenterinfo_`: the datacenter metadata + +See below for the configuration options for Eureka discovery: + +```yaml +# The URL to connect to the Eureka server. +server: + +# Sets the `Authorization` header on every request with the +# configured username and password. +# password and password_file are mutually exclusive. +basic_auth: + [ username: ] + [ password: ] + [ password_file: ] + +# Sets the `Authorization` header on every request with +# the configured bearer token. It is mutually exclusive with `bearer_token_file`. +[ bearer_token: ] + +# Sets the `Authorization` header on every request with the bearer token +# read from the configured file. It is mutually exclusive with `bearer_token`. +[ bearer_token_file: ] + +# Configures the scrape request's TLS settings. +tls_config: + [ ] + +# Optional proxy URL. +[ proxy_url: ] + +# Refresh interval to re-read the app instance list. +[ refresh_interval: | default = 30s ] +``` + +See [the Prometheus eureka-sd configuration file](/documentation/examples/prometheus-eureka.yml) +for a practical example on how to set up your Eureka app and your Prometheus +configuration. + + ### `` A `static_config` allows specifying a list of targets and a common label set @@ -1557,6 +1627,10 @@ dns_sd_configs: ec2_sd_configs: [ - ... ] +# List of Eureka service discovery configurations. +eureka_sd_configs: + [ - ... ] + # List of file service discovery configurations. file_sd_configs: [ - ... ] diff --git a/documentation/examples/prometheus-eureka.yml b/documentation/examples/prometheus-eureka.yml new file mode 100644 index 000000000..ef69a55eb --- /dev/null +++ b/documentation/examples/prometheus-eureka.yml @@ -0,0 +1,66 @@ +# A example scrape configuration for running Prometheus with Eureka. + +scrape_configs: + + # Make Prometheus scrape itself for metrics. + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # Discover Eureka services to scrape. + - job_name: 'eureka' + + # Scrape Eureka itself to discover new services. + eureka_sd_configs: + - server: http://localhost:8761/eureka + + relabel_configs: + # You can use Eureka's application instance metadata. + # If you are using SpringBoot, you can add metadata using eureka.instance.metadataMap like this: + # application.yaml (spring-boot) + # eureka: + # instance: + # metadataMap: + # "prometheus.scrape": "true" + # "prometheus.path": "/actuator/prometheus" + # "prometheus.port": "8080" + # + # + # Example relabel to scrape only application that have + # "prometheus.scrape = true" metadata. + # - source_labels: [__meta_eureka_app_instance_metadata_prometheus_scrape] + # action: keep + # regex: true + # + # application.yaml (spring-boot) + # eureka: + # instance: + # metadataMap: + # "prometheus.scrape": "true" + # + # Example relabel to customize metric path based on application + # "prometheus.path = " annotation. + # - source_labels: [__meta_eureka_app_instance_metadata_prometheus_path] + # action: replace + # target_label: __metrics_path__ + # regex: (.+) + # + # application.yaml (spring-boot) + # eureka: + # instance: + # metadataMap: + # "prometheus.path": "/actuator/prometheus" + # + # Example relabel to scrape only single, desired port for the application + # based on application "prometheus.port = " metadata. + # - source_labels: [__address__, __meta_eureka_app_instance_metadata_prometheus_port] + # action: replace + # regex: ([^:]+)(?::\d+)?;(\d+) + # replacement: $1:$2 + # target_label: __address__ + # + # application.yaml (spring-boot) + # eureka: + # instance: + # metadataMap: + # "prometheus.port": "8080"