1// Copyright 2013 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package web 15 16import ( 17 "bytes" 18 "encoding/json" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net" 23 "net/http" 24 "net/http/pprof" 25 "net/url" 26 "os" 27 "path" 28 "path/filepath" 29 "sort" 30 "strings" 31 "sync" 32 "sync/atomic" 33 "time" 34 35 pprof_runtime "runtime/pprof" 36 template_text "text/template" 37 38 "github.com/opentracing-contrib/go-stdlib/nethttp" 39 "github.com/opentracing/opentracing-go" 40 "github.com/prometheus/client_golang/prometheus" 41 "github.com/prometheus/common/log" 42 "github.com/prometheus/common/model" 43 "github.com/prometheus/common/route" 44 "golang.org/x/net/context" 45 "golang.org/x/net/netutil" 46 47 "github.com/prometheus/prometheus/config" 48 "github.com/prometheus/prometheus/notifier" 49 "github.com/prometheus/prometheus/promql" 50 "github.com/prometheus/prometheus/retrieval" 51 "github.com/prometheus/prometheus/rules" 52 "github.com/prometheus/prometheus/storage/local" 53 "github.com/prometheus/prometheus/template" 54 "github.com/prometheus/prometheus/util/httputil" 55 api_v1 "github.com/prometheus/prometheus/web/api/v1" 56 "github.com/prometheus/prometheus/web/ui" 57) 58 59var localhostRepresentations = []string{"127.0.0.1", "localhost"} 60 61// Handler serves various HTTP endpoints of the Prometheus server 62type Handler struct { 63 targetManager *retrieval.TargetManager 64 ruleManager *rules.Manager 65 queryEngine *promql.Engine 66 context context.Context 67 storage local.Storage 68 notifier *notifier.Notifier 69 70 apiV1 *api_v1.API 71 72 router *route.Router 73 listenErrCh chan error 74 quitCh chan struct{} 75 reloadCh chan chan error 76 options *Options 77 config *config.Config 78 versionInfo *PrometheusVersion 79 birth time.Time 80 cwd string 81 flagsMap map[string]string 82 83 mtx sync.RWMutex 84 now func() model.Time 85 86 ready uint32 // ready is uint32 rather than boolean to be able to use atomic functions. 87} 88 89// ApplyConfig updates the config field of the Handler struct 90func (h *Handler) ApplyConfig(conf *config.Config) error { 91 h.mtx.Lock() 92 defer h.mtx.Unlock() 93 94 h.config = conf 95 96 return nil 97} 98 99// PrometheusVersion contains build information about Prometheus. 100type PrometheusVersion struct { 101 Version string `json:"version"` 102 Revision string `json:"revision"` 103 Branch string `json:"branch"` 104 BuildUser string `json:"buildUser"` 105 BuildDate string `json:"buildDate"` 106 GoVersion string `json:"goVersion"` 107} 108 109// Options for the web Handler. 110type Options struct { 111 Context context.Context 112 Storage local.Storage 113 QueryEngine *promql.Engine 114 TargetManager *retrieval.TargetManager 115 RuleManager *rules.Manager 116 Notifier *notifier.Notifier 117 Version *PrometheusVersion 118 Flags map[string]string 119 120 ListenAddress string 121 ReadTimeout time.Duration 122 MaxConnections int 123 ExternalURL *url.URL 124 RoutePrefix string 125 MetricsPath string 126 UseLocalAssets bool 127 UserAssetsPath string 128 ConsoleTemplatesPath string 129 ConsoleLibrariesPath string 130 EnableQuit bool 131} 132 133// New initializes a new web Handler. 134func New(o *Options) *Handler { 135 router := route.New() 136 cwd, err := os.Getwd() 137 138 if err != nil { 139 cwd = "<error retrieving current working directory>" 140 } 141 142 h := &Handler{ 143 router: router, 144 listenErrCh: make(chan error), 145 quitCh: make(chan struct{}), 146 reloadCh: make(chan chan error), 147 options: o, 148 versionInfo: o.Version, 149 birth: time.Now(), 150 cwd: cwd, 151 flagsMap: o.Flags, 152 153 context: o.Context, 154 targetManager: o.TargetManager, 155 ruleManager: o.RuleManager, 156 queryEngine: o.QueryEngine, 157 storage: o.Storage, 158 notifier: o.Notifier, 159 160 now: model.Now, 161 162 ready: 0, 163 } 164 165 h.apiV1 = api_v1.NewAPI( 166 o.QueryEngine, 167 o.Storage, 168 o.TargetManager, 169 o.Notifier, 170 func() config.Config { 171 h.mtx.RLock() 172 defer h.mtx.RUnlock() 173 return *h.config 174 }, 175 h.testReady, 176 ) 177 178 if o.RoutePrefix != "/" { 179 // If the prefix is missing for the root path, prepend it. 180 router.Get("/", func(w http.ResponseWriter, r *http.Request) { 181 http.Redirect(w, r, o.RoutePrefix, http.StatusFound) 182 }) 183 router = router.WithPrefix(o.RoutePrefix) 184 } 185 186 instrh := prometheus.InstrumentHandler 187 instrf := prometheus.InstrumentHandlerFunc 188 readyf := h.testReady 189 190 router.Get("/", func(w http.ResponseWriter, r *http.Request) { 191 http.Redirect(w, r, path.Join(o.ExternalURL.Path, "/graph"), http.StatusFound) 192 }) 193 194 router.Get("/alerts", readyf(instrf("alerts", h.alerts))) 195 router.Get("/graph", readyf(instrf("graph", h.graph))) 196 router.Get("/status", readyf(instrf("status", h.status))) 197 router.Get("/flags", readyf(instrf("flags", h.flags))) 198 router.Get("/config", readyf(instrf("config", h.serveConfig))) 199 router.Get("/rules", readyf(instrf("rules", h.rules))) 200 router.Get("/targets", readyf(instrf("targets", h.targets))) 201 router.Get("/version", readyf(instrf("version", h.version))) 202 203 router.Get("/heap", readyf(instrf("heap", dumpHeap))) 204 205 router.Get(o.MetricsPath, readyf(prometheus.Handler().ServeHTTP)) 206 207 router.Get("/federate", readyf(instrh("federate", httputil.CompressionHandler{ 208 Handler: http.HandlerFunc(h.federation), 209 }))) 210 211 h.apiV1.Register(router.WithPrefix("/api/v1")) 212 213 router.Get("/consoles/*filepath", readyf(instrf("consoles", h.consoles))) 214 215 router.Get("/static/*filepath", readyf(instrf("static", serveStaticAsset))) 216 217 if o.UserAssetsPath != "" { 218 router.Get("/user/*filepath", readyf(instrf("user", route.FileServe(o.UserAssetsPath)))) 219 } 220 221 if o.EnableQuit { 222 router.Post("/-/quit", readyf(h.quit)) 223 } 224 225 router.Post("/-/reload", readyf(h.reload)) 226 router.Get("/-/reload", func(w http.ResponseWriter, r *http.Request) { 227 w.WriteHeader(http.StatusMethodNotAllowed) 228 fmt.Fprintf(w, "This endpoint requires a POST request.\n") 229 }) 230 231 router.Get("/debug/*subpath", readyf(serveDebug)) 232 router.Post("/debug/*subpath", readyf(serveDebug)) 233 234 router.Get("/-/healthy", func(w http.ResponseWriter, r *http.Request) { 235 w.WriteHeader(http.StatusOK) 236 fmt.Fprintf(w, "Prometheus is Healthy.\n") 237 }) 238 router.Get("/-/ready", readyf(func(w http.ResponseWriter, r *http.Request) { 239 w.WriteHeader(http.StatusOK) 240 fmt.Fprintf(w, "Prometheus is Ready.\n") 241 })) 242 243 return h 244} 245 246func serveDebug(w http.ResponseWriter, req *http.Request) { 247 ctx := req.Context() 248 subpath := route.Param(ctx, "subpath") 249 250 if subpath == "/pprof" { 251 http.Redirect(w, req, req.URL.Path+"/", http.StatusMovedPermanently) 252 return 253 } 254 255 if !strings.HasPrefix(subpath, "/pprof/") { 256 http.NotFound(w, req) 257 return 258 } 259 subpath = strings.TrimPrefix(subpath, "/pprof/") 260 261 switch subpath { 262 case "cmdline": 263 pprof.Cmdline(w, req) 264 case "profile": 265 pprof.Profile(w, req) 266 case "symbol": 267 pprof.Symbol(w, req) 268 case "trace": 269 pprof.Trace(w, req) 270 default: 271 req.URL.Path = "/debug/pprof/" + subpath 272 pprof.Index(w, req) 273 } 274} 275 276func serveStaticAsset(w http.ResponseWriter, req *http.Request) { 277 fp := route.Param(req.Context(), "filepath") 278 fp = filepath.Join("web/ui/static", fp) 279 280 info, err := ui.AssetInfo(fp) 281 if err != nil { 282 log.With("file", fp).Warn("Could not get file info: ", err) 283 w.WriteHeader(http.StatusNotFound) 284 return 285 } 286 file, err := ui.Asset(fp) 287 if err != nil { 288 if err != io.EOF { 289 log.With("file", fp).Warn("Could not get file: ", err) 290 } 291 w.WriteHeader(http.StatusNotFound) 292 return 293 } 294 295 http.ServeContent(w, req, info.Name(), info.ModTime(), bytes.NewReader(file)) 296} 297 298// Ready sets Handler to be ready. 299func (h *Handler) Ready() { 300 atomic.StoreUint32(&h.ready, 1) 301} 302 303// Verifies whether the server is ready or not. 304func (h *Handler) isReady() bool { 305 ready := atomic.LoadUint32(&h.ready) 306 if ready == 0 { 307 return false 308 } 309 return true 310} 311 312// Checks if server is ready, calls f if it is, returns 503 if it is not. 313func (h *Handler) testReady(f http.HandlerFunc) http.HandlerFunc { 314 return func(w http.ResponseWriter, r *http.Request) { 315 if h.isReady() { 316 f(w, r) 317 } else { 318 w.WriteHeader(http.StatusServiceUnavailable) 319 fmt.Fprintf(w, "Service Unavailable") 320 } 321 } 322} 323 324// ListenError returns the receive-only channel that signals errors while starting the web server. 325func (h *Handler) ListenError() <-chan error { 326 return h.listenErrCh 327} 328 329// Quit returns the receive-only quit channel. 330func (h *Handler) Quit() <-chan struct{} { 331 return h.quitCh 332} 333 334// Reload returns the receive-only channel that signals configuration reload requests. 335func (h *Handler) Reload() <-chan chan error { 336 return h.reloadCh 337} 338 339// Run serves the HTTP endpoints. 340func (h *Handler) Run() { 341 log.Infof("Listening on %s", h.options.ListenAddress) 342 operationName := nethttp.OperationNameFunc(func(r *http.Request) string { 343 return fmt.Sprintf("%s %s", r.Method, r.URL.Path) 344 }) 345 server := &http.Server{ 346 Addr: h.options.ListenAddress, 347 Handler: nethttp.Middleware(opentracing.GlobalTracer(), h.router, operationName), 348 ErrorLog: log.NewErrorLogger(), 349 ReadTimeout: h.options.ReadTimeout, 350 } 351 listener, err := net.Listen("tcp", h.options.ListenAddress) 352 if err != nil { 353 h.listenErrCh <- err 354 } else { 355 limitedListener := netutil.LimitListener(listener, h.options.MaxConnections) 356 h.listenErrCh <- server.Serve(limitedListener) 357 } 358} 359 360func (h *Handler) alerts(w http.ResponseWriter, r *http.Request) { 361 alerts := h.ruleManager.AlertingRules() 362 alertsSorter := byAlertStateAndNameSorter{alerts: alerts} 363 sort.Sort(alertsSorter) 364 365 alertStatus := AlertStatus{ 366 AlertingRules: alertsSorter.alerts, 367 AlertStateToRowClass: map[rules.AlertState]string{ 368 rules.StateInactive: "success", 369 rules.StatePending: "warning", 370 rules.StateFiring: "danger", 371 }, 372 } 373 h.executeTemplate(w, "alerts.html", alertStatus) 374} 375 376func (h *Handler) consoles(w http.ResponseWriter, r *http.Request) { 377 ctx := r.Context() 378 name := route.Param(ctx, "filepath") 379 380 file, err := http.Dir(h.options.ConsoleTemplatesPath).Open(name) 381 if err != nil { 382 http.Error(w, err.Error(), http.StatusNotFound) 383 return 384 } 385 text, err := ioutil.ReadAll(file) 386 if err != nil { 387 http.Error(w, err.Error(), http.StatusInternalServerError) 388 return 389 } 390 391 // Provide URL parameters as a map for easy use. Advanced users may have need for 392 // parameters beyond the first, so provide RawParams. 393 rawParams, err := url.ParseQuery(r.URL.RawQuery) 394 if err != nil { 395 http.Error(w, err.Error(), http.StatusBadRequest) 396 return 397 } 398 params := map[string]string{} 399 for k, v := range rawParams { 400 params[k] = v[0] 401 } 402 data := struct { 403 RawParams url.Values 404 Params map[string]string 405 Path string 406 }{ 407 RawParams: rawParams, 408 Params: params, 409 Path: strings.TrimLeft(name, "/"), 410 } 411 412 tmpl := template.NewTemplateExpander(h.context, string(text), "__console_"+name, data, h.now(), h.queryEngine, h.options.ExternalURL) 413 filenames, err := filepath.Glob(h.options.ConsoleLibrariesPath + "/*.lib") 414 if err != nil { 415 http.Error(w, err.Error(), http.StatusInternalServerError) 416 return 417 } 418 result, err := tmpl.ExpandHTML(filenames) 419 if err != nil { 420 http.Error(w, err.Error(), http.StatusInternalServerError) 421 return 422 } 423 io.WriteString(w, result) 424} 425 426func (h *Handler) graph(w http.ResponseWriter, r *http.Request) { 427 h.executeTemplate(w, "graph.html", nil) 428} 429 430func (h *Handler) status(w http.ResponseWriter, r *http.Request) { 431 h.executeTemplate(w, "status.html", struct { 432 Birth time.Time 433 CWD string 434 Version *PrometheusVersion 435 Alertmanagers []*url.URL 436 }{ 437 Birth: h.birth, 438 CWD: h.cwd, 439 Version: h.versionInfo, 440 Alertmanagers: h.notifier.Alertmanagers(), 441 }) 442} 443 444func (h *Handler) flags(w http.ResponseWriter, r *http.Request) { 445 h.executeTemplate(w, "flags.html", h.flagsMap) 446} 447 448func (h *Handler) serveConfig(w http.ResponseWriter, r *http.Request) { 449 h.mtx.RLock() 450 defer h.mtx.RUnlock() 451 452 h.executeTemplate(w, "config.html", h.config.String()) 453} 454 455func (h *Handler) rules(w http.ResponseWriter, r *http.Request) { 456 h.executeTemplate(w, "rules.html", h.ruleManager) 457} 458 459func (h *Handler) targets(w http.ResponseWriter, r *http.Request) { 460 // Bucket targets by job label 461 tps := map[string][]*retrieval.Target{} 462 for _, t := range h.targetManager.Targets() { 463 job := string(t.Labels()[model.JobLabel]) 464 tps[job] = append(tps[job], t) 465 } 466 467 for _, targets := range tps { 468 sort.Slice(targets, func(i, j int) bool { 469 return targets[i].Labels()[model.InstanceLabel] < targets[j].Labels()[model.InstanceLabel] 470 }) 471 } 472 473 h.executeTemplate(w, "targets.html", struct { 474 TargetPools map[string][]*retrieval.Target 475 }{ 476 TargetPools: tps, 477 }) 478} 479 480func (h *Handler) version(w http.ResponseWriter, r *http.Request) { 481 dec := json.NewEncoder(w) 482 if err := dec.Encode(h.versionInfo); err != nil { 483 http.Error(w, fmt.Sprintf("error encoding JSON: %s", err), http.StatusInternalServerError) 484 } 485} 486 487func (h *Handler) quit(w http.ResponseWriter, r *http.Request) { 488 fmt.Fprintf(w, "Requesting termination... Goodbye!") 489 close(h.quitCh) 490} 491 492func (h *Handler) reload(w http.ResponseWriter, r *http.Request) { 493 rc := make(chan error) 494 h.reloadCh <- rc 495 if err := <-rc; err != nil { 496 http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) 497 } 498} 499 500func (h *Handler) consolesPath() string { 501 if _, err := os.Stat(h.options.ConsoleTemplatesPath + "/index.html"); !os.IsNotExist(err) { 502 return h.options.ExternalURL.Path + "/consoles/index.html" 503 } 504 if h.options.UserAssetsPath != "" { 505 if _, err := os.Stat(h.options.UserAssetsPath + "/index.html"); !os.IsNotExist(err) { 506 return h.options.ExternalURL.Path + "/user/index.html" 507 } 508 } 509 return "" 510} 511 512func tmplFuncs(consolesPath string, opts *Options) template_text.FuncMap { 513 return template_text.FuncMap{ 514 "since": func(t time.Time) time.Duration { 515 return time.Since(t) / time.Millisecond * time.Millisecond 516 }, 517 "consolesPath": func() string { return consolesPath }, 518 "pathPrefix": func() string { return opts.ExternalURL.Path }, 519 "buildVersion": func() string { return opts.Version.Revision }, 520 "stripLabels": func(lset model.LabelSet, labels ...model.LabelName) model.LabelSet { 521 for _, ln := range labels { 522 delete(lset, ln) 523 } 524 return lset 525 }, 526 "globalURL": func(u *url.URL) *url.URL { 527 host, port, err := net.SplitHostPort(u.Host) 528 if err != nil { 529 return u 530 } 531 for _, lhr := range localhostRepresentations { 532 if host == lhr { 533 _, ownPort, err := net.SplitHostPort(opts.ListenAddress) 534 if err != nil { 535 return u 536 } 537 538 if port == ownPort { 539 // Only in the case where the target is on localhost and its port is 540 // the same as the one we're listening on, we know for sure that 541 // we're monitoring our own process and that we need to change the 542 // scheme, hostname, and port to the externally reachable ones as 543 // well. We shouldn't need to touch the path at all, since if a 544 // path prefix is defined, the path under which we scrape ourselves 545 // should already contain the prefix. 546 u.Scheme = opts.ExternalURL.Scheme 547 u.Host = opts.ExternalURL.Host 548 } else { 549 // Otherwise, we only know that localhost is not reachable 550 // externally, so we replace only the hostname by the one in the 551 // external URL. It could be the wrong hostname for the service on 552 // this port, but it's still the best possible guess. 553 host, _, err := net.SplitHostPort(opts.ExternalURL.Host) 554 if err != nil { 555 return u 556 } 557 u.Host = host + ":" + port 558 } 559 break 560 } 561 } 562 return u 563 }, 564 "numHealthy": func(pool []*retrieval.Target) int { 565 alive := len(pool) 566 for _, p := range pool { 567 if p.Health() != retrieval.HealthGood { 568 alive-- 569 } 570 } 571 572 return alive 573 }, 574 "healthToClass": func(th retrieval.TargetHealth) string { 575 switch th { 576 case retrieval.HealthUnknown: 577 return "warning" 578 case retrieval.HealthGood: 579 return "success" 580 default: 581 return "danger" 582 } 583 }, 584 "alertStateToClass": func(as rules.AlertState) string { 585 switch as { 586 case rules.StateInactive: 587 return "success" 588 case rules.StatePending: 589 return "warning" 590 case rules.StateFiring: 591 return "danger" 592 default: 593 panic("unknown alert state") 594 } 595 }, 596 } 597} 598 599func (h *Handler) getTemplate(name string) (string, error) { 600 baseTmpl, err := ui.Asset("web/ui/templates/_base.html") 601 if err != nil { 602 return "", fmt.Errorf("error reading base template: %s", err) 603 } 604 pageTmpl, err := ui.Asset(filepath.Join("web/ui/templates", name)) 605 if err != nil { 606 return "", fmt.Errorf("error reading page template %s: %s", name, err) 607 } 608 return string(baseTmpl) + string(pageTmpl), nil 609} 610 611func (h *Handler) executeTemplate(w http.ResponseWriter, name string, data interface{}) { 612 text, err := h.getTemplate(name) 613 if err != nil { 614 http.Error(w, err.Error(), http.StatusInternalServerError) 615 } 616 617 tmpl := template.NewTemplateExpander(h.context, text, name, data, h.now(), h.queryEngine, h.options.ExternalURL) 618 tmpl.Funcs(tmplFuncs(h.consolesPath(), h.options)) 619 620 result, err := tmpl.ExpandHTML(nil) 621 if err != nil { 622 http.Error(w, err.Error(), http.StatusInternalServerError) 623 return 624 } 625 io.WriteString(w, result) 626} 627 628func dumpHeap(w http.ResponseWriter, r *http.Request) { 629 target := fmt.Sprintf("/tmp/%d.heap", time.Now().Unix()) 630 f, err := os.Create(target) 631 if err != nil { 632 log.Error("Could not dump heap: ", err) 633 } 634 fmt.Fprintf(w, "Writing to %s...", target) 635 defer f.Close() 636 pprof_runtime.WriteHeapProfile(f) 637 fmt.Fprintf(w, "Done") 638} 639 640// AlertStatus bundles alerting rules and the mapping of alert states to row classes. 641type AlertStatus struct { 642 AlertingRules []*rules.AlertingRule 643 AlertStateToRowClass map[rules.AlertState]string 644} 645 646type byAlertStateAndNameSorter struct { 647 alerts []*rules.AlertingRule 648} 649 650func (s byAlertStateAndNameSorter) Len() int { 651 return len(s.alerts) 652} 653 654func (s byAlertStateAndNameSorter) Less(i, j int) bool { 655 return s.alerts[i].State() > s.alerts[j].State() || 656 (s.alerts[i].State() == s.alerts[j].State() && 657 s.alerts[i].Name() < s.alerts[j].Name()) 658} 659 660func (s byAlertStateAndNameSorter) Swap(i, j int) { 661 s.alerts[i], s.alerts[j] = s.alerts[j], s.alerts[i] 662} 663