From dc7d27ab9a2813ece3b9feac9e71301084d51348 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 22 Jun 2015 22:35:19 +0200 Subject: [PATCH 1/4] retrieval: add honor label handling and parametrized querying. This commit adds the honor_labels and params arguments to the scrape config. This allows to specify query parameters used by the scrapers and handling scraped labels with precedence. --- .../client_golang/model/labelname.go | 4 +- config/config.go | 6 ++ config/config_test.go | 1 + config/testdata/conf.good.yml | 1 + retrieval/helpers_test.go | 5 ++ retrieval/target.go | 26 +++++- retrieval/target_test.go | 85 +++++++++++++++++++ storage/local/storage.go | 5 ++ 8 files changed, 129 insertions(+), 4 deletions(-) diff --git a/Godeps/_workspace/src/github.com/prometheus/client_golang/model/labelname.go b/Godeps/_workspace/src/github.com/prometheus/client_golang/model/labelname.go index ed6915ce8..0fa5f5b4c 100644 --- a/Godeps/_workspace/src/github.com/prometheus/client_golang/model/labelname.go +++ b/Godeps/_workspace/src/github.com/prometheus/client_golang/model/labelname.go @@ -21,9 +21,9 @@ import ( ) const ( - // ExporterLabelPrefix is the label name prefix to prepend if a + // ExportedLabelPrefix is the label name prefix to prepend if a // synthetic label is already present in the exported metrics. - ExporterLabelPrefix LabelName = "exporter_" + ExportedLabelPrefix LabelName = "exported_" // MetricNameLabel is the label name indicating the metric name of a // timeseries. diff --git a/config/config.go b/config/config.go index 2fe6bd410..4f21f14e3 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/url" "regexp" "strings" "time" @@ -62,6 +63,7 @@ var ( // configured globals. MetricsPath: "/metrics", Scheme: "http", + HonorLabels: false, } // The default Relabel configuration. @@ -190,6 +192,10 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type ScrapeConfig struct { // The job name to which the job label is set by default. JobName string `yaml:"job_name"` + // Indicator whether the scraped metrics should remain unmodified. + HonorLabels bool `yaml:"honor_labels,omitempty"` + // A set of query parameters with which the target is scraped. + Params url.Values `yaml:"params,omitempty"` // How frequently to scrape the targets of this scrape config. ScrapeInterval Duration `yaml:"scrape_interval,omitempty"` // The timeout for scraping targets of this config. diff --git a/config/config_test.go b/config/config_test.go index 16fe1e119..8d902c049 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -36,6 +36,7 @@ var expectedConf = &Config{ { JobName: "prometheus", + HonorLabels: true, ScrapeInterval: Duration(15 * time.Second), ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout, diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 9b4921996..c0fefc1a7 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -16,6 +16,7 @@ rule_files: scrape_configs: - job_name: prometheus + honor_labels: true # scrape_interval is defined by the configured global (15s). # scrape_timeout is defined by the global default (10s). diff --git a/retrieval/helpers_test.go b/retrieval/helpers_test.go index f477bbd89..f72c93fe2 100644 --- a/retrieval/helpers_test.go +++ b/retrieval/helpers_test.go @@ -38,6 +38,11 @@ type collectResultAppender struct { } func (a *collectResultAppender) Append(s *clientmodel.Sample) { + for ln, lv := range s.Metric { + if len(lv) == 0 { + delete(s.Metric, ln) + } + } a.result = append(a.result, s) } diff --git a/retrieval/target.go b/retrieval/target.go index de219d9c0..86173d582 100644 --- a/retrieval/target.go +++ b/retrieval/target.go @@ -166,6 +166,9 @@ type Target struct { deadline time.Duration // The time between two scrapes. scrapeInterval time.Duration + // Whether the target's labels have precedence over the base labels + // assigned by the scraping instance. + honorLabels bool } // NewTarget creates a reasonably configured target for querying. @@ -198,11 +201,13 @@ func (t *Target) Update(cfg *config.ScrapeConfig, baseLabels, metaLabels clientm if cfg.BasicAuth != nil { t.url.User = url.UserPassword(cfg.BasicAuth.Username, cfg.BasicAuth.Password) } + t.url.RawQuery = cfg.Params.Encode() t.scrapeInterval = time.Duration(cfg.ScrapeInterval) t.deadline = time.Duration(cfg.ScrapeTimeout) t.httpClient = httputil.NewDeadlineClient(time.Duration(cfg.ScrapeTimeout)) + t.honorLabels = cfg.HonorLabels t.metaLabels = metaLabels t.baseLabels = clientmodel.LabelSet{} // All remaining internal labels will not be part of the label set. @@ -363,12 +368,29 @@ func (t *Target) scrape(sampleAppender storage.SampleAppender) (err error) { for samples := range t.ingestedSamples { for _, s := range samples { - s.Metric.MergeFromLabelSet(baseLabels, clientmodel.ExporterLabelPrefix) + if t.honorLabels { + // Merge the metric with the baseLabels for labels not already set in the + // metric. This also considers labels explicitly set to the empty string. + for ln, lv := range baseLabels { + if _, ok := s.Metric[ln]; !ok { + s.Metric[ln] = lv + } + } + } else { + // Merge the ingested metric with the base label set. On a collision the + // value of the label is stored in a label prefixed with the exported prefix. + for ln, lv := range baseLabels { + if v, ok := s.Metric[ln]; ok && v != "" { + s.Metric[clientmodel.ExportedLabelPrefix+ln] = v + } + s.Metric[ln] = lv + } + } // Avoid the copy in Relabel if there are no configs. if len(t.metricRelabelConfigs) > 0 { labels, err := Relabel(clientmodel.LabelSet(s.Metric), t.metricRelabelConfigs...) if err != nil { - log.Errorf("error while relabeling metric %s of instance %s: ", s.Metric, t.url, err) + log.Errorf("Error while relabeling metric %s of instance %s: %s", s.Metric, t.url, err) continue } // Check if the timeseries was dropped. diff --git a/retrieval/target_test.go b/retrieval/target_test.go index 87522ee0f..b16b1cd78 100644 --- a/retrieval/target_test.go +++ b/retrieval/target_test.go @@ -44,6 +44,91 @@ func TestBaseLabels(t *testing.T) { } } +func TestOverwriteLabels(t *testing.T) { + type test struct { + metric string + resultNormal clientmodel.Metric + resultHonor clientmodel.Metric + } + var tests []test + + server := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", `text/plain; version=0.0.4`) + for _, test := range tests { + w.Write([]byte(test.metric)) + w.Write([]byte(" 1\n")) + } + }, + ), + ) + defer server.Close() + addr := clientmodel.LabelValue(strings.Split(server.URL, "://")[1]) + + tests = []test{ + { + metric: `foo{}`, + resultNormal: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + clientmodel.InstanceLabel: addr, + }, + resultHonor: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + clientmodel.InstanceLabel: addr, + }, + }, + { + metric: `foo{instance=""}`, + resultNormal: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + clientmodel.InstanceLabel: addr, + }, + resultHonor: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + }, + }, + { + metric: `foo{instance="other_instance"}`, + resultNormal: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + clientmodel.InstanceLabel: addr, + clientmodel.ExportedLabelPrefix + clientmodel.InstanceLabel: "other_instance", + }, + resultHonor: clientmodel.Metric{ + clientmodel.MetricNameLabel: "foo", + clientmodel.InstanceLabel: "other_instance", + }, + }, + } + + target := newTestTarget(server.URL, 10*time.Millisecond, nil) + + target.honorLabels = false + app := &collectResultAppender{} + if err := target.scrape(app); err != nil { + t.Fatal(err) + } + + for i, test := range tests { + if !reflect.DeepEqual(app.result[i].Metric, test.resultNormal) { + t.Errorf("Error comparing %q:\nExpected:\n%s\nGot:\n%s\n", test.metric, test.resultNormal, app.result[i].Metric) + } + } + + target.honorLabels = true + app = &collectResultAppender{} + if err := target.scrape(app); err != nil { + t.Fatal(err) + } + + for i, test := range tests { + if !reflect.DeepEqual(app.result[i].Metric, test.resultHonor) { + t.Errorf("Error comparing %q:\nExpected:\n%s\nGot:\n%s\n", test.metric, test.resultHonor, app.result[i].Metric) + } + + } +} func TestTargetScrapeUpdatesState(t *testing.T) { testTarget := newTestTarget("bad schema", 0, nil) diff --git a/storage/local/storage.go b/storage/local/storage.go index 62c8a9fca..76f961bf9 100644 --- a/storage/local/storage.go +++ b/storage/local/storage.go @@ -510,6 +510,11 @@ func (s *memorySeriesStorage) DropMetricsForFingerprints(fps ...clientmodel.Fing // Append implements Storage. func (s *memorySeriesStorage) Append(sample *clientmodel.Sample) { + for ln, lv := range sample.Metric { + if len(lv) == 0 { + delete(sample.Metric, ln) + } + } if s.getNumChunksToPersist() >= s.maxChunksToPersist { log.Warnf( "%d chunks waiting for persistence, sample ingestion suspended.", From 39a82549633c3d1b499697cdac1ee758b54b6dbb Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 22 Jun 2015 22:46:55 +0200 Subject: [PATCH 2/4] web: add basic federation support. This commit adds a federation handler on /federate. It accepts `match[]` query parameters containing vector selectors. Their intersection determines the in-memory metrics that are returned in the same way as the /metrics endpoint does (modulo sorting). --- web/federate.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ web/web.go | 9 +++- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 web/federate.go diff --git a/web/federate.go b/web/federate.go new file mode 100644 index 000000000..b2db6aeae --- /dev/null +++ b/web/federate.go @@ -0,0 +1,108 @@ +package web + +import ( + "io" + "net/http" + + "bitbucket.org/ww/goautoneg" + "github.com/golang/protobuf/proto" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/text" + + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/storage/local" + + clientmodel "github.com/prometheus/client_golang/model" + dto "github.com/prometheus/client_model/go" +) + +type Federation struct { + Storage local.Storage +} + +func (fed *Federation) ServeHTTP(w http.ResponseWriter, req *http.Request) { + req.ParseForm() + + metrics := map[clientmodel.Fingerprint]clientmodel.COWMetric{} + + for _, s := range req.Form["match[]"] { + matchers, err := promql.ParseMetricSelector(s) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + for fp, met := range fed.Storage.MetricsForLabelMatchers(matchers...) { + metrics[fp] = met + } + } + + enc, contentType := chooseEncoder(req) + w.Header().Set("Content-Type", contentType) + + protMetric := &dto.Metric{ + Label: []*dto.LabelPair{}, + Untyped: &dto.Untyped{}, + } + protMetricFam := &dto.MetricFamily{ + Metric: []*dto.Metric{protMetric}, + Type: dto.MetricType_UNTYPED.Enum(), + } + + for fp, met := range metrics { + sp := fed.Storage.LastSamplePairForFingerprint(fp) + if sp == nil { + continue + } + + // Reset label slice. + protMetric.Label = protMetric.Label[:0] + + for ln, lv := range met.Metric { + if ln == clientmodel.MetricNameLabel { + protMetricFam.Name = proto.String(string(lv)) + continue + } + protMetric.Label = append(protMetric.Label, &dto.LabelPair{ + Name: proto.String(string(ln)), + Value: proto.String(string(lv)), + }) + } + protMetric.TimestampMs = (*int64)(&sp.Timestamp) + protMetric.Untyped.Value = (*float64)(&sp.Value) + + if _, err := enc(w, protMetricFam); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +type encoder func(w io.Writer, p *dto.MetricFamily) (int, error) + +func chooseEncoder(req *http.Request) (encoder, string) { + accepts := goautoneg.ParseAccept(req.Header.Get("Accept")) + for _, accept := range accepts { + switch { + case accept.Type == "application" && + accept.SubType == "vnd.google.protobuf" && + accept.Params["proto"] == "io.prometheus.client.MetricFamily": + switch accept.Params["encoding"] { + case "delimited": + return text.WriteProtoDelimited, prometheus.DelimitedTelemetryContentType + case "text": + return text.WriteProtoText, prometheus.ProtoTextTelemetryContentType + case "compact-text": + return text.WriteProtoCompactText, prometheus.ProtoCompactTextTelemetryContentType + default: + continue + } + case accept.Type == "text" && + accept.SubType == "plain" && + (accept.Params["version"] == "0.0.4" || accept.Params["version"] == ""): + return text.MetricFamilyToText, prometheus.TextTelemetryContentType + default: + continue + } + } + return text.MetricFamilyToText, prometheus.TextTelemetryContentType +} diff --git a/web/web.go b/web/web.go index 5a88be937..70136544a 100644 --- a/web/web.go +++ b/web/web.go @@ -54,8 +54,9 @@ type Handler struct { ruleManager *rules.Manager queryEngine *promql.Engine - apiV1 *v1.API - apiLegacy *legacy.API + apiV1 *v1.API + apiLegacy *legacy.API + federation *Federation router *route.Router quitCh chan struct{} @@ -125,6 +126,9 @@ func New(st local.Storage, qe *promql.Engine, rm *rules.Manager, status *Prometh Storage: st, Now: clientmodel.Now, }, + federation: &Federation{ + Storage: st, + }, } if o.PathPrefix != "" { @@ -145,6 +149,7 @@ func New(st local.Storage, qe *promql.Engine, rm *rules.Manager, status *Prometh router.Get("/heap", instrf("heap", dumpHeap)) + router.Get("/federate", instrh("federate", h.federation)) router.Get(o.MetricsPath, prometheus.Handler().ServeHTTP) h.apiLegacy.Register(router.WithPrefix("/api")) From 6bfb4549a689cfbd68d348dd0acb98d1dad0903b Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 22 Jun 2015 22:50:47 +0200 Subject: [PATCH 3/4] storage: add LastSamplePairForFingerprint method --- storage/local/chunk.go | 14 ++++++++++++++ storage/local/interface.go | 3 +++ storage/local/storage.go | 14 +++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/storage/local/chunk.go b/storage/local/chunk.go index 52e13eece..112198c0d 100644 --- a/storage/local/chunk.go +++ b/storage/local/chunk.go @@ -142,6 +142,20 @@ func (cd *chunkDesc) lastTime() clientmodel.Timestamp { return cd.c.newIterator().lastTimestamp() } +func (cd *chunkDesc) lastSamplePair() *metric.SamplePair { + cd.Lock() + defer cd.Unlock() + + if cd.c == nil { + return nil + } + it := cd.c.newIterator() + return &metric.SamplePair{ + Timestamp: it.lastTimestamp(), + Value: it.lastSampleValue(), + } +} + func (cd *chunkDesc) isEvicted() bool { cd.Lock() defer cd.Unlock() diff --git a/storage/local/interface.go b/storage/local/interface.go index 71fa5ad83..245fe33cc 100644 --- a/storage/local/interface.go +++ b/storage/local/interface.go @@ -40,6 +40,9 @@ type Storage interface { // label matchers. At least one label matcher must be specified that does not // match the empty string. MetricsForLabelMatchers(matchers ...*metric.LabelMatcher) map[clientmodel.Fingerprint]clientmodel.COWMetric + // LastSamplePairForFingerprint returns the last sample pair for the provided fingerprint. + // If the respective time series is evicted, nil is returned. + LastSamplePairForFingerprint(clientmodel.Fingerprint) *metric.SamplePair // Get all of the label values that are associated with a given label name. LabelValuesForLabelName(clientmodel.LabelName) clientmodel.LabelValues // Get the metric associated with the provided fingerprint. diff --git a/storage/local/storage.go b/storage/local/storage.go index 76f961bf9..66b7aa961 100644 --- a/storage/local/storage.go +++ b/storage/local/storage.go @@ -301,7 +301,7 @@ func (s *memorySeriesStorage) WaitForIndexing() { s.persistence.waitForIndexing() } -// NewIterator implements storage. +// NewIterator implements Storage. func (s *memorySeriesStorage) NewIterator(fp clientmodel.Fingerprint) SeriesIterator { s.fpLocker.Lock(fp) defer s.fpLocker.Unlock(fp) @@ -321,6 +321,18 @@ func (s *memorySeriesStorage) NewIterator(fp clientmodel.Fingerprint) SeriesIter } } +// LastSampleForFingerprint implements Storage. +func (s *memorySeriesStorage) LastSamplePairForFingerprint(fp clientmodel.Fingerprint) *metric.SamplePair { + s.fpLocker.Lock(fp) + defer s.fpLocker.Unlock(fp) + + series, ok := s.fpToSeries.get(fp) + if !ok { + return nil + } + return series.head().lastSamplePair() +} + // boundedIterator wraps a SeriesIterator and does not allow fetching // data from earlier than the configured start time. type boundedIterator struct { From 53b9d5917dce9dedeff9351e900d75d41a6f0b00 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Mon, 22 Jun 2015 22:57:32 +0200 Subject: [PATCH 4/4] web: improve target URL handling and display. --- retrieval/target.go | 10 ++++++---- web/blob/files.go | 10 +++++----- web/blob/templates/status.html | 7 ++++++- web/web.go | 10 ++++++---- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/retrieval/target.go b/retrieval/target.go index 86173d582..c51f5048c 100644 --- a/retrieval/target.go +++ b/retrieval/target.go @@ -336,7 +336,7 @@ func (t *Target) scrape(sampleAppender storage.SampleAppender) (err error) { recordScrapeHealth(sampleAppender, clientmodel.TimestampFromTime(start), baseLabels, t.status.Health(), time.Since(start)) }() - req, err := http.NewRequest("GET", t.URL(), nil) + req, err := http.NewRequest("GET", t.URL().String(), nil) if err != nil { panic(err) } @@ -405,11 +405,13 @@ func (t *Target) scrape(sampleAppender storage.SampleAppender) (err error) { return err } -// URL implements Target. -func (t *Target) URL() string { +// URL returns a copy of the target's URL. +func (t *Target) URL() *url.URL { t.RLock() defer t.RUnlock() - return t.url.String() + u := &url.URL{} + *u = *t.url + return u } // InstanceIdentifier returns the identifier for the target. diff --git a/web/blob/files.go b/web/blob/files.go index 4e4cb9b3a..a19411228 100644 --- a/web/blob/files.go +++ b/web/blob/files.go @@ -165,7 +165,7 @@ func templatesGraphHtml() (*asset, error) { return a, nil } -var _templatesStatusHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x56\xcb\x6e\xdb\x3a\x10\xdd\xfb\x2b\x78\x85\x2c\xaf\x6c\xe0\x02\x77\x13\xd8\x5e\x38\x48\x91\x00\x4e\x11\xe4\xb1\xe9\x26\xa0\xa4\xb1\xc4\x96\x21\x05\x92\x4a\x13\x28\xfa\xf7\xce\x50\x92\x2d\xeb\xd1\x06\x7d\xa0\xd9\x08\x1c\x72\x34\x8f\x73\xce\x50\x2a\xcb\x04\x76\x42\x01\x0b\x32\xe0\x49\x50\x55\xcb\x7f\xc2\x90\x29\xf1\xcc\xc2\x70\x5d\x96\xa0\x92\xaa\x9a\xcd\x0e\x5e\xb1\x56\x0e\x94\x43\xc7\x19\x63\xcb\x44\x3c\xb1\x58\x72\x6b\x57\xfe\x80\xa3\x8b\x09\x77\xb2\x10\x49\xb0\xc6\x73\xf4\xc8\xfe\x5b\xdf\x14\xca\x89\x47\x60\x97\x6a\xa7\xcd\x23\x77\x42\xab\xe5\x02\xf7\x6b\x07\xc7\x23\x09\x6d\x90\xda\xf0\xcf\x10\x03\x26\xa0\x2c\x24\x8d\x1d\x69\x93\x80\xd9\x9b\xd6\x19\x91\xef\xad\x4c\x3f\x81\x69\x72\x52\xd0\x48\x27\x2f\xad\x45\xb6\x39\x18\x64\x66\xeb\xfb\x9c\x6a\x5a\x2e\x70\x79\x74\x92\x60\xd3\xf3\x5b\xc7\x5d\x61\xe7\x1b\x61\x5c\x86\x88\x2c\x70\xf7\x10\x6b\x71\x08\x86\xeb\x43\x22\x34\xa8\x94\xf5\x6c\xdf\xf8\xa6\x10\x32\xf9\x8b\x6d\x97\xa5\xe1\x2a\x05\x76\xf2\x05\x5e\xfe\x65\x27\x4f\x5c\x16\xc0\x4e\x57\x6c\x4e\x25\x79\x02\xa7\xe0\x61\x36\xd6\x39\xac\x02\xa3\xbf\x06\x08\x08\x05\xf0\x38\x8c\x80\x55\x87\xfd\x1e\x4a\x54\x48\xad\xa3\x37\xa1\x76\xa6\xd5\x4e\xa4\x85\xe9\x23\x96\x1b\xe8\x70\x53\x7b\x51\x5a\xda\x9f\x75\xc4\x26\xc1\x0e\xde\xaa\x71\x88\xb9\x94\xac\x0d\xe0\x1d\xab\x0a\x23\x5e\xdc\x5d\x6d\x6f\x95\xc8\x73\x70\x2c\xe7\x2e\xbb\x36\xa8\xf5\x67\x0c\x1d\x99\x45\x3b\x02\xfd\x34\x77\xdc\xa4\xe0\x3a\x89\xfe\x10\xa5\x1d\x12\x3f\xeb\x08\x49\xcc\xb5\x96\xc4\xe1\x51\x2f\x75\x35\xd7\x78\x64\x3b\xb4\x7a\x26\x71\xa8\xbb\x9c\xd5\x64\x13\xc3\x31\x3a\xe7\x5c\xad\x82\xff\x83\xb6\x66\xcc\xf0\x40\x2f\x50\x7e\x24\x16\xcd\x86\xf4\x63\x36\x47\x24\xd3\x24\x5b\x9f\xab\x24\xd7\x42\xb9\xbe\x54\xda\x73\xaa\x77\x30\x74\xed\xe1\x86\x5b\x60\x5b\x1e\x81\xb4\x53\x2e\x5b\x6e\x1d\xbb\x8d\x0d\xcf\x27\xa3\x9c\x1b\xa3\xcd\xf0\xb0\xdf\x02\x79\xf4\xb0\xe9\x8f\x4f\x07\x7b\x42\xfd\x08\xd9\x09\x04\x92\xfe\x16\x6e\x72\x96\xa1\xa0\x56\x01\x2a\xed\xfe\x66\xcb\x5e\x59\x2a\x75\xc4\x25\xae\xab\x8a\x70\x9e\xfb\xd5\x72\xc1\x07\xe1\x16\xc3\x78\xe3\x29\x88\xc8\x96\x44\x2e\xc1\x38\xe6\x9f\x61\x59\xee\x25\x72\x01\x5c\x22\xeb\xaf\x2c\xf3\x8b\x3b\x7d\x46\xee\xac\xaa\x50\x76\x24\x9e\x07\xeb\xfd\x1e\xfc\x8b\xc1\x30\x07\x81\x71\x1c\xab\x87\x47\x5d\x31\x55\xf2\x7b\xfa\x88\x0b\x63\xb5\x09\xbd\x9c\x50\x90\x2c\xe1\x8e\x87\x4e\xa7\xa9\xc4\x7b\xc9\x21\x1f\x4e\xe4\x01\x73\xc2\x91\xdd\x1c\x6b\x23\x52\xa1\xb8\x0c\x9b\xed\x0d\xe0\xd5\x0b\xcc\x80\x24\x55\x09\x95\x9e\x52\x17\x57\xe0\x78\x2d\x33\x22\x60\xb4\xd3\x93\x08\xb5\x58\xfb\xd0\xb0\xf9\x09\x6d\xcc\xf9\xe6\x70\x44\x13\x13\xb0\x40\x28\x04\x4f\xc5\x10\x8c\x40\x42\xd1\xc4\x8e\x75\x02\x8e\x3a\x75\xb4\xe6\x6b\xed\x5e\xd7\x3f\x7e\xb7\x07\x9d\x8f\xc0\xfc\x33\xcc\x8d\x78\xe4\xe6\xc5\x0f\xb4\xdf\xa9\x2a\x92\x62\x7b\x6b\x07\xe3\x94\xb5\x25\x75\xaf\xed\xde\x89\xb4\x30\x51\xcd\x64\x2d\xf8\x0f\xc1\x0b\x89\xea\x52\x5a\xc1\x74\xe2\xa9\xb4\xbf\xa8\x2e\xcf\x43\x2b\x61\xba\x48\xea\x7b\x64\x7e\x69\x3f\x81\xc1\xcf\xe1\x47\xc0\x8b\xb7\x6d\xac\x2c\xad\x40\x46\x47\xfc\x71\x62\x78\xaa\xc7\x6b\xfc\xf9\x5a\xfc\xad\x35\xd6\xf3\xd4\x68\x27\x24\x16\x33\x3e\xbc\x87\x49\xed\x84\x9e\xc2\xfb\xad\x9d\xf4\x2f\xd0\xe1\x7b\x47\xdf\xf5\xa1\xcb\xf0\x4b\x8f\x45\x1a\x57\xe4\xec\x83\xe4\xa9\x7d\x2f\xff\x46\x0d\x72\xbe\xa6\xf7\xf6\x8f\x44\x4b\xfc\xe5\x5e\xcf\x5a\xe7\x6f\x01\x00\x00\xff\xff\x50\x97\xbd\xdc\xbe\x0b\x00\x00") +var _templatesStatusHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x57\xcd\x8e\xdb\x36\x10\xbe\xfb\x29\x58\x61\x8f\x95\x05\x14\xe8\x65\x61\xeb\xe0\x20\xc5\x06\xd8\x14\xdb\xec\xe6\xd2\xcb\x82\x92\xc6\x12\x5b\x9a\x14\x48\x6a\x1b\x43\xd1\xbb\x77\x86\x92\x6c\xfd\x76\xdb\x06\x41\x72\x31\x38\xe4\x70\x7e\xbe\xf9\x66\x44\xd7\x75\x06\x47\xa1\x80\x05\x05\xf0\x2c\x68\x9a\xdd\x0f\x61\xc8\x94\xf8\xc4\xc2\x30\xae\x6b\x50\x59\xd3\x6c\x36\x57\xad\x54\x2b\x07\xca\xa1\xe2\x86\xb1\x5d\x26\x5e\x58\x2a\xb9\xb5\x7b\x7f\xc0\x51\xc5\x84\x47\x59\x89\x2c\x88\xf1\x1c\x35\x8a\x9f\xe2\x0f\x95\x72\xe2\x04\xec\x9d\x3a\x6a\x73\xe2\x4e\x68\xb5\x8b\x70\xbf\x55\x70\x3c\x91\xd0\x1b\x69\x05\xff\x1b\xa2\xc1\x0c\x94\x85\xac\x93\x13\x6d\x32\x30\x17\xd1\x3a\x23\xca\x8b\x54\xe8\x17\x30\x9d\x4f\x32\x9a\xe8\xec\xdc\x4b\x24\x9b\xab\x40\x62\x11\x7f\x2c\x29\xa6\x5d\x84\xcb\xd1\x49\x86\x49\x6f\x1f\x1d\x77\x95\xdd\x1e\x84\x71\x05\x22\x12\xe1\xee\xd5\x56\x74\x35\x86\xeb\xab\x23\x14\x28\x94\x78\x73\x49\xfc\x50\x09\x99\x7d\xc3\xb4\xeb\xda\x70\x95\x03\xbb\xf9\x13\xce\x3f\xb2\x9b\x17\x2e\x2b\x60\xb7\x7b\xb6\xa5\x90\x7c\x01\xd7\xe0\x61\x36\xd5\x25\xec\x03\xa3\xff\x0a\x10\x10\x32\xe0\x71\x58\x00\xab\x35\xfb\x4f\x28\x51\x20\x2d\x8f\xfe\x15\x6a\x6f\xb4\x3a\x8a\xbc\x32\x53\xc4\x4a\x03\x83\xda\xb4\x5a\xe4\x96\xf6\x37\x03\xb2\x49\xb0\xb3\x5b\x2d\x0e\x29\x97\x92\xf5\x06\xbc\x62\xd3\xa0\xc5\xbb\xa7\xf7\xf7\x8f\x4a\x94\x25\x38\x56\x72\x57\x3c\x18\xe4\xfa\x27\x34\x9d\x98\xa8\x6f\x81\xa9\x9b\x27\x6e\x72\x70\x03\x47\x5f\xa9\xa4\x83\x22\xfe\xa1\x13\x2c\x62\xa9\xb5\xa4\x1a\x8e\x72\x69\xa3\x79\xc0\x23\x3b\x28\xab\xaf\x24\x36\xf5\xb0\x66\x6d\xb1\xa9\xc2\x29\x2a\x97\x5c\xed\x83\x9f\x83\x3e\x66\xf4\xf0\x4c\x17\xc8\x3f\x16\x16\xc5\xae\xe8\xe3\x6a\x2e\x50\xa6\x73\x16\xbf\x55\x59\xa9\x85\x72\x53\xaa\xf4\xe7\x14\xef\xac\xe9\xfa\xc3\x03\xb7\xc0\xee\x79\x02\xd2\xae\xa9\xdc\x73\xeb\xd8\x63\x6a\x78\xb9\x6a\xe5\xad\x31\xda\xcc\x0f\xa7\x29\x90\xc6\x04\x9b\x69\xfb\x0c\xb0\x27\xd4\x47\xc8\xae\x20\x90\xcd\xb6\x38\x2b\x90\x4e\xfb\x00\x79\xf6\xf1\xc3\x3d\xfb\xcc\x72\xa9\x13\x2e\x71\xdd\x34\x84\x32\xed\x6e\x1f\xd3\x02\x4e\xd8\x43\xb7\x51\xd4\xed\xdc\x69\xeb\x3c\x39\x49\x78\xe0\xed\x18\xe2\x31\x52\x72\xea\x61\x10\xa5\x24\xec\xfa\x46\xb7\xbe\xd3\xe9\xfa\x6f\x15\x98\x33\x9b\x84\x3f\xba\x28\x86\xd3\xa1\xbb\xbe\xa0\xbf\x23\xc2\xf4\x64\xf1\xce\x98\xff\x0d\x4b\x23\x4e\xdc\x9c\x3d\x6b\xfc\x4e\xd3\x50\xc6\xfd\x68\x08\x76\x11\xdd\x5c\x8a\x7c\x38\x17\x5e\xdb\x1f\x4f\x98\x55\xc8\x27\x71\x72\x09\xc6\x31\xff\x1b\xd6\xf5\xa5\x65\xee\x80\x4b\xec\x82\xcf\xac\xf0\x8b\x27\xfd\x86\xd4\x11\x25\x6c\x43\x6a\xa6\x67\xeb\xf5\x9e\xfd\xc5\x60\xee\x83\x82\x1c\xdb\x5a\x02\x6c\x31\xef\xff\x97\x47\x5a\x19\xab\x4d\xe8\xdb\x0b\x1b\x94\x65\xdc\xf1\xd0\xe9\x3c\x97\x38\xa7\x1d\xf2\xd3\x89\x32\x60\x4e\x38\x92\xbb\x63\x6d\x44\x2e\x14\x97\x61\xb7\x7d\x00\xfc\x14\x01\x33\xe0\x8b\x24\x54\x7e\x4b\x59\xbc\x07\xc7\xdb\xb6\x23\x4a\x2e\x66\x7a\x93\x60\x6f\xb6\x3a\x44\x11\x3f\xb1\x3a\x71\x7b\xb8\x1e\xd1\x04\x09\x58\x20\x14\x82\xa7\x52\x08\x16\x20\x21\x6b\xe2\xc8\x06\x06\x17\x95\xd6\x58\xed\x09\xfa\xea\xdd\xaf\x40\xd5\x36\xa4\x65\xba\xfa\x13\x69\x61\x25\x9a\xd5\x58\xf0\x4d\xc5\x2b\x89\xec\x52\x5a\xc1\xba\xe3\x35\xb7\x5f\xc8\x2e\x5f\x87\x9e\xc2\x34\x58\xdb\xb9\xba\x7d\x67\x7f\x07\x83\xcf\x83\x5f\x01\x3f\x44\x7d\x62\x75\x6d\x05\x56\x74\x41\x1f\x3b\x86\xe7\xfa\x0b\x3b\x76\x16\x8b\x9f\xe2\xaf\x8d\xa0\x61\x6b\x67\x44\x16\xb3\xdc\xbc\xd7\x4e\x1d\x98\xfe\x6f\x33\x69\x9e\xc9\xf4\x83\x32\xbf\x37\x7a\xe7\xcc\x55\xe6\x2f\x1f\x0c\xd2\xb8\xaa\x64\xbf\x48\x9e\xdb\xef\xe5\xad\xd8\x21\xe7\x63\xfa\xde\xde\x8c\xb4\xc4\xbf\x20\xf1\xa6\x57\xfe\x3b\x00\x00\xff\xff\x9d\x54\xec\xc3\xce\x0c\x00\x00") func templatesStatusHtmlBytes() ([]byte, error) { return bindataRead( @@ -180,7 +180,7 @@ func templatesStatusHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/status.html", size: 3006, mode: os.FileMode(420), modTime: time.Unix(1434457438, 0)} + info := bindataFileInfo{name: "templates/status.html", size: 3278, mode: os.FileMode(420), modTime: time.Unix(1435006390, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -220,7 +220,7 @@ func staticCssGraphCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/css/graph.css", size: 2668, mode: os.FileMode(420), modTime: time.Unix(1434193569, 0)} + info := bindataFileInfo{name: "static/css/graph.css", size: 2668, mode: os.FileMode(420), modTime: time.Unix(1435004178, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -320,7 +320,7 @@ func staticJsGraphJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/graph.js", size: 19251, mode: os.FileMode(420), modTime: time.Unix(1434193569, 0)} + info := bindataFileInfo{name: "static/js/graph.js", size: 19251, mode: os.FileMode(420), modTime: time.Unix(1435004178, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -340,7 +340,7 @@ func staticJsGraph_templateHandlebar() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/graph_template.handlebar", size: 6365, mode: os.FileMode(420), modTime: time.Unix(1434193569, 0)} + info := bindataFileInfo{name: "static/js/graph_template.handlebar", size: 6365, mode: os.FileMode(420), modTime: time.Unix(1435004178, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/web/blob/templates/status.html b/web/blob/templates/status.html index 5aced8667..becba8f6c 100644 --- a/web/blob/templates/status.html +++ b/web/blob/templates/status.html @@ -47,7 +47,12 @@ {{range $pool}} - {{.URL}} + {{.URL.Scheme}}://{{.URL.Host}}{{.URL.Path}}
+ {{range $label, $values := .URL.Query }} + {{range $i, $value := $values}} + {{$label}}="{{$value}}" + {{end}} + {{end}} diff --git a/web/web.go b/web/web.go index 70136544a..844d0cda3 100644 --- a/web/web.go +++ b/web/web.go @@ -334,11 +334,13 @@ func (h *Handler) getTemplate(name string) (*template_std.Template, error) { } return lset }, - "globalURL": func(url string) string { - for _, localhostRepresentation := range localhostRepresentations { - url = strings.Replace(url, "//"+localhostRepresentation, "//"+h.options.Hostname, 1) + "globalURL": func(u *url.URL) *url.URL { + for _, lhr := range localhostRepresentations { + if strings.HasPrefix(u.Host, lhr+":") { + u.Host = strings.Replace(u.Host, lhr+":", h.options.Hostname+":", 1) + } } - return url + return u }, "healthToClass": func(th retrieval.TargetHealth) string { switch th {