1// Copyright (c) The Thanos Authors. 2// Licensed under the Apache License 2.0. 3 4package ui 5 6import ( 7 "html/template" 8 "net/http" 9 "os" 10 "path" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/go-kit/kit/log" 16 "github.com/prometheus/client_golang/prometheus" 17 "github.com/prometheus/common/model" 18 "github.com/prometheus/common/route" 19 "github.com/prometheus/common/version" 20 "github.com/thanos-io/thanos/pkg/component" 21 extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http" 22 "github.com/thanos-io/thanos/pkg/query" 23) 24 25type Query struct { 26 *BaseUI 27 storeSet *query.StoreSet 28 29 flagsMap map[string]string 30 31 cwd string 32 birth time.Time 33 reg prometheus.Registerer 34 now func() model.Time 35} 36 37type thanosVersion struct { 38 Version string `json:"version"` 39 Revision string `json:"revision"` 40 Branch string `json:"branch"` 41 BuildUser string `json:"buildUser"` 42 BuildDate string `json:"buildDate"` 43 GoVersion string `json:"goVersion"` 44} 45 46func NewQueryUI(logger log.Logger, reg prometheus.Registerer, storeSet *query.StoreSet, flagsMap map[string]string) *Query { 47 cwd, err := os.Getwd() 48 if err != nil { 49 cwd = "<error retrieving current working directory>" 50 } 51 return &Query{ 52 BaseUI: NewBaseUI(logger, "query_menu.html", queryTmplFuncs()), 53 storeSet: storeSet, 54 flagsMap: flagsMap, 55 cwd: cwd, 56 birth: time.Now(), 57 reg: reg, 58 now: model.Now, 59 } 60} 61 62func queryTmplFuncs() template.FuncMap { 63 return template.FuncMap{ 64 "since": func(t time.Time) time.Duration { 65 return time.Since(t) / time.Millisecond * time.Millisecond 66 }, 67 "formatTimestamp": func(timestamp int64) string { 68 return time.Unix(timestamp/1000, 0).Format(time.RFC3339) 69 }, 70 "title": strings.Title, 71 } 72} 73 74// Register registers new GET routes for subpages and retirects from / to /graph. 75func (q *Query) Register(r *route.Router, ins extpromhttp.InstrumentationMiddleware) { 76 instrf := func(name string, next func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc { 77 return ins.NewHandler(name, http.HandlerFunc(next)) 78 } 79 80 r.Get("/", instrf("root", q.root)) 81 r.Get("/graph", instrf("graph", q.graph)) 82 r.Get("/stores", instrf("stores", q.stores)) 83 r.Get("/status", instrf("status", q.status)) 84 85 r.Get("/static/*filepath", instrf("static", q.serveStaticAsset)) 86 // TODO(bplotka): Consider adding more Thanos related data e.g: 87 // - What store nodes we see currently. 88 // - What sidecars we see currently. 89} 90 91// Root redirects "/" requests to "/graph", taking into account the path prefix value. 92func (q *Query) root(w http.ResponseWriter, r *http.Request) { 93 prefix := GetWebPrefix(q.logger, q.flagsMap, r) 94 95 http.Redirect(w, r, path.Join(prefix, "/graph"), http.StatusFound) 96} 97 98func (q *Query) graph(w http.ResponseWriter, r *http.Request) { 99 prefix := GetWebPrefix(q.logger, q.flagsMap, r) 100 101 q.executeTemplate(w, "graph.html", prefix, nil) 102} 103 104func (q *Query) status(w http.ResponseWriter, r *http.Request) { 105 prefix := GetWebPrefix(q.logger, q.flagsMap, r) 106 107 q.executeTemplate(w, "status.html", prefix, struct { 108 Birth time.Time 109 CWD string 110 Version thanosVersion 111 }{ 112 Birth: q.birth, 113 CWD: q.cwd, 114 Version: thanosVersion{ 115 Version: version.Version, 116 Revision: version.Revision, 117 Branch: version.Branch, 118 BuildUser: version.BuildUser, 119 BuildDate: version.BuildDate, 120 GoVersion: version.GoVersion, 121 }, 122 }) 123} 124 125func (q *Query) stores(w http.ResponseWriter, r *http.Request) { 126 prefix := GetWebPrefix(q.logger, q.flagsMap, r) 127 statuses := make(map[component.StoreAPI][]query.StoreStatus) 128 for _, status := range q.storeSet.GetStoreStatus() { 129 statuses[status.StoreType] = append(statuses[status.StoreType], status) 130 } 131 132 sources := make([]component.StoreAPI, 0, len(statuses)) 133 for k := range statuses { 134 sources = append(sources, k) 135 } 136 sort.Slice(sources, func(i int, j int) bool { 137 if sources[i] == nil { 138 return false 139 } 140 if sources[j] == nil { 141 return true 142 } 143 return sources[i].String() < sources[j].String() 144 }) 145 146 q.executeTemplate(w, "stores.html", prefix, struct { 147 Stores map[component.StoreAPI][]query.StoreStatus 148 Sources []component.StoreAPI 149 }{ 150 Stores: statuses, 151 Sources: sources, 152 }) 153} 154