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"