Repository: prometheus/prometheus
Pull request #6794 (2020-02-08 21:02:45)

Adds normalization of localhost urls in targets page react
Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>
Fixes: #6301 

<!--
    Don't forget!
    
    - If the PR adds or changes a behaviour or fixes a bug of an exported API it would need a unit/e2e test.
    
    - Where possible use only exported APIs for tests to simplify the review and make it as close as possible to an actual library usage.
    
    - No tests are needed for internal implementation changes.
    
    - Performance improvements would need a benchmark test to prove it.
    
    - All exposed objects should have a comment.
    
    - All comments should start with a capital letter and end with a full stop.
 -->

![targets_globalurl](https://user-images.githubusercontent.com/33792202/74091993-5b1f5380-4ae4-11ea-8f06-c9b1d39806a0.gif)


juliusv: Hmm, interesting approach, thanks! My initial idea was to inject yet another global variable into the app (like we do in https://github.com/prometheus/prometheus/blob/master/web/ui/react-app/public/index.html#L12-L24), and then only do the URL transformations on the client side.

However, now that I see the global URL added as another field in the targets API, I wonder if that might not make sense too, since other consumers than the UI might want to externally probe the metrics endpoints. So it would have value beyond the UI. OTHO one could assume that API consumers know the external URL of the Prometheus server, so they could also do that kind of transformation themselves anyway. @brian-brazil what do you think?
Harkishen-Singh: > Hmm, interesting approach, thanks! My initial idea was to inject yet another global variable into the app (like we do in https://github.com/prometheus/prometheus/blob/master/web/ui/react-app/public/index.html#L12-L24), and then only do the URL transformations on the client side.
> 
> However, now that I see the global URL added as another field in the targets API, I wonder if that might not make sense too, since other consumers than the UI might want to externally probe the metrics endpoints. So it would have value beyond the UI. OTOH one could assume that API consumers know the external URL of the Prometheus server, so they could also do that kind of transformation themselves anyway. @brian-brazil what do you think?

@juliusv how about sending the globalURL in scrapeURL, if we do not have any problem showing it in the UI, a bit different from the old UI?
juliusv: > how about sending the globalURL in scrapeURL, if we do not have any problem showing it in the UI, a bit different from the old UI?

@Harkishen-Singh We are not allowed to do that because it would be a breaking change to the targets API. But even if we were allowed, I think it might be misleading to display a URL that is different from the one that Prometheus actually scrapes. So I think either your current way or the global injected variable way are ok. Just would like to get a second opinion on adding this new field to the API.

Patch:
diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go
index f8913c4e6..abfdcc3d4 100644
--- a/discovery/kubernetes/kubernetes.go
+++ b/discovery/kubernetes/kubernetes.go
@@ -16,6 +16,7 @@ package kubernetes
 import (
 	"context"
 	"reflect"
+	"strings"
 	"sync"
 	"time"
 
@@ -28,6 +29,7 @@ import (
 	apiv1 "k8s.io/api/core/v1"
 	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/kubernetes"
@@ -91,6 +93,26 @@ type SDConfig struct {
 	Role               Role                         `yaml:"role"`
 	HTTPClientConfig   config_util.HTTPClientConfig `yaml:",inline"`
 	NamespaceDiscovery NamespaceDiscovery           `yaml:"namespaces,omitempty"`
+	Selectors          []SelectorConfig             `yaml:"selectors,omitempty"`
+}
+
+type roleSelector struct {
+	node      resourceSelector
+	pod       resourceSelector
+	service   resourceSelector
+	endpoints resourceSelector
+	ingress   resourceSelector
+}
+
+type SelectorConfig struct {
+	Role  Role   `yaml:"role,omitempty"`
+	Label string `yaml:"label,omitempty"`
+	Field string `yaml:"field,omitempty"`
+}
+
+type resourceSelector struct {
+	label string
+	field string
 }
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -111,6 +133,46 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	if c.APIServer.URL == nil && !reflect.DeepEqual(c.HTTPClientConfig, config_util.HTTPClientConfig{}) {
 		return errors.Errorf("to use custom HTTP client configuration please provide the 'api_server' URL explicitly")
 	}
+
+	foundSelectorRoles := make(map[Role]struct{})
+	allowedSelectors := map[Role][]string{
+		RolePod:      {string(RolePod)},
+		RoleService:  {string(RoleService)},
+		RoleEndpoint: {string(RolePod), string(RoleService), string(RoleEndpoint)},
+		RoleNode:     {string(RoleNode)},
+		RoleIngress:  {string(RoleIngress)},
+	}
+
+	for _, selector := range c.Selectors {
+		if _, ok := foundSelectorRoles[selector.Role]; ok {
+			return errors.Errorf("duplicated selector role: %s", selector.Role)
+		}
+		foundSelectorRoles[selector.Role] = struct{}{}
+
+		if _, ok := allowedSelectors[c.Role]; !ok {
+			return errors.Errorf("invalid role: %q, expecting one of: pod, service, endpoints, node or ingress", c.Role)
+		}
+		var allowed bool
+		for _, role := range allowedSelectors[c.Role] {
+			if role == string(selector.Role) {
+				allowed = true
+				break
+			}
+		}
+
+		if !allowed {
+			return errors.Errorf("%s role supports only %s selectors", c.Role, strings.Join(allowedSelectors[c.Role], ", "))
+		}
+
+		_, err := fields.ParseSelector(selector.Field)
+		if err != nil {
+			return err
+		}
+		_, err = fields.ParseSelector(selector.Label)
+		if err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
@@ -161,6 +223,7 @@ type Discovery struct {
 	logger             log.Logger
 	namespaceDiscovery *NamespaceDiscovery
 	discoverers        []discoverer
+	selectors          roleSelector
 }
 
 func (d *Discovery) getNamespaces() []string {
@@ -211,9 +274,34 @@ func New(l log.Logger, conf *SDConfig) (*Discovery, error) {
 		role:               conf.Role,
 		namespaceDiscovery: &conf.NamespaceDiscovery,
 		discoverers:        make([]discoverer, 0),
+		selectors:          mapSelector(conf.Selectors),
 	}, nil
 }
 
+func mapSelector(rawSelector []SelectorConfig) roleSelector {
+	rs := roleSelector{}
+	for _, resourceSelectorRaw := range rawSelector {
+		switch resourceSelectorRaw.Role {
+		case RoleEndpoint:
+			rs.endpoints.field = resourceSelectorRaw.Field
+			rs.endpoints.label = resourceSelectorRaw.Label
+		case RoleIngress:
+			rs.ingress.field = resourceSelectorRaw.Field
+			rs.ingress.label = resourceSelectorRaw.Label
+		case RoleNode:
+			rs.node.field = resourceSelectorRaw.Field
+			rs.node.label = resourceSelectorRaw.Label
+		case RolePod:
+			rs.pod.field = resourceSelectorRaw.Field
+			rs.pod.label = resourceSelectorRaw.Label
+		case RoleService:
+			rs.service.field = resourceSelectorRaw.Field
+			rs.service.label = resourceSelectorRaw.Label
+		}
+	}
+	return rs
+}
+
 const resyncPeriod = 10 * time.Minute
 
 // Run implements the discoverer interface.
@@ -227,27 +315,39 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
 			e := d.client.CoreV1().Endpoints(namespace)
 			elw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.endpoints.field
+					options.LabelSelector = d.selectors.endpoints.label
 					return e.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.endpoints.field
+					options.LabelSelector = d.selectors.endpoints.label
 					return e.Watch(options)
 				},
 			}
 			s := d.client.CoreV1().Services(namespace)
 			slw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.service.field
+					options.LabelSelector = d.selectors.service.label
 					return s.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.service.field
+					options.LabelSelector = d.selectors.service.label
 					return s.Watch(options)
 				},
 			}
 			p := d.client.CoreV1().Pods(namespace)
 			plw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.pod.field
+					options.LabelSelector = d.selectors.pod.label
 					return p.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.pod.field
+					options.LabelSelector = d.selectors.pod.label
 					return p.Watch(options)
 				},
 			}
