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