From: Daniel Swarbrick <dswarbrick@debian.org>
Date: Tue, 25 Oct 2022 22:21:11 +0200
Subject: Splice the classic web UI into source

Forwarded: not-needed
Last-Update: 2022-10-20

This patch reinstates the template rendering handlers which are required
by the classic web UI (split off into the prometheus-ui-classic) package.
---
 rules/alerting.go       |  38 +++++
 rules/alerting_test.go  |  18 +++
 rules/manager.go        |   4 +
 rules/origin_test.go    |   3 +
 rules/recording.go      |  24 +++
 rules/recording_test.go |  16 ++
 web/web.go              |  23 +++
 web/web_classic.go      | 406 ++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 532 insertions(+)
 create mode 100644 web/web_classic.go

diff --git a/rules/alerting.go b/rules/alerting.go
index aed4e8f..d7983b8 100644
--- a/rules/alerting.go
+++ b/rules/alerting.go
@@ -16,6 +16,7 @@ package rules
 import (
 	"context"
 	"fmt"
+	html_template "html/template"
 	"net/url"
 	"strings"
 	"sync"
@@ -34,6 +35,7 @@ import (
 	"github.com/prometheus/prometheus/promql/parser"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/template"
+	"github.com/prometheus/prometheus/util/strutil"
 )
 
 const (
@@ -42,6 +44,8 @@ const (
 	// AlertForStateMetricName is the metric name for 'for' state of alert.
 	alertForStateMetricName = "ALERTS_FOR_STATE"
 
+	// AlertNameLabel is the label name indicating the name of an alert.
+	alertNameLabel = "alertname"
 	// AlertStateLabel is the label name indicating the state of an alert.
 	alertStateLabel = "alertstate"
 )
@@ -562,3 +566,37 @@ func (r *AlertingRule) String() string {
 
 	return string(byt)
 }
+
+// HTMLSnippet returns an HTML snippet representing this alerting rule. The
+// resulting snippet is expected to be presented in a <pre> element, so that
+// line breaks and other returned whitespace is respected.
+func (r *AlertingRule) HTMLSnippet(pathPrefix string) html_template.HTML {
+	alertMetric := model.Metric{
+		model.MetricNameLabel: alertMetricName,
+		alertNameLabel:        model.LabelValue(r.name),
+	}
+
+	labelsMap := make(map[string]string, len(r.labels))
+	for _, l := range r.labels {
+		labelsMap[l.Name] = html_template.HTMLEscapeString(l.Value)
+	}
+
+	annotationsMap := make(map[string]string, len(r.annotations))
+	for _, l := range r.annotations {
+		annotationsMap[l.Name] = html_template.HTMLEscapeString(l.Value)
+	}
+
+	ar := rulefmt.Rule{
+		Alert:       fmt.Sprintf("<a href=%q>%s</a>", pathPrefix+strutil.TableLinkForExpression(alertMetric.String()), r.name),
+		Expr:        fmt.Sprintf("<a href=%q>%s</a>", pathPrefix+strutil.TableLinkForExpression(r.vector.String()), html_template.HTMLEscapeString(r.vector.String())),
+		For:         model.Duration(r.holdDuration),
+		Labels:      labelsMap,
+		Annotations: annotationsMap,
+	}
+
+	byt, err := yaml.Marshal(ar)
+	if err != nil {
+		return html_template.HTML(fmt.Sprintf("error marshaling alerting rule: %q", html_template.HTMLEscapeString(err.Error())))
+	}
+	return html_template.HTML(byt)
+}
diff --git a/rules/alerting_test.go b/rules/alerting_test.go
index 8cd0da2..2f9d4dd 100644
--- a/rules/alerting_test.go
+++ b/rules/alerting_test.go
@@ -16,6 +16,7 @@ package rules
 import (
 	"context"
 	"errors"
+	"html/template"
 	"testing"
 	"time"
 
@@ -33,6 +34,23 @@ import (
 	"github.com/prometheus/prometheus/util/teststorage"
 )
 
+func TestAlertingRuleHTMLSnippet(t *testing.T) {
+	expr, err := parser.ParseExpr(`foo{html="<b>BOLD<b>"}`)
+	require.NoError(t, err)
+	rule := NewAlertingRule("testrule", expr, 0, 0, labels.FromStrings("html", "<b>BOLD</b>"), labels.FromStrings("html", "<b>BOLD</b>"), nil, "", false, nil)
+
+	const want = template.HTML(`alert: <a href="/test/prefix/graph?g0.expr=ALERTS%7Balertname%3D%22testrule%22%7D&g0.tab=1">testrule</a>
+expr: <a href="/test/prefix/graph?g0.expr=foo%7Bhtml%3D%22%3Cb%3EBOLD%3Cb%3E%22%7D&g0.tab=1">foo{html=&#34;&lt;b&gt;BOLD&lt;b&gt;&#34;}</a>
+labels:
+  html: '&lt;b&gt;BOLD&lt;/b&gt;'
+annotations:
+  html: '&lt;b&gt;BOLD&lt;/b&gt;'
+`)
+
+	got := rule.HTMLSnippet("/test/prefix")
+	require.Equal(t, want, got, "incorrect HTML snippet; want:\n\n|%v|\n\ngot:\n\n|%v|", want, got)
+}
+
 func TestAlertingRuleState(t *testing.T) {
 	tests := []struct {
 		name   string
diff --git a/rules/manager.go b/rules/manager.go
index 31c90e9..bff0dc0 100644
--- a/rules/manager.go
+++ b/rules/manager.go
@@ -17,6 +17,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	html_template "html/template"
 	"math"
 	"net/url"
 	"sort"
@@ -240,6 +241,9 @@ type Rule interface {
 	// GetEvaluationTimestamp returns last evaluation timestamp.
 	// NOTE: Used dynamically by rules.html template.
 	GetEvaluationTimestamp() time.Time
+	// HTMLSnippet returns a human-readable string representation of the rule,
+	// decorated with HTML elements for use the web frontend.
+	HTMLSnippet(pathPrefix string) html_template.HTML
 }
 
 // Group is a set of rules that have a logical relation.
diff --git a/rules/origin_test.go b/rules/origin_test.go
index ea4f4f9..1d1c580 100644
--- a/rules/origin_test.go
+++ b/rules/origin_test.go
@@ -15,6 +15,7 @@ package rules
 
 import (
 	"context"
+	html_template "html/template"
 	"net/url"
 	"testing"
 	"time"
@@ -44,6 +45,8 @@ func (u unknownRule) GetEvaluationDuration() time.Duration { return 0 }
 func (u unknownRule) SetEvaluationTimestamp(time.Time)     {}
 func (u unknownRule) GetEvaluationTimestamp() time.Time    { return time.Time{} }
 
+func (u unknownRule) HTMLSnippet(pathPrefix string) html_template.HTML { return html_template.HTML("") }
+
 func TestNewRuleDetailPanics(t *testing.T) {
 	require.PanicsWithValue(t, `unknown rule type "rules.unknownRule"`, func() {
 		NewRuleDetail(unknownRule{})
diff --git a/rules/recording.go b/rules/recording.go
index b6a886c..5403639 100644
--- a/rules/recording.go
+++ b/rules/recording.go
@@ -16,6 +16,7 @@ package rules
 import (
 	"context"
 	"fmt"
+	"html/template"
 	"net/url"
 	"time"
 
@@ -26,6 +27,7 @@ import (
 	"github.com/prometheus/prometheus/model/rulefmt"
 	"github.com/prometheus/prometheus/promql"
 	"github.com/prometheus/prometheus/promql/parser"
+	"github.com/prometheus/prometheus/util/strutil"
 )
 
 // A RecordingRule records its vector expression into new timeseries.
@@ -166,3 +168,25 @@ func (rule *RecordingRule) SetEvaluationTimestamp(ts time.Time) {
 func (rule *RecordingRule) GetEvaluationTimestamp() time.Time {
 	return rule.evaluationTimestamp.Load()
 }
+
+// HTMLSnippet returns an HTML snippet representing this rule.
+func (rule *RecordingRule) HTMLSnippet(pathPrefix string) template.HTML {
+	ruleExpr := rule.vector.String()
+	labels := make(map[string]string, len(rule.labels))
+	for _, l := range rule.labels {
+		labels[l.Name] = template.HTMLEscapeString(l.Value)
+	}
+
+	r := rulefmt.Rule{
+		Record: fmt.Sprintf(`<a href="%s">%s</a>`, pathPrefix+strutil.TableLinkForExpression(rule.name), rule.name),
+		Expr:   fmt.Sprintf(`<a href="%s">%s</a>`, pathPrefix+strutil.TableLinkForExpression(ruleExpr), template.HTMLEscapeString(ruleExpr)),
+		Labels: labels,
+	}
+
+	byt, err := yaml.Marshal(r)
+	if err != nil {
+		return template.HTML(fmt.Sprintf("error marshaling recording rule: %q", template.HTMLEscapeString(err.Error())))
+	}
+
+	return template.HTML(byt)
+}
diff --git a/rules/recording_test.go b/rules/recording_test.go
index 35a0b1a..a638899 100644
--- a/rules/recording_test.go
+++ b/rules/recording_test.go
@@ -15,6 +15,7 @@ package rules
 
 import (
 	"context"
+	"html/template"
 	"testing"
 	"time"
 
@@ -158,6 +159,21 @@ func BenchmarkRuleEval(b *testing.B) {
 	}
 }
 
+func TestRecordingRuleHTMLSnippet(t *testing.T) {
+	expr, err := parser.ParseExpr(`foo{html="<b>BOLD<b>"}`)
+	require.NoError(t, err)
+	rule := NewRecordingRule("testrule", expr, labels.FromStrings("html", "<b>BOLD</b>"))
+
+	const want = template.HTML(`record: <a href="/test/prefix/graph?g0.expr=testrule&g0.tab=1">testrule</a>
+expr: <a href="/test/prefix/graph?g0.expr=foo%7Bhtml%3D%22%3Cb%3EBOLD%3Cb%3E%22%7D&g0.tab=1">foo{html=&#34;&lt;b&gt;BOLD&lt;b&gt;&#34;}</a>
+labels:
+  html: '&lt;b&gt;BOLD&lt;/b&gt;'
+`)
+
+	got := rule.HTMLSnippet("/test/prefix")
+	require.Equal(t, want, got, "incorrect HTML snippet; want:\n\n%s\n\ngot:\n\n%s", want, got)
+}
+
 // TestRuleEvalDuplicate tests for duplicate labels in recorded metrics, see #5529.
 func TestRuleEvalDuplicate(t *testing.T) {
 	storage := teststorage.New(t)
diff --git a/web/web.go b/web/web.go
index d261ddb..2dd1865 100644
--- a/web/web.go
+++ b/web/web.go
@@ -367,10 +367,33 @@ func New(logger log.Logger, o *Options) *Handler {
 	router.Get("/", func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, path.Join(o.ExternalURL.Path, homePage), http.StatusFound)
 	})
+	router.Get("/classic/", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, path.Join(o.ExternalURL.Path, "/classic/graph"), http.StatusFound)
+	})
+
+	router.Get("/classic/agent", readyf(h.agent))
+	router.Get("/classic/alerts", readyf(h.alerts))
+	router.Get("/classic/config", readyf(h.serveConfig))
+	router.Get("/classic/flags", readyf(h.flags))
+	router.Get("/classic/graph", readyf(h.graph))
+	router.Get("/classic/rules", readyf(h.rules))
+	router.Get("/classic/service-discovery", readyf(h.serviceDiscovery))
+	router.Get("/classic/status", readyf(h.status))
+	router.Get("/classic/targets", readyf(h.targets))
 
 	// The console library examples at 'console_libraries/prom.lib' still depend on old asset files being served under `classic`.
 	router.Get("/classic/static/*filepath", route.FileServe(filepath.Join(o.LocalAssets, "static")))
 
+	// Make sure that "<path-prefix>/classic" is redirected to "<path-prefix>/classic/" and
+	// not just the naked "/classic/", which would be the default behavior of the router
+	// with the "RedirectTrailingSlash" option (https://pkg.go.dev/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash),
+	// and which breaks users with a --web.route-prefix that deviates from the path derived
+	// from the external URL.
+	// See https://github.com/prometheus/prometheus/issues/6163#issuecomment-553855129.
+	router.Get("/classic", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, path.Join(o.ExternalURL.Path, "classic")+"/", http.StatusFound)
+	})
+
 	router.Get("/version", h.version)
 	router.Get("/metrics", promhttp.Handler().ServeHTTP)
 
diff --git a/web/web_classic.go b/web/web_classic.go
new file mode 100644
index 0000000..598db2a
--- /dev/null
+++ b/web/web_classic.go
@@ -0,0 +1,406 @@
+// Copyright 2013 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 web
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"runtime"
+	"sort"
+	template_text "text/template"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"github.com/prometheus/common/model"
+	"github.com/prometheus/prometheus/rules"
+	"github.com/prometheus/prometheus/scrape"
+	"github.com/prometheus/prometheus/template"
+	"github.com/prometheus/prometheus/tsdb"
+	"github.com/prometheus/prometheus/tsdb/index"
+	api_v1 "github.com/prometheus/prometheus/web/api/v1"
+)
+
+// AlertStatus bundles alerting rules and the mapping of alert states to row classes.
+type AlertStatus struct {
+	Groups               []*rules.Group
+	AlertStateToRowClass map[rules.AlertState]string
+	Counts               AlertByStateCount
+}
+
+type AlertByStateCount struct {
+	Inactive int32
+	Pending  int32
+	Firing   int32
+}
+
+func (h *Handler) agent(w http.ResponseWriter, r *http.Request) {
+	h.executeTemplate(w, "agent.html", nil)
+}
+
+func (h *Handler) alerts(w http.ResponseWriter, r *http.Request) {
+	var groups []*rules.Group
+	for _, group := range h.ruleManager.RuleGroups() {
+		if group.HasAlertingRules() {
+			groups = append(groups, group)
+		}
+	}
+
+	alertStatus := AlertStatus{
+		Groups: groups,
+		AlertStateToRowClass: map[rules.AlertState]string{
+			rules.StateInactive: "success",
+			rules.StatePending:  "warning",
+			rules.StateFiring:   "danger",
+		},
+		Counts: alertCounts(groups),
+	}
+	h.executeTemplate(w, "alerts.html", alertStatus)
+}
+
+func (h *Handler) flags(w http.ResponseWriter, r *http.Request) {
+	h.executeTemplate(w, "flags.html", h.flagsMap)
+}
+
+func (h *Handler) graph(w http.ResponseWriter, r *http.Request) {
+	h.executeTemplate(w, "graph.html", nil)
+}
+
+func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {
+	h.executeTemplate(w, "rules.html", h.ruleManager)
+}
+
+func (h *Handler) serveConfig(w http.ResponseWriter, r *http.Request) {
+	h.mtx.RLock()
+	defer h.mtx.RUnlock()
+
+	h.executeTemplate(w, "config.html", h.config.String())
+}
+
+func (h *Handler) serviceDiscovery(w http.ResponseWriter, r *http.Request) {
+	var index []string
+	targets := h.scrapeManager.TargetsAll()
+	for job := range targets {
+		index = append(index, job)
+	}
+	sort.Strings(index)
+	scrapeConfigData := struct {
+		Index   []string
+		Targets map[string][]*scrape.Target
+		Active  []int
+		Dropped []int
+		Total   []int
+	}{
+		Index:   index,
+		Targets: make(map[string][]*scrape.Target),
+		Active:  make([]int, len(index)),
+		Dropped: make([]int, len(index)),
+		Total:   make([]int, len(index)),
+	}
+	for i, job := range scrapeConfigData.Index {
+		scrapeConfigData.Targets[job] = make([]*scrape.Target, 0, len(targets[job]))
+		scrapeConfigData.Total[i] = len(targets[job])
+		for _, target := range targets[job] {
+			// Do not display more than 100 dropped targets per job to avoid
+			// returning too much data to the clients.
+			if target.Labels().Len() == 0 {
+				scrapeConfigData.Dropped[i]++
+				if scrapeConfigData.Dropped[i] > 100 {
+					continue
+				}
+			} else {
+				scrapeConfigData.Active[i]++
+			}
+			scrapeConfigData.Targets[job] = append(scrapeConfigData.Targets[job], target)
+		}
+	}
+
+	h.executeTemplate(w, "service-discovery.html", scrapeConfigData)
+}
+
+func (h *Handler) status(w http.ResponseWriter, r *http.Request) {
+	status := struct {
+		Birth               time.Time
+		CWD                 string
+		Version             *PrometheusVersion
+		Alertmanagers       []*url.URL
+		GoroutineCount      int
+		GOMAXPROCS          int
+		GOGC                string
+		GODEBUG             string
+		CorruptionCount     int64
+		ChunkCount          int64
+		TimeSeriesCount     int64
+		LastConfigTime      time.Time
+		ReloadConfigSuccess bool
+		StorageRetention    string
+		NumSeries           uint64
+		MaxTime             int64
+		MinTime             int64
+		Stats               *index.PostingsStats
+		Duration            string
+	}{
+		Birth:          h.birth,
+		CWD:            h.cwd,
+		Version:        h.versionInfo,
+		Alertmanagers:  h.notifier.Alertmanagers(),
+		GoroutineCount: runtime.NumGoroutine(),
+		GOMAXPROCS:     runtime.GOMAXPROCS(0),
+		GOGC:           os.Getenv("GOGC"),
+		GODEBUG:        os.Getenv("GODEBUG"),
+	}
+
+	if h.options.TSDBRetentionDuration != 0 {
+		status.StorageRetention = h.options.TSDBRetentionDuration.String()
+	}
+	if h.options.TSDBMaxBytes != 0 {
+		if status.StorageRetention != "" {
+			status.StorageRetention = status.StorageRetention + " or "
+		}
+		status.StorageRetention = status.StorageRetention + h.options.TSDBMaxBytes.String()
+	}
+
+	metrics, err := h.gatherer.Gather()
+	if err != nil {
+		http.Error(w, fmt.Sprintf("error gathering runtime status: %s", err), http.StatusInternalServerError)
+		return
+	}
+	for _, mF := range metrics {
+		switch *mF.Name {
+		case "prometheus_tsdb_head_chunks":
+			status.ChunkCount = int64(toFloat64(mF))
+		case "prometheus_tsdb_head_series":
+			status.TimeSeriesCount = int64(toFloat64(mF))
+		case "prometheus_tsdb_wal_corruptions_total":
+			status.CorruptionCount = int64(toFloat64(mF))
+		case "prometheus_config_last_reload_successful":
+			status.ReloadConfigSuccess = toFloat64(mF) != 0
+		case "prometheus_config_last_reload_success_timestamp_seconds":
+			status.LastConfigTime = time.Unix(int64(toFloat64(mF)), 0).UTC()
+		}
+	}
+
+	startTime := time.Now().UnixNano()
+	s, err := h.localStorage.Stats("__name__", 10)
+	if err != nil {
+		if errors.Cause(err) == tsdb.ErrNotReady {
+			http.Error(w, tsdb.ErrNotReady.Error(), http.StatusServiceUnavailable)
+			return
+		}
+		http.Error(w, fmt.Sprintf("error gathering local storage statistics: %s", err), http.StatusInternalServerError)
+		return
+	}
+	status.Duration = fmt.Sprintf("%.3f", float64(time.Now().UnixNano()-startTime)/float64(1e9))
+	status.Stats = s.IndexPostingStats
+	status.NumSeries = s.NumSeries
+	status.MaxTime = s.MaxTime
+	status.MinTime = s.MinTime
+
+	h.executeTemplate(w, "status.html", status)
+}
+
+func (h *Handler) targets(w http.ResponseWriter, r *http.Request) {
+	tps := h.scrapeManager.TargetsActive()
+	for _, targets := range tps {
+		sort.Slice(targets, func(i, j int) bool {
+			iJobLabel := targets[i].Labels().Get(model.JobLabel)
+			jJobLabel := targets[j].Labels().Get(model.JobLabel)
+			if iJobLabel == jJobLabel {
+				return targets[i].Labels().Get(model.InstanceLabel) < targets[j].Labels().Get(model.InstanceLabel)
+			}
+			return iJobLabel < jJobLabel
+		})
+	}
+
+	h.executeTemplate(w, "targets.html", struct {
+		TargetPools map[string][]*scrape.Target
+	}{
+		TargetPools: tps,
+	})
+}
+
+func (h *Handler) executeTemplate(w http.ResponseWriter, name string, data interface{}) {
+	text, err := h.getTemplate(name)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+
+	tmpl := template.NewTemplateExpander(
+		h.context,
+		text,
+		name,
+		data,
+		h.now(),
+		template.QueryFunc(rules.EngineQueryFunc(h.queryEngine, h.storage)),
+		h.options.ExternalURL,
+		nil,
+	)
+	tmpl.Funcs(tmplFuncs(h.consolesPath(), h.options))
+
+	result, err := tmpl.ExpandHTML(nil)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	io.WriteString(w, result)
+}
+
+func (h *Handler) getTemplate(name string) (string, error) {
+	var tmpl string
+
+	appendf := func(name string) error {
+		f, err := os.Open(filepath.Join(h.options.LocalAssets, "templates", name))
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		b, err := ioutil.ReadAll(f)
+		if err != nil {
+			return err
+		}
+		tmpl += string(b)
+		return nil
+	}
+
+	err := appendf("_base.html")
+	if err != nil {
+		return "", errors.Wrap(err, "error reading base template")
+	}
+	err = appendf(name)
+	if err != nil {
+		return "", errors.Wrapf(err, "error reading page template %s", name)
+	}
+
+	return tmpl, nil
+}
+
+func alertCounts(groups []*rules.Group) AlertByStateCount {
+	result := AlertByStateCount{}
+
+	for _, group := range groups {
+		for _, alert := range group.AlertingRules() {
+			switch alert.State() {
+			case rules.StateInactive:
+				result.Inactive++
+			case rules.StatePending:
+				result.Pending++
+			case rules.StateFiring:
+				result.Firing++
+			}
+		}
+	}
+	return result
+}
+
+func tmplFuncs(consolesPath string, opts *Options) template_text.FuncMap {
+	return template_text.FuncMap{
+		"since": func(t time.Time) time.Duration {
+			return time.Since(t) / time.Millisecond * time.Millisecond
+		},
+		"unixToTime": func(i int64) time.Time {
+			t := time.Unix(i/int64(time.Microsecond), 0).UTC()
+			return t
+		},
+		"consolesPath": func() string { return consolesPath },
+		"pathPrefix":   func() string { return opts.ExternalURL.Path },
+		"pageTitle":    func() string { return opts.PageTitle },
+		"buildVersion": func() string { return opts.Version.Revision },
+		"globalURL": func(u *url.URL) *url.URL {
+			host, port, err := net.SplitHostPort(u.Host)
+			if err != nil {
+				return u
+			}
+			for _, lhr := range api_v1.LocalhostRepresentations {
+				if host == lhr {
+					_, ownPort, err := net.SplitHostPort(opts.ListenAddress)
+					if err != nil {
+						return u
+					}
+
+					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.ExternalURL.Scheme
+						u.Host = opts.ExternalURL.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.ExternalURL.Host)
+						if err != nil {
+							return u
+						}
+						u.Host = host + ":" + port
+					}
+					break
+				}
+			}
+			return u
+		},
+		"numHealthy": func(pool []*scrape.Target) int {
+			alive := len(pool)
+			for _, p := range pool {
+				if p.Health() != scrape.HealthGood {
+					alive--
+				}
+			}
+
+			return alive
+		},
+		"targetHealthToClass": func(th scrape.TargetHealth) string {
+			switch th {
+			case scrape.HealthUnknown:
+				return "warning"
+			case scrape.HealthGood:
+				return "success"
+			default:
+				return "danger"
+			}
+		},
+		"ruleHealthToClass": func(rh rules.RuleHealth) string {
+			switch rh {
+			case rules.HealthUnknown:
+				return "warning"
+			case rules.HealthGood:
+				return "success"
+			default:
+				return "danger"
+			}
+		},
+		"alertStateToClass": func(as rules.AlertState) string {
+			switch as {
+			case rules.StateInactive:
+				return "success"
+			case rules.StatePending:
+				return "warning"
+			case rules.StateFiring:
+				return "danger"
+			default:
+				panic("unknown alert state")
+			}
+		},
+	}
+}