@@ -267,9 +367,13 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
 			p := d.client.CoreV1().Pods(namespace)
 			plw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.pod.field
+					options.LabelSelector = d.selectors.pod.label
 					return p.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.pod.field
+					options.LabelSelector = d.selectors.pod.label
 					return p.Watch(options)
 				},
 			}
@@ -285,9 +389,13 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
 			s := d.client.CoreV1().Services(namespace)
 			slw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.service.field
+					options.LabelSelector = d.selectors.service.label
 					return s.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.service.field
+					options.LabelSelector = d.selectors.service.label
 					return s.Watch(options)
 				},
 			}
@@ -303,9 +411,13 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
 			i := d.client.ExtensionsV1beta1().Ingresses(namespace)
 			ilw := &cache.ListWatch{
 				ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+					options.FieldSelector = d.selectors.ingress.field
+					options.LabelSelector = d.selectors.ingress.label
 					return i.List(options)
 				},
 				WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+					options.FieldSelector = d.selectors.ingress.field
+					options.LabelSelector = d.selectors.ingress.label
 					return i.Watch(options)
 				},
 			}
@@ -319,9 +431,13 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
 	case RoleNode:
 		nlw := &cache.ListWatch{
 			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+				options.FieldSelector = d.selectors.node.field
+				options.LabelSelector = d.selectors.node.label
 				return d.client.CoreV1().Nodes().List(options)
 			},
 			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+				options.FieldSelector = d.selectors.node.field
+				options.LabelSelector = d.selectors.node.label
 				return d.client.CoreV1().Nodes().Watch(options)
 			},
 		}
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 413f6f26f..9cc7a159a 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -854,6 +854,23 @@ tls_config:
 namespaces:
   names:
     [ - <string> ]
+
+# Optional label and field selectors to limit the discovery process to a subset of available resources. 
+# See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
+# and https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ to learn more about the possible 
+# filters that can be used. Endpoints role supports pod, service and endpoints selectors, other roles
+# only support selectors matching the role itself (e.g. node role can only contain node selectors).
+
+# Note: When making decision about using field/label selector make sure that this 
+# is the best approach - it will prevent Prometheus from reusing single list/watch
+# for all scrape configs. This might result in a bigger load on the Kubernetes API,
+# because per each selector combination there will be additional LIST/WATCH. On the other hand,
+# if you just want to monitor small subset of pods in large cluster it's recommended to use selectors.
+# Decision, if selectors should be used or not depends on the particular situation.
+[ selectors:
+  [ - role: <role>  
+    [ label: <string> ]
+    [ field: <string> ] ]]
 ```
 
 Where `<role>` must be `endpoints`, `service`, `pod`, `node`, or
diff --git a/tsdb/db.go b/tsdb/db.go
index d1cd35cf6..53257b798 100644
--- a/tsdb/db.go
+++ b/tsdb/db.go
@@ -91,7 +91,7 @@ type Options struct {
 	// WALCompression will turn on Snappy compression for records on the WAL.
 	WALCompression bool
 
-	// StripeSize is the size in entries of the series hash map. Reducing the size will save memory but impact perfomance.
+	// StripeSize is the size in entries of the series hash map. Reducing the size will save memory but impact performance.
 	StripeSize int
 }
 
@@ -324,7 +324,7 @@ func (db *DBReadOnly) FlushWAL(dir string) error {
 	}
 	mint := head.MinTime()
 	maxt := head.MaxTime()
-	rh := &rangeHead{
+	rh := &RangeHead{
 		head: head,
 		mint: mint,
 		maxt: maxt,
@@ -685,41 +685,61 @@ func (db *DB) Compact() (err error) {
 		maxt := rangeForTimestamp(mint, db.head.chunkRange)
 
 		// Wrap head into a range that bounds all reads to it.
-		head := &rangeHead{
-			head: db.head,
-			mint: mint,
-			// We remove 1 millisecond from maxt because block
-			// intervals are half-open: [b.MinTime, b.MaxTime). But
-			// chunk intervals are closed: [c.MinTime, c.MaxTime];
-			// so in order to make sure that overlaps are evaluated
-			// consistently, we explicitly remove the last value
-			// from the block interval here.
-			maxt: maxt - 1,
-		}
-		uid, err := db.compactor.Write(db.dir, head, mint, maxt, nil)
-		if err != nil {
-			return errors.Wrap(err, "persist head block")
+		// We remove 1 millisecond from maxt because block
+		// intervals are half-open: [b.MinTime, b.MaxTime). But
+		// chunk intervals are closed: [c.MinTime, c.MaxTime];
+		// so in order to make sure that overlaps are evaluated
+		// consistently, we explicitly remove the last value
+		// from the block interval here.
+		head := NewRangeHead(db.head, mint, maxt-1)
+		if err := db.compactHead(head, mint, maxt); err != nil {
+			return err
 		}
+	}
 
-		runtime.GC()
+	return db.compactBlocks()
+}
 
-		if err := db.reload(); err != nil {
-			if err := os.RemoveAll(filepath.Join(db.dir, uid.String())); err != nil {
-				return errors.Wrapf(err, "delete persisted head block after failed db reload:%s", uid)
-			}
-			return errors.Wrap(err, "reload blocks")
+// CompactHead compacts the given the RangeHead.
+func (db *DB) CompactHead(head *RangeHead, mint, maxt int64) (err error) {
+	db.cmtx.Lock()
+	defer db.cmtx.Unlock()
+
+	return db.compactHead(head, mint, maxt)
+}
+
+// compactHead compacts the given the RangeHead.
+// The compaction mutex should be held before calling this method.
+func (db *DB) compactHead(head *RangeHead, mint, maxt int64) (err error) {
+	uid, err := db.compactor.Write(db.dir, head, mint, maxt, nil)
+	if err != nil {
+		return errors.Wrap(err, "persist head block")
+	}
+
+	runtime.GC()
+
+	if err := db.reload(); err != nil {
+		if err := os.RemoveAll(filepath.Join(db.dir, uid.String())); err != nil {
+			return errors.Wrapf(err, "delete persisted head block after failed db reload:%s", uid)
 		}
-		if (uid == ulid.ULID{}) {
-			// Compaction resulted in an empty block.
-			// Head truncating during db.reload() depends on the persisted blocks and
-			// in this case no new block will be persisted so manually truncate the head.
-			if err = db.head.Truncate(maxt); err != nil {
-				return errors.Wrap(err, "head truncate failed (in compact)")
-			}
+		return errors.Wrap(err, "reload blocks")
+	}
+	if (uid == ulid.ULID{}) {
+		// Compaction resulted in an empty block.
+		// Head truncating during db.reload() depends on the persisted blocks and
+		// in this case no new block will be persisted so manually truncate the head.
+		if err = db.head.Truncate(maxt); err != nil {
+			return errors.Wrap(err, "head truncate failed (in compact)")
 		}
-		runtime.GC()
 	}
+	runtime.GC()
+
+	return nil
+}
 
+// compactBlocks compacts all the eligible on-disk blocks.
+// The compaction mutex should be held before calling this method.
+func (db *DB) compactBlocks() (err error) {
 	// Check for compactions of multiple blocks.
 	for {
 		plan, err := db.compactor.Plan(db.dir)
@@ -1192,7 +1212,7 @@ func (db *DB) Snapshot(dir string, withHead bool) error {
 
 	mint := db.head.MinTime()
 	maxt := db.head.MaxTime()
-	head := &rangeHead{
+	head := &RangeHead{
 		head: db.head,
 		mint: mint,
 		maxt: maxt,
@@ -1221,7 +1241,7 @@ func (db *DB) Querier(mint, maxt int64) (Querier, error) {
 		}
 	}
 	if maxt >= db.head.MinTime() {
-		blocks = append(blocks, &rangeHead{
+		blocks = append(blocks, &RangeHead{
 			head: db.head,
 			mint: mint,
 			maxt: maxt,
diff --git a/tsdb/head.go b/tsdb/head.go
index 5769c770a..f00abde97 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -748,36 +748,45 @@ func (h *Head) initTime(t int64) (initialized bool) {
 	return true
 }
 
-type rangeHead struct {
+type RangeHead struct {
 	head       *Head
 	mint, maxt int64
 }
 
-func (h *rangeHead) Index() (IndexReader, error) {
+// NewRangeHead returns a *rangeHead.
+func NewRangeHead(head *Head, mint, maxt int64) *RangeHead {
+	return &RangeHead{
+		head: head,
+		mint: mint,
+		maxt: maxt,
+	}
+}
+
+func (h *RangeHead) Index() (IndexReader, error) {
 	return h.head.indexRange(h.mint, h.maxt), nil
 }
 
-func (h *rangeHead) Chunks() (ChunkReader, error) {
+func (h *RangeHead) Chunks() (ChunkReader, error) {
 	return h.head.chunksRange(h.mint, h.maxt), nil
 }
 
-func (h *rangeHead) Tombstones() (tombstones.Reader, error) {
+func (h *RangeHead) Tombstones() (tombstones.Reader, error) {
 	return h.head.tombstones, nil
 }
 
-func (h *rangeHead) MinTime() int64 {
+func (h *RangeHead) MinTime() int64 {
 	return h.mint
 }
 
-func (h *rangeHead) MaxTime() int64 {
+func (h *RangeHead) MaxTime() int64 {
 	return h.maxt
 }
 
-func (h *rangeHead) NumSeries() uint64 {
+func (h *RangeHead) NumSeries() uint64 {
 	return h.head.NumSeries()
 }
 
-func (h *rangeHead) Meta() BlockMeta {
+func (h *RangeHead) Meta() BlockMeta {
 	return BlockMeta{
 		MinTime: h.MinTime(),
 		MaxTime: h.MaxTime(),
diff --git a/tsdb/wal/wal.go b/tsdb/wal/wal.go
index 510326b88..bb57975ad 100644
--- a/tsdb/wal/wal.go
+++ b/tsdb/wal/wal.go
@@ -190,7 +190,7 @@ type walMetrics struct {
 	writesFailed    prometheus.Counter
 }
 
-func newWALMetrics(w *WAL, r prometheus.Registerer) *walMetrics {
+func newWALMetrics(r prometheus.Registerer) *walMetrics {
 	m := &walMetrics{}
 
 	m.fsyncDuration = prometheus.NewSummary(prometheus.SummaryOpts{
@@ -264,7 +264,7 @@ func NewSize(logger log.Logger, reg prometheus.Registerer, dir string, segmentSi
 		stopc:       make(chan chan struct{}),
 		compress:    compress,
 	}
-	w.metrics = newWALMetrics(w, reg)
+	w.metrics = newWALMetrics(reg)
 
 	_, last, err := w.Segments()
 	if err != nil {
diff --git a/web/api/v1/api.go b/web/api/v1/api.go
index 54eb2d570..8370ec9f3 100644
--- a/web/api/v1/api.go
+++ b/web/api/v1/api.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"math"
 	"math/rand"
+	"net"
 	"net/http"
 	"net/url"
 	"os"
@@ -79,12 +80,15 @@ const (
 	errorNotFound    errorType = "not_found"
 )
 
-var remoteReadQueries = prometheus.NewGauge(prometheus.GaugeOpts{
-	Namespace: namespace,
-	Subsystem: subsystem,
-	Name:      "remote_read_queries",
-	Help:      "The current number of remote read queries being executed or waiting.",
-})
+var (
+	LocalhostRepresentations = []string{"127.0.0.1", "localhost"}
+	remoteReadQueries        = prometheus.NewGauge(prometheus.GaugeOpts{
+		Namespace: namespace,
+		Subsystem: subsystem,
+		Name:      "remote_read_queries",
+		Help:      "The current number of remote read queries being executed or waiting.",
+	})
+)
 
 type apiError struct {
 	typ errorType
@@ -175,6 +179,7 @@ type API struct {
 	config                func() config.Config
 	flagsMap              map[string]string
 	ready                 func(http.HandlerFunc) http.HandlerFunc
+	globalURLOptions      GlobalURLOptions
 
 	db                        func() TSDBAdmin
 	enableAdmin               bool
@@ -200,6 +205,7 @@ func NewAPI(
 	ar alertmanagerRetriever,
 	configFunc func() config.Config,
 	flagsMap map[string]string,
+	globalURLOptions GlobalURLOptions,
 	readyFunc func(http.HandlerFunc) http.HandlerFunc,
 	db func() TSDBAdmin,
 	enableAdmin bool,
@@ -222,6 +228,7 @@ func NewAPI(
 		config:                    configFunc,
 		flagsMap:                  flagsMap,
 		ready:                     readyFunc,
+		globalURLOptions:          globalURLOptions,
 		db:                        db,
 		enableAdmin:               enableAdmin,
 		rulesRetriever:            rr,
@@ -583,6 +590,7 @@ type Target struct {
 
 	ScrapePool string `json:"scrapePool"`
 	ScrapeURL  string `json:"scrapeUrl"`
+	GlobalURL  string `json:"globalUrl"`
 
 	LastError          string              `json:"lastError"`
 	LastScrape         time.Time           `json:"lastScrape"`
@@ -602,6 +610,54 @@ type TargetDiscovery struct {
 	DroppedTargets []*DroppedTarget `json:"droppedTargets"`
 }
 
+// GlobalURLOptions contains fields used for deriving the global URL for local targets.
+type GlobalURLOptions struct {
+	ListenAddress string
+	Host          string
+	Scheme        string
+}
+
+func getGlobalURL(u *url.URL, opts GlobalURLOptions) (*url.URL, error) {
+	host, port, err := net.SplitHostPort(u.Host)
+	if err != nil {
+		return u, err
+	}
+
+	for _, lhr := range LocalhostRepresentations {
+		if host == lhr {
+			_, ownPort, err := net.SplitHostPort(opts.ListenAddress)
+			if err != nil {
+				return u, err
+			}
+
+			if port == ownPort {
+				// Only in the case where the target is on localhost and its port is
+				// the same as the one we're listening on, we know for sure that
+				// we're monitoring our own process and that we need to change the
+				// scheme, hostname, and port to the externally reachable ones as
+				// well. We shouldn't need to touch the path at all, since if a
+				// path prefix is defined, the path under which we scrape ourselves
+				// should already contain the prefix.
+				u.Scheme = opts.Scheme
+				u.Host = opts.Host
+			} else {
+				// Otherwise, we only know that localhost is not reachable
+				// externally, so we replace only the hostname by the one in the
+				// external URL. It could be the wrong hostname for the service on
+				// this port, but it's still the best possible guess.
+				host, _, err := net.SplitHostPort(opts.Host)
+				if err != nil {
+					return u, err
+				}
+				u.Host = host + ":" + port
+			}
+			break
+		}
+	}
+
+	return u, nil
+}
+
 func (api *API) targets(r *http.Request) apiFuncResult {
 	sortKeys := func(targets map[string][]*scrape.Target) ([]string, int) {
 		var n int
@@ -641,12 +697,22 @@ func (api *API) targets(r *http.Request) apiFuncResult {
 					lastErrStr = lastErr.Error()
 				}
 
+				globalURL, err := getGlobalURL(target.URL(), api.globalURLOptions)
+
 				res.ActiveTargets = append(res.ActiveTargets, &Target{
-					DiscoveredLabels:   target.DiscoveredLabels().Map(),
-					Labels:             target.Labels().Map(),
-					ScrapePool:         key,
-					ScrapeURL:          target.URL().String(),
-					LastError:          lastErrStr,
+					DiscoveredLabels: target.DiscoveredLabels().Map(),
+					Labels:           target.Labels().Map(),
+					ScrapePool:       key,
+					ScrapeURL:        target.URL().String(),
+					GlobalURL:        globalURL.String(),
+					LastError: func() string {
+						if err == nil && lastErrStr == "" {
+							return ""
+						} else if err != nil {
+							return errors.Wrapf(err, lastErrStr).Error()
+						}
+						return lastErrStr
+					}(),
 					LastScrape:         target.LastScrape(),
 					LastScrapeDuration: target.LastScrapeDuration().Seconds(),
 					Health:             target.Health(),
diff --git a/web/ui/react-app/src/pages/targets/EndpointLink.tsx b/web/ui/react-app/src/pages/targets/EndpointLink.tsx
index b67fcf455..3f524658b 100644
--- a/web/ui/react-app/src/pages/targets/EndpointLink.tsx
+++ b/web/ui/react-app/src/pages/targets/EndpointLink.tsx
@@ -3,9 +3,10 @@ import { Badge, Alert } from 'reactstrap';
 
 export interface EndpointLinkProps {
   endpoint: string;
+  globalUrl: string;
 }
 
-const EndpointLink: FC<EndpointLinkProps> = ({ endpoint }) => {
+const EndpointLink: FC<EndpointLinkProps> = ({ endpoint, globalUrl }) => {
   let url: URL;
   try {
     url = new URL(endpoint);
@@ -22,7 +23,7 @@ const EndpointLink: FC<EndpointLinkProps> = ({ endpoint }) => {
 
   return (
     <>
-      <a href={endpoint}>{`${protocol}//${host}${pathname}`}</a>
+      <a href={globalUrl}>{`${protocol}//${host}${pathname}`}</a>
       {params.length > 0 ? <br /> : null}
       {params.map(([labelName, labelValue]: [string, string]) => {
         return (
diff --git a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx
index 9abffaa4d..390fe3da9 100644
--- a/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx
+++ b/web/ui/react-app/src/pages/targets/ScrapePoolPanel.tsx
@@ -49,6 +49,7 @@ const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup }) => {
                 labels,
                 scrapePool,
                 scrapeUrl,
+                globalUrl,
                 lastError,
                 lastScrape,
                 lastScrapeDuration,
@@ -59,7 +60,7 @@ const ScrapePoolPanel: FC<PanelProps> = ({ scrapePool, targetGroup }) => {
               return (
                 <tr key={scrapeUrl}>
                   <td className={styles.endpoint}>
-                    <EndpointLink endpoint={scrapeUrl} />
+                    <EndpointLink endpoint={scrapeUrl} globalUrl={globalUrl} />
                   </td>
                   <td className={styles.state}>
                     <Badge color={color}>{health.toUpperCase()}</Badge>
diff --git a/web/ui/react-app/src/pages/targets/target.ts b/web/ui/react-app/src/pages/targets/target.ts
index 509bb9803..909c67e7b 100644
--- a/web/ui/react-app/src/pages/targets/target.ts
+++ b/web/ui/react-app/src/pages/targets/target.ts
@@ -7,6 +7,7 @@ export interface Target {
   labels: Labels;
   scrapePool: string;
   scrapeUrl: string;
+  globalUrl: string;
   lastError: string;
   lastScrape: string;
   lastScrapeDuration: number;
diff --git a/web/web.go b/web/web.go
index 732702a0b..3a3fedfb4 100644
--- a/web/web.go
+++ b/web/web.go
@@ -70,24 +70,20 @@ import (
 	"github.com/prometheus/prometheus/web/ui"
 )
 
-var (
-	localhostRepresentations = []string{"127.0.0.1", "localhost"}
-
-	// Paths that are handled by the React / Reach router that should all be served the main React app's index.html.
-	reactRouterPaths = []string{
-		"/",
-		"/alerts",
-		"/config",
-		"/flags",
-		"/graph",
-		"/rules",
-		"/service-discovery",
-		"/status",
-		"/targets",
-		"/tsdb-status",
-		"/version",
-	}
-)
+// Paths that are handled by the React / Reach router that should all be served the main React app's index.html.
+var reactRouterPaths = []string{
+	"/",
+	"/alerts",
+	"/config",
+	"/flags",
+	"/graph",
+	"/rules",
+	"/service-discovery",
+	"/status",
+	"/targets",
+	"/tsdb-status",
+	"/version",
+}
 
 // withStackTrace logs the stack trace in case the request panics. The function
 // will re-raise the error which will then be handled by the net/http package.
@@ -300,6 +296,11 @@ func New(logger log.Logger, o *Options) *Handler {
 			return *h.config
 		},
 		o.Flags,
+		api_v1.GlobalURLOptions{
+			ListenAddress: o.ListenAddress,
+			Host:          o.ExternalURL.Host,
+			Scheme:        o.ExternalURL.Scheme,
+		},
 		h.testReady,
 		func() api_v1.TSDBAdmin {
 			return h.options.TSDB()
@@ -973,7 +974,7 @@ func tmplFuncs(consolesPath string, opts *Options) template_text.FuncMap {
 			if err != nil {
 				return u
 			}
-			for _, lhr := range localhostRepresentations {
+			for _, lhr := range api_v1.LocalhostRepresentations {
 				if host == lhr {
 					_, ownPort, err := net.SplitHostPort(opts.ListenAddress)
 					if err != nil {


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 4fbbded64..4d8018c07 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -709,6 +709,19 @@ func TestKubernetesEmptyAPIServer(t *testing.T) {
 	testutil.Ok(t, err)
 }
 
+func TestKubernetesSelectors(t *testing.T) {
+	_, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml")
+	testutil.Ok(t, err)
+	_, err = LoadFile("testdata/kubernetes_selectors_node.good.yml")
+	testutil.Ok(t, err)
+	_, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml")
+	testutil.Ok(t, err)
+	_, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml")
+	testutil.Ok(t, err)
+	_, err = LoadFile("testdata/kubernetes_selectors_service.good.yml")
+	testutil.Ok(t, err)
+}
+
 var expectedErrors = []struct {
 	filename string
 	errMsg   string
@@ -791,9 +804,30 @@ var expectedErrors = []struct {
 	}, {
 		filename: "kubernetes_role.bad.yml",
 		errMsg:   "role",
+	}, {
+		filename: "kubernetes_selectors_endpoints.bad.yml",
+		errMsg:   "endpoints role supports only pod, service, endpoints selectors",
+	}, {
+		filename: "kubernetes_selectors_ingress.bad.yml",
+		errMsg:   "ingress role supports only ingress selectors",
+	}, {
+		filename: "kubernetes_selectors_node.bad.yml",
+		errMsg:   "node role supports only node selectors",
+	}, {
+		filename: "kubernetes_selectors_pod.bad.yml",
+		errMsg:   "pod role supports only pod selectors",
+	}, {
+		filename: "kubernetes_selectors_service.bad.yml",
+		errMsg:   "service role supports only service selectors",
 	}, {
 		filename: "kubernetes_namespace_discovery.bad.yml",
 		errMsg:   "field foo not found in type kubernetes.plain",
+	}, {
+		filename: "kubernetes_selectors_duplicated_role.bad.yml",
+		errMsg:   "duplicated selector role: pod",
+	}, {
+		filename: "kubernetes_selectors_incorrect_selector.bad.yml",
+		errMsg:   "invalid selector: 'metadata.status-Running'; can't understand 'metadata.status-Running'",
 	}, {
 		filename: "kubernetes_bearertoken_basicauth.bad.yml",
 		errMsg:   "at most one of basic_auth, bearer_token & bearer_token_file must be configured",
diff --git a/config/testdata/kubernetes_selectors_duplicated_role.bad.yml b/config/testdata/kubernetes_selectors_duplicated_role.bad.yml
new file mode 100644
index 000000000..dc3dec25c
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_duplicated_role.bad.yml
@@ -0,0 +1,11 @@
+scrape_configs:
+- job_name: prometheus
+  kubernetes_sd_configs:
+  - role: endpoints
+    selectors:
+    - role: "pod"
+      label: "foo=bar"
+      field: "metadata.status=Running"
+    - role: "pod"
+      label: "foo=bar"
+      field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_endpoints.bad.yml b/config/testdata/kubernetes_selectors_endpoints.bad.yml
new file mode 100644
index 000000000..d2128f374
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_endpoints.bad.yml
@@ -0,0 +1,14 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: endpoints
+        selectors:
+        - role: "node"
+          label: "foo=bar"
+          field: "metadata.status=Running"
+        - role: "service"
+          label: "foo=bar"
+          field: "metadata.status=Running"
+        - role: "endpoints"
+          label: "foo=bar"
+          field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_endpoints.good.yml b/config/testdata/kubernetes_selectors_endpoints.good.yml
new file mode 100644
index 000000000..d0b22ff93
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_endpoints.good.yml
@@ -0,0 +1,14 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: endpoints
+        selectors:
+        - role: "pod"
+          label: "foo=bar"
+          field: "metadata.status=Running"
+        - role: "service"
+          label: "foo=bar"
+          field: "metadata.status=Running"
+        - role: "endpoints"
+          label: "foo=bar"
+          field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_incorrect_selector.bad.yml b/config/testdata/kubernetes_selectors_incorrect_selector.bad.yml
new file mode 100644
index 000000000..72d2d90e6
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_incorrect_selector.bad.yml
@@ -0,0 +1,7 @@
+scrape_configs:
+- job_name: prometheus
+  kubernetes_sd_configs:
+  - role: endpoints
+    selectors:
+    - role: "pod"
+      field: "metadata.status-Running"
diff --git a/config/testdata/kubernetes_selectors_ingress.bad.yml b/config/testdata/kubernetes_selectors_ingress.bad.yml
new file mode 100644
index 000000000..7698de35a
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_ingress.bad.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: ingress
+        selectors:
+          - role:  "node"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_ingress.good.yml b/config/testdata/kubernetes_selectors_ingress.good.yml
new file mode 100644
index 000000000..6aa1edf20
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_ingress.good.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: ingress
+        selectors:
+          - role:  "ingress"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_node.bad.yml b/config/testdata/kubernetes_selectors_node.bad.yml
new file mode 100644
index 000000000..969435151
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_node.bad.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: node
+        selectors:
+          - role:  "pod"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_node.good.yml b/config/testdata/kubernetes_selectors_node.good.yml
new file mode 100644
index 000000000..d355c62ab
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_node.good.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: node
+        selectors:
+          - role:  "node"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_pod.bad.yml b/config/testdata/kubernetes_selectors_pod.bad.yml
new file mode 100644
index 000000000..daa7290a4
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_pod.bad.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: pod
+        selectors:
+          - role:  "node"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_pod.good.yml b/config/testdata/kubernetes_selectors_pod.good.yml
new file mode 100644
index 000000000..e5ab315fd
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_pod.good.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: pod
+        selectors:
+          - role:  "pod"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_service.bad.yml b/config/testdata/kubernetes_selectors_service.bad.yml
new file mode 100644
index 000000000..78179be69
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_service.bad.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: service
+        selectors:
+          - role:  "pod"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/config/testdata/kubernetes_selectors_service.good.yml b/config/testdata/kubernetes_selectors_service.good.yml
new file mode 100644
index 000000000..9c7705dc6
--- /dev/null
+++ b/config/testdata/kubernetes_selectors_service.good.yml
@@ -0,0 +1,8 @@
+scrape_configs:
+  - job_name: prometheus
+    kubernetes_sd_configs:
+      - role: service
+        selectors:
+          - role:  "service"
+            label: "foo=bar"
+            field: "metadata.status=Running"
diff --git a/promql/engine_test.go b/promql/engine_test.go
index 24d275564..78f6711dc 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -1154,7 +1154,7 @@ func TestQueryLogger_basic(t *testing.T) {
 		testutil.Ok(t, res.Err)
 	}
 
-	// Query works without query log initalized.
+	// Query works without query log initialized.
 	queryExec()
 
 	f1 := NewFakeQueryLogger()
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 836daab1a..104db08a9 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -826,8 +826,9 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "blackbox",
 						ScrapeURL:          "http://localhost:9115/probe?target=example.com",
+						GlobalURL:          "http://localhost:9115/probe?target=example.com",
 						Health:             "down",
-						LastError:          "failed",
+						LastError:          "failed: missing port in address",
 						LastScrape:         scrapeStart,
 						LastScrapeDuration: 0.1,
 					},
@@ -838,6 +839,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "test",
 						ScrapeURL:          "http://example.com:8080/metrics",
+						GlobalURL:          "http://example.com:8080/metrics",
 						Health:             "up",
 						LastError:          "",
 						LastScrape:         scrapeStart,
@@ -870,8 +872,9 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "blackbox",
 						ScrapeURL:          "http://localhost:9115/probe?target=example.com",
+						GlobalURL:          "http://localhost:9115/probe?target=example.com",
 						Health:             "down",
-						LastError:          "failed",
+						LastError:          "failed: missing port in address",
 						LastScrape:         scrapeStart,
 						LastScrapeDuration: 0.1,
 					},
@@ -882,6 +885,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "test",
 						ScrapeURL:          "http://example.com:8080/metrics",
+						GlobalURL:          "http://example.com:8080/metrics",
 						Health:             "up",
 						LastError:          "",
 						LastScrape:         scrapeStart,
@@ -914,8 +918,9 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "blackbox",
 						ScrapeURL:          "http://localhost:9115/probe?target=example.com",
+						GlobalURL:          "http://localhost:9115/probe?target=example.com",
 						Health:             "down",
-						LastError:          "failed",
+						LastError:          "failed: missing port in address",
 						LastScrape:         scrapeStart,
 						LastScrapeDuration: 0.1,
 					},
@@ -926,6 +931,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, testLabelAPI
 						},
 						ScrapePool:         "test",
 						ScrapeURL:          "http://example.com:8080/metrics",
+						GlobalURL:          "http://example.com:8080/metrics",
 						Health:             "up",
 						LastError:          "",
 						LastScrape:         scrapeStart,
diff --git a/web/ui/react-app/src/pages/targets/EndpointLink.test.tsx b/web/ui/react-app/src/pages/targets/EndpointLink.test.tsx
index 4ad68b96c..d89326f73 100644
--- a/web/ui/react-app/src/pages/targets/EndpointLink.test.tsx
+++ b/web/ui/react-app/src/pages/targets/EndpointLink.test.tsx
@@ -6,19 +6,21 @@ import EndpointLink from './EndpointLink';
 describe('EndpointLink', () => {
   it('renders a simple anchor if the endpoint has no query params', () => {
     const endpoint = 'http://100.104.208.71:15090/stats/prometheus';
-    const endpointLink = shallow(<EndpointLink endpoint={endpoint} />);
+    const globalURL = 'http://100.104.208.71:15090/stats/prometheus';
+    const endpointLink = shallow(<EndpointLink endpoint={endpoint} globalUrl={globalURL} />);
     const anchor = endpointLink.find('a');
-    expect(anchor.prop('href')).toEqual(endpoint);
+    expect(anchor.prop('href')).toEqual(globalURL);
     expect(anchor.children().text()).toEqual(endpoint);
     expect(endpointLink.find('br')).toHaveLength(0);
   });
 
   it('renders an anchor targeting endpoint but with query param labels if the endpoint has query params', () => {
     const endpoint = 'http://100.99.128.71:9115/probe?module=http_2xx&target=http://some-service';
-    const endpointLink = shallow(<EndpointLink endpoint={endpoint} />);
+    const globalURL = 'http://100.99.128.71:9115/probe?module=http_2xx&target=http://some-service';
+    const endpointLink = shallow(<EndpointLink endpoint={endpoint} globalUrl={globalURL} />);
     const anchor = endpointLink.find('a');
     const badges = endpointLink.find(Badge);
-    expect(anchor.prop('href')).toEqual(endpoint);
+    expect(anchor.prop('href')).toEqual(globalURL);
     expect(anchor.children().text()).toEqual('http://100.99.128.71:9115/probe');
     expect(endpointLink.find('br')).toHaveLength(1);
     expect(badges).toHaveLength(2);
@@ -29,7 +31,7 @@ describe('EndpointLink', () => {
   });
 
   it('renders an alert if url is invalid', () => {
-    const endpointLink = shallow(<EndpointLink endpoint={'afdsacas'} />);
+    const endpointLink = shallow(<EndpointLink endpoint={'afdsacas'} globalUrl={'afdsacas'} />);
     const err = endpointLink.find(Alert);
     expect(err.render().text()).toEqual('Error: Invalid URL');
   });
diff --git a/web/ui/react-app/src/pages/targets/__testdata__/testdata.ts b/web/ui/react-app/src/pages/targets/__testdata__/testdata.ts
index 0a6ed47e8..53f6a7e48 100644
--- a/web/ui/react-app/src/pages/targets/__testdata__/testdata.ts
+++ b/web/ui/react-app/src/pages/targets/__testdata__/testdata.ts
@@ -20,6 +20,7 @@ export const targetGroups: ScrapePools = Object.freeze({
         },
         scrapePool: 'blackbox',
         scrapeUrl: 'http://127.0.0.1:9115/probe?module=http_2xx&target=http%3A%2F%2Fprometheus.io',
+        globalUrl: 'http://localhost.localdomain:9115/probe?module=http_2xx&target=http%3A%2F%2Fprometheus.io',
         lastError: '',
         lastScrape: '2019-11-04T11:52:14.759299-07:00',
         lastScrapeDuration: 36560147,
@@ -39,6 +40,7 @@ export const targetGroups: ScrapePools = Object.freeze({
         },
         scrapePool: 'blackbox',
         scrapeUrl: 'http://127.0.0.1:9115/probe?module=http_2xx&target=https%3A%2F%2Fprometheus.io',
+        globalUrl: 'http://localhost.localdomain:9115/probe?module=http_2xx&target=https%3A%2F%2Fprometheus.io',
         lastError: '',
         lastScrape: '2019-11-04T11:52:24.731096-07:00',
         lastScrapeDuration: 49448763,
@@ -58,6 +60,7 @@ export const targetGroups: ScrapePools = Object.freeze({
         },
         scrapePool: 'blackbox',
         scrapeUrl: 'http://127.0.0.1:9115/probe?module=http_2xx&target=http%3A%2F%2Fexample.com%3A8080',
+        globalUrl: 'http://localhost.localdomain:9115/probe?module=http_2xx&target=http%3A%2F%2Fexample.com%3A8080',
         lastError: '',
         lastScrape: '2019-11-04T11:52:13.516654-07:00',
         lastScrapeDuration: 120916592,
@@ -81,6 +84,7 @@ export const targetGroups: ScrapePools = Object.freeze({
         },
         scrapePool: 'node_exporter',
         scrapeUrl: 'http://localhost:9100/metrics',
+        globalUrl: 'http://localhost.localdomain:9100/metrics',
         lastError: '',
         lastScrape: '2019-11-04T11:52:14.145703-07:00',
         lastScrapeDuration: 3842307,
@@ -104,6 +108,7 @@ export const targetGroups: ScrapePools = Object.freeze({
         },
         scrapePool: 'prometheus',
         scrapeUrl: 'http://localhost:9090/metrics',
+        globalUrl: 'http://localhost.localdomain:9000/metrics',
         lastError: '',
         lastScrape: '2019-11-04T11:52:18.479731-07:00',
         lastScrapeDuration: 4050976,
diff --git a/web/web_test.go b/web/web_test.go
index 8a1f70bdd..f9e6b1413 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -305,7 +305,11 @@ func TestRoutePrefix(t *testing.T) {
 		Notifier:       nil,
 		RoutePrefix:    "/prometheus",
 		EnableAdminAPI: true,
-		TSDB:           func() *libtsdb.DB { return db },
+		ExternalURL: &url.URL{
+			Host:   "localhost.localdomain:9090",
+			Scheme: "http",
+		},
+		TSDB: func() *libtsdb.DB { return db },
 	}
 
 	opts.Flags = map[string]string{}
@@ -391,7 +395,12 @@ func TestDebugHandler(t *testing.T) {
 		{"/foo", "/bar/debug/pprof/goroutine", 404},
 	} {
 		opts := &Options{
-			RoutePrefix: tc.prefix,
+			RoutePrefix:   tc.prefix,
+			ListenAddress: "somehost:9090",
+			ExternalURL: &url.URL{
+				Host:   "localhost.localdomain:9090",
+				Scheme: "http",
+			},
 		}
 		handler := New(nil, opts)
 		handler.Ready()
@@ -411,7 +420,14 @@ func TestDebugHandler(t *testing.T) {
 func TestHTTPMetrics(t *testing.T) {
 	t.Parallel()
 
-	handler := New(nil, &Options{RoutePrefix: "/"})
+	handler := New(nil, &Options{
+		RoutePrefix:   "/",
+		ListenAddress: "somehost:9090",
+		ExternalURL: &url.URL{
+			Host:   "localhost.localdomain:9090",
+			Scheme: "http",
+		},
+	})
 	getReady := func() int {
 		t.Helper()
 		w := httptest.NewRecorder()