diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97d028dc77..34f20d3806 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,8 +14,6 @@ Although TiDB Dashboard can also be integrated into [PD], this form is not conve
### Step 1. Start a TiDB cluster
-#### Solution A. Use TiUP (Recommended)
-
[TiUP] is the offical component manager for [TiDB]. It can help you set up a local TiDB cluster in a few minutes.
Download and install TiUP:
@@ -40,73 +38,7 @@ Start a local TiDB cluster:
tiup playground nightly
```
-> Note: you might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, we will not use the that TiDB Dashboard. Please keep following the rest of the steps in this document.
-
-#### Solution B. Download and Run Binary Manually
-
-
-
-Alternatively, you can deploy a cluster with binary files manually.
-
-1. Download binaries
-
- Linux:
-
- ```bash
- mkdir tidb_cluster
- cd tidb_cluster
- wget https://download.pingcap.org/tidb-nightly-linux-amd64.tar.gz
- tar -xzf tidb-nightly-linux-amd64.tar.gz
- cd tidb-nightly-linux-amd64
- ```
-
- MacOS:
-
- ```bash
- mkdir tidb_cluster
- cd tidb_cluster
- wget https://download.pingcap.org/tidb-nightly-darwin-amd64.tar.gz
- wget https://download.pingcap.org/tikv-nightly-darwin-amd64.tar.gz
- wget https://download.pingcap.org/pd-nightly-darwin-amd64.tar.gz
- mkdir tidb-nightly-darwin-amd64
- tar -xzf tidb-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1
- tar -xzf tikv-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1
- tar -xzf pd-nightly-darwin-amd64.tar.gz -C tidb-nightly-darwin-amd64 --strip-components=1
- cd tidb-nightly-darwin-amd64
- ```
-
-2. Start a PD server
-
- ```bash
- ./bin/pd-server --name=pd --data-dir=pd --client-urls=http://127.0.0.1:2379 --log-file=pd.log
- # Now pd-server is listen on port 2379
- ```
-
-3. Start a TiKV server
-
- Open a new terminal:
-
- ```bash
- ./bin/tikv-server --addr="127.0.0.1:20160" --pd-endpoints="127.0.0.1:2379" --data-dir=tikv --log-file=./tikv.log
- # Now tikv-server is listen on port 20160
- ```
-
-4. Start a TiDB server
-
- Open a new terminal:
-
- ```bash
- ./bin/tidb-server --store=tikv --path="127.0.0.1:2379" --log-file=tidb.log
- # Now tidb-server is listen on port 4000
- ```
-
-5. Use mysql-client to check everything works fine:
-
- ```bash
- mysql -h 127.0.0.1 -P 4000 -uroot
- ```
-
-
+You might notice that there is already a TiDB Dashboard integrated into the PD started by TiUP. For development purpose, it will not be used intentionally.
### Step 2. Prepare Prerequisites
@@ -124,7 +56,7 @@ The followings are required for developing TiDB Dashboard:
1. Clone the repository:
```bash
- git clone https://github.com/pingcap-incubator/tidb-dashboard.git
+ git clone https://github.com/pingcap/tidb-dashboard.git
cd tidb-dashboard
```
@@ -144,17 +76,11 @@ The followings are required for developing TiDB Dashboard:
yarn start
```
-1. That's it! You can access TiDB Dashboard now:
-
- TiDB Dashboard UI: http://127.0.0.1:3001
-
- Swagger UI for TiDB Dashboard APIs: http://localhost:12333/dashboard/api/swagger
+1. That's it! You can access TiDB Dashboard now: http://127.0.0.1:3001
### Step 4. Run E2E Tests (optional)
-Now we have only a few e2e tests in the `ui/tests` folder, you can contribute more for it.
-
-After finishing the above steps, we can run the tests by following commands:
+When back-end server and front-end server are both started, E2E tests can be run by:
```bash
cd ui/tests
@@ -162,16 +88,30 @@ yarn
yarn test
```
-### Step 5. Run Storybook Playground (optional)
+> Now we have only a few e2e tests. Contributions are welcome!
+
+## Additional Guides
+
+### Swagger UI
+
+We use [Swagger] to generate the API server and corresponding clients. Swagger provides a web UI in which you can
+see all TiDB Dashboard API endpoints and specifications, or even send API requests.
+
+Swagger UI is available at http://localhost:12333/dashboard/api/swagger after the above Step 3 is finished.
+
+### Storybook
+
+We expose some UI components in a playground provided by [React Storybook]. In the playground you can see what
+components look like and how to use them.
-After finishing the above steps, we can run the storybook playground by following commands:
+Storybook can be started using the following commands:
```bash
cd ui
yarn storybook
```
-You can add more stories for your components to the playground.
+> We have not yet make all components available in the Storybook. Contributions are welcome!
## Contribution flow
@@ -256,3 +196,5 @@ The body of the commit message should describe why the change was made and at a
[tidb]: https://github.com/pingcap/tidb
[tikv]: https://github.com/tikv/tikv
[tiup]: https://tiup.io
+[Swagger]: https://swagger.io
+[React Storybook]: https://storybook.js.org
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 84184fdcbc..1fad317dda 100644
--- a/go.mod
+++ b/go.mod
@@ -43,6 +43,7 @@ require (
go.uber.org/zap v1.13.0
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect
+ golang.org/x/sync v0.0.0-20190423024810-112230192c58
google.golang.org/grpc v1.25.1
gopkg.in/oleiade/reflections.v1 v1.0.0
)
diff --git a/go.sum b/go.sum
index 36b1c7ab53..647834bb0d 100644
--- a/go.sum
+++ b/go.sum
@@ -424,6 +424,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 3b23d5e37f..b519f5f33c 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -126,18 +126,18 @@ func (s *Service) Start(ctx context.Context) error {
),
fx.Populate(&s.apiHandlerEngine),
fx.Invoke(
- user.Register,
- info.Register,
- clusterinfo.Register,
- profiling.Register,
- logsearch.Register,
- slowquery.Register,
- statement.Register,
- diagnose.Register,
- keyvisual.Register,
- metrics.Register,
- queryeditor.Register,
- configuration.Register,
+ user.RegisterRouter,
+ info.RegisterRouter,
+ clusterinfo.RegisterRouter,
+ profiling.RegisterRouter,
+ logsearch.RegisterRouter,
+ slowquery.RegisterRouter,
+ statement.RegisterRouter,
+ diagnose.RegisterRouter,
+ keyvisual.RegisterRouter,
+ metrics.RegisterRouter,
+ queryeditor.RegisterRouter,
+ configuration.RegisterRouter,
// Must be at the end
s.status.Register,
),
diff --git a/pkg/apiserver/clusterinfo/service.go b/pkg/apiserver/clusterinfo/service.go
index 6b9eb70591..312a479eff 100644
--- a/pkg/apiserver/clusterinfo/service.go
+++ b/pkg/apiserver/clusterinfo/service.go
@@ -60,7 +60,7 @@ func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
return s
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/topology")
endpoint.Use(auth.MWAuthRequired())
endpoint.GET("/tidb", s.getTiDBTopology)
diff --git a/pkg/apiserver/configuration/router.go b/pkg/apiserver/configuration/router.go
index 00bf34670c..380586ec7e 100644
--- a/pkg/apiserver/configuration/router.go
+++ b/pkg/apiserver/configuration/router.go
@@ -22,7 +22,7 @@ import (
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"
)
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/configuration")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
diff --git a/pkg/apiserver/diagnose/diagnose.go b/pkg/apiserver/diagnose/diagnose.go
index e339a99815..149caf83ae 100644
--- a/pkg/apiserver/diagnose/diagnose.go
+++ b/pkg/apiserver/diagnose/diagnose.go
@@ -57,7 +57,7 @@ func NewService(config *config.Config, tidbClient *tidb.Client, db *dbstore.DB,
}
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/diagnose")
endpoint.GET("/reports",
auth.MWAuthRequired(),
diff --git a/pkg/apiserver/info/info.go b/pkg/apiserver/info/info.go
index 45b405210b..c298ca256c 100644
--- a/pkg/apiserver/info/info.go
+++ b/pkg/apiserver/info/info.go
@@ -44,7 +44,7 @@ func NewService(p ServiceParams) *Service {
return &Service{params: p}
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/info")
endpoint.GET("/info", s.infoHandler)
endpoint.Use(auth.MWAuthRequired())
diff --git a/pkg/apiserver/logsearch/service.go b/pkg/apiserver/logsearch/service.go
index ea48b169ee..d73c113531 100644
--- a/pkg/apiserver/logsearch/service.go
+++ b/pkg/apiserver/logsearch/service.go
@@ -72,7 +72,7 @@ func NewService(lc fx.Lifecycle, config *config.Config, db *dbstore.DB) *Service
return service
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/logs")
{
endpoint.GET("/download", s.DownloadLogs)
diff --git a/pkg/apiserver/metrics/metrics.go b/pkg/apiserver/metrics/metrics.go
deleted file mode 100644
index d41c9da3f1..0000000000
--- a/pkg/apiserver/metrics/metrics.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package metrics
-
-import (
- "context"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "strconv"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/joomcode/errorx"
- "go.etcd.io/etcd/clientv3"
- "go.uber.org/fx"
-
- "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user"
- "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"
- "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc"
- "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology"
-)
-
-var (
- ErrNS = errorx.NewNamespace("error.api.metrics")
- ErrPrometheusNotFound = ErrNS.NewType("prometheus_not_found")
- ErrPrometheusQueryFailed = ErrNS.NewType("prometheus_query_failed")
-)
-
-const (
- defaultPromQueryTimeout = time.Second * 30
-)
-
-type ServiceParams struct {
- fx.In
- HTTPClient *httpc.Client
- EtcdClient *clientv3.Client
-}
-
-type Service struct {
- params ServiceParams
- lifecycleCtx context.Context
-}
-
-func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
- s := &Service{params: p}
-
- lc.Append(fx.Hook{
- OnStart: func(ctx context.Context) error {
- s.lifecycleCtx = ctx
- return nil
- },
- })
-
- return s
-}
-
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
- endpoint := r.Group("/metrics")
- endpoint.Use(auth.MWAuthRequired())
- endpoint.GET("/query", s.queryHandler)
-}
-
-type QueryRequest struct {
- StartTimeSec int `json:"start_time_sec" form:"start_time_sec"`
- EndTimeSec int `json:"end_time_sec" form:"end_time_sec"`
- StepSec int `json:"step_sec" form:"step_sec"`
- Query string `json:"query" form:"query"`
-}
-
-type QueryResponse struct {
- Status string `json:"status"`
- Data map[string]interface{} `json:"data"`
-}
-
-// @Summary Query metrics
-// @Description Query metrics in the given range
-// @Param q query QueryRequest true "Query"
-// @Success 200 {object} QueryResponse
-// @Failure 401 {object} utils.APIError "Unauthorized failure"
-// @Security JwtAuth
-// @Router /metrics/query [get]
-func (s *Service) queryHandler(c *gin.Context) {
- var req QueryRequest
- if err := c.ShouldBindQuery(&req); err != nil {
- utils.MakeInvalidRequestErrorFromError(c, err)
- return
- }
-
- pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient)
- if err != nil {
- _ = c.Error(err)
- return
- }
- if pi == nil {
- _ = c.Error(ErrPrometheusNotFound.NewWithNoMessage())
- return
- }
-
- params := url.Values{}
- params.Add("query", req.Query)
- params.Add("start", strconv.Itoa(req.StartTimeSec))
- params.Add("end", strconv.Itoa(req.EndTimeSec))
- params.Add("step", strconv.Itoa(req.StepSec))
-
- uri := fmt.Sprintf("http://%s:%d/api/v1/query_range?%s", pi.IP, pi.Port, params.Encode())
- promReq, err := http.NewRequestWithContext(s.lifecycleCtx, http.MethodGet, uri, nil)
- if err != nil {
- _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to build Prometheus request"))
- return
- }
-
- promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq)
- if err != nil {
- _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus"))
- return
- }
-
- defer promResp.Body.Close()
- if promResp.StatusCode != http.StatusOK {
- _ = c.Error(ErrPrometheusQueryFailed.New("failed to query Prometheus"))
- return
- }
-
- body, err := ioutil.ReadAll(promResp.Body)
- if err != nil {
- _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result"))
- return
- }
-
- c.Data(promResp.StatusCode, promResp.Header.Get("content-type"), body)
-}
diff --git a/pkg/apiserver/metrics/prom_resolve.go b/pkg/apiserver/metrics/prom_resolve.go
new file mode 100644
index 0000000000..5c65b1762b
--- /dev/null
+++ b/pkg/apiserver/metrics/prom_resolve.go
@@ -0,0 +1,188 @@
+// Copyright 2020 PingCAP, Inc.
+//
+// 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,
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/pingcap-incubator/tidb-dashboard/pkg/utils/topology"
+)
+
+const (
+ promCacheTTL = time.Second * 5
+)
+
+type promAddressCacheEntity struct {
+ address string
+ cacheAt time.Time
+}
+
+type pdServerConfig struct {
+ MetricStorage string `json:"metric-storage"`
+}
+
+type pdConfig struct {
+ PdServer pdServerConfig `json:"pd-server"`
+}
+
+// Check and normalize a Prometheus address supplied by user.
+func normalizeCustomizedPromAddress(addr string) (string, error) {
+ if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") {
+ addr = "http://" + addr
+ }
+ u, err := url.Parse(addr)
+ if err != nil {
+ return "", fmt.Errorf("invalid Prometheus address format: %v", err)
+ }
+ if len(u.Host) == 0 || len(u.Scheme) == 0 {
+ return "", fmt.Errorf("invalid Prometheus address format")
+ }
+ // Normalize the address, remove unnecessary parts.
+ addr = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
+ return addr, nil
+}
+
+// Resolve the customized Prometheus address in PD config. If it is not configured, empty address will be returned.
+// The returned address must be valid. If an invalid Prometheus address is configured, errors will be returned.
+func (s *Service) resolveCustomizedPromAddress(acceptInvalidAddr bool) (string, error) {
+ // Lookup "metric-storage" cluster config in PD.
+ data, err := s.params.PDClient.SendGetRequest("/config")
+ if err != nil {
+ return "", err
+ }
+ var config pdConfig
+ if err := json.Unmarshal(data, &config); err != nil {
+ return "", err
+ }
+ addr := config.PdServer.MetricStorage
+ if len(addr) > 0 {
+ if acceptInvalidAddr {
+ return addr, nil
+ }
+ // Verify whether address is valid. If not valid, throw error.
+ addr, err = normalizeCustomizedPromAddress(addr)
+ if err != nil {
+ return "", err
+ }
+ return addr, nil
+ }
+ return "", nil
+}
+
+// Resolve the Prometheus address recorded by deployment tools in the `/topology` etcd namespace.
+// If the address is not recorded (for example, when Prometheus is not deployed), empty address will be returned.
+func (s *Service) resolveDeployedPromAddress() (string, error) {
+ pi, err := topology.FetchPrometheusTopology(s.lifecycleCtx, s.params.EtcdClient)
+ if err != nil {
+ return "", err
+ }
+ if pi == nil {
+ return "", nil
+ }
+ return fmt.Sprintf("http://%s:%d", pi.IP, pi.Port), nil
+}
+
+// Resolve the final Prometheus address. When user has customized an address, this address is returned. Otherwise,
+// address recorded by deployment tools will be returned.
+// If neither custom address nor deployed address is available, empty address will be returned.
+func (s *Service) resolveFinalPromAddress() (string, error) {
+ addr, err := s.resolveCustomizedPromAddress(false)
+ if err != nil {
+ return "", err
+ }
+ if addr != "" {
+ return addr, nil
+ }
+ addr, err = s.resolveDeployedPromAddress()
+ if err != nil {
+ return "", err
+ }
+ if addr != "" {
+ return addr, nil
+ }
+ return "", nil
+}
+
+// Get the final Prometheus address from cache. If cache item is not valid, the address will be resolved from PD
+// or etcd and then the cache will be updated.
+func (s *Service) getPromAddressFromCache() (string, error) {
+ fn := func() (string, error) {
+ // Check whether cache is valid, and use the cache if possible.
+ if v := s.promAddressCache.Load(); v != nil {
+ entity := v.(*promAddressCacheEntity)
+ if entity.cacheAt.Add(promCacheTTL).After(time.Now()) {
+ return entity.address, nil
+ }
+ }
+
+ // Cache is not valid, read from PD and etcd.
+ addr, err := s.resolveFinalPromAddress()
+
+ if err != nil {
+ return "", err
+ }
+
+ s.promAddressCache.Store(&promAddressCacheEntity{
+ address: addr,
+ cacheAt: time.Now(),
+ })
+
+ return addr, nil
+ }
+
+ resolveResult, err, _ := s.promRequestGroup.Do("any_key", func() (interface{}, error) {
+ return fn()
+ })
+ if err != nil {
+ return "", err
+ }
+ return resolveResult.(string), nil
+}
+
+// Set the customized Prometheus address. Address can be empty or a valid address like `http://host:port`.
+// If address is set to empty, address from deployment tools will be used later.
+func (s *Service) setCustomPromAddress(addr string) (string, error) {
+ var err error
+ if len(addr) > 0 {
+ addr, err = normalizeCustomizedPromAddress(addr)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ body := make(map[string]interface{})
+ body["metric-storage"] = addr
+ bodyJSON, err := json.Marshal(&body)
+ if err != nil {
+ return "", err
+ }
+
+ _, err = s.params.PDClient.SendPostRequest("/config", bytes.NewBuffer(bodyJSON))
+ if err != nil {
+ return "", err
+ }
+
+ // Invalidate cache immediately.
+ s.promAddressCache.Value.Store(&promAddressCacheEntity{
+ address: addr,
+ cacheAt: time.Time{},
+ })
+
+ return addr, nil
+}
diff --git a/pkg/apiserver/metrics/router.go b/pkg/apiserver/metrics/router.go
new file mode 100644
index 0000000000..59a33611f3
--- /dev/null
+++ b/pkg/apiserver/metrics/router.go
@@ -0,0 +1,164 @@
+// Copyright 2020 PingCAP, Inc.
+//
+// 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,
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+
+ "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user"
+ "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"
+)
+
+type QueryRequest struct {
+ StartTimeSec int `json:"start_time_sec" form:"start_time_sec"`
+ EndTimeSec int `json:"end_time_sec" form:"end_time_sec"`
+ StepSec int `json:"step_sec" form:"step_sec"`
+ Query string `json:"query" form:"query"`
+}
+
+type QueryResponse struct {
+ Status string `json:"status"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+ endpoint := r.Group("/metrics")
+ endpoint.Use(auth.MWAuthRequired())
+ endpoint.GET("/query", s.queryMetrics)
+ endpoint.GET("/prom_address", s.getPromAddressConfig)
+ endpoint.PUT("/prom_address", s.putCustomPromAddress)
+}
+
+// @Summary Query metrics
+// @Description Query metrics in the given range
+// @Param q query QueryRequest true "Query"
+// @Success 200 {object} QueryResponse
+// @Failure 401 {object} utils.APIError "Unauthorized failure"
+// @Security JwtAuth
+// @Router /metrics/query [get]
+func (s *Service) queryMetrics(c *gin.Context) {
+ var req QueryRequest
+ if err := c.ShouldBindQuery(&req); err != nil {
+ utils.MakeInvalidRequestErrorFromError(c, err)
+ return
+ }
+
+ addr, err := s.getPromAddressFromCache()
+ if err != nil {
+ _ = c.Error(ErrLoadPrometheusAddressFailed.Wrap(err, "Load prometheus address failed"))
+ return
+ }
+ if addr == "" {
+ _ = c.Error(ErrPrometheusNotFound.New("Prometheus is not deployed in the cluster"))
+ return
+ }
+
+ params := url.Values{}
+ params.Add("query", req.Query)
+ params.Add("start", strconv.Itoa(req.StartTimeSec))
+ params.Add("end", strconv.Itoa(req.EndTimeSec))
+ params.Add("step", strconv.Itoa(req.StepSec))
+
+ uri := fmt.Sprintf("%s/api/v1/query_range?%s", addr, params.Encode())
+ promReq, err := http.NewRequestWithContext(s.lifecycleCtx, http.MethodGet, uri, nil)
+ if err != nil {
+ _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to build Prometheus request"))
+ return
+ }
+
+ promResp, err := s.params.HTTPClient.WithTimeout(defaultPromQueryTimeout).Do(promReq)
+ if err != nil {
+ _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to send requests to Prometheus"))
+ return
+ }
+
+ defer promResp.Body.Close()
+ if promResp.StatusCode != http.StatusOK {
+ _ = c.Error(ErrPrometheusQueryFailed.New("failed to query Prometheus"))
+ return
+ }
+
+ body, err := ioutil.ReadAll(promResp.Body)
+ if err != nil {
+ _ = c.Error(ErrPrometheusQueryFailed.Wrap(err, "failed to read Prometheus query result"))
+ return
+ }
+
+ c.Data(promResp.StatusCode, promResp.Header.Get("content-type"), body)
+}
+
+type GetPromAddressConfigResponse struct {
+ CustomizedAddr string `json:"customized_addr"`
+ DeployedAddr string `json:"deployed_addr"`
+}
+
+// @ID metricsGetPromAddress
+// @Summary Get the Prometheus address cluster config
+// @Success 200 {object} GetPromAddressConfigResponse
+// @Failure 401 {object} utils.APIError "Unauthorized failure"
+// @Security JwtAuth
+// @Router /metrics/prom_address [get]
+func (s *Service) getPromAddressConfig(c *gin.Context) {
+ cAddr, err := s.resolveCustomizedPromAddress(true)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+ dAddr, err := s.resolveDeployedPromAddress()
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+ c.JSON(http.StatusOK, GetPromAddressConfigResponse{
+ CustomizedAddr: cAddr,
+ DeployedAddr: dAddr,
+ })
+}
+
+type PutCustomPromAddressRequest struct {
+ Addr string `json:"address"`
+}
+
+type PutCustomPromAddressResponse struct {
+ NormalizedAddr string `json:"normalized_address"`
+}
+
+// @ID metricsSetCustomPromAddress
+// @Summary Set or clear the customized Prometheus address
+// @Param request body PutCustomPromAddressRequest true "Request body"
+// @Success 200 {object} PutCustomPromAddressResponse
+// @Failure 401 {object} utils.APIError "Unauthorized failure"
+// @Security JwtAuth
+// @Router /metrics/prom_address [put]
+func (s *Service) putCustomPromAddress(c *gin.Context) {
+ var req PutCustomPromAddressRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.MakeInvalidRequestErrorFromError(c, err)
+ return
+ }
+ addr, err := s.setCustomPromAddress(req.Addr)
+ if err != nil {
+ _ = c.Error(err)
+ return
+ }
+ c.JSON(http.StatusOK, PutCustomPromAddressResponse{
+ NormalizedAddr: addr,
+ })
+}
diff --git a/pkg/apiserver/metrics/service.go b/pkg/apiserver/metrics/service.go
new file mode 100644
index 0000000000..91bedfd578
--- /dev/null
+++ b/pkg/apiserver/metrics/service.go
@@ -0,0 +1,67 @@
+// Copyright 2020 PingCAP, Inc.
+//
+// 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,
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+ "context"
+ "time"
+
+ "github.com/joomcode/errorx"
+ "go.etcd.io/etcd/clientv3"
+ "go.uber.org/atomic"
+ "go.uber.org/fx"
+ "golang.org/x/sync/singleflight"
+
+ "github.com/pingcap-incubator/tidb-dashboard/pkg/httpc"
+ "github.com/pingcap-incubator/tidb-dashboard/pkg/pd"
+)
+
+var (
+ ErrNS = errorx.NewNamespace("error.api.metrics")
+ ErrLoadPrometheusAddressFailed = ErrNS.NewType("load_prom_address_failed")
+ ErrPrometheusNotFound = ErrNS.NewType("prom_not_found")
+ ErrPrometheusQueryFailed = ErrNS.NewType("prom_query_failed")
+)
+
+const (
+ defaultPromQueryTimeout = time.Second * 30
+)
+
+type ServiceParams struct {
+ fx.In
+ HTTPClient *httpc.Client
+ EtcdClient *clientv3.Client
+ PDClient *pd.Client
+}
+
+type Service struct {
+ params ServiceParams
+ lifecycleCtx context.Context
+
+ promRequestGroup singleflight.Group
+ promAddressCache atomic.Value
+}
+
+func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
+ s := &Service{params: p}
+
+ lc.Append(fx.Hook{
+ OnStart: func(ctx context.Context) error {
+ s.lifecycleCtx = ctx
+ return nil
+ },
+ })
+
+ return s
+}
diff --git a/pkg/apiserver/profiling/router.go b/pkg/apiserver/profiling/router.go
index 97bb1db488..a09eca5b1c 100644
--- a/pkg/apiserver/profiling/router.go
+++ b/pkg/apiserver/profiling/router.go
@@ -30,7 +30,7 @@ import (
)
// Register register the handlers to the service.
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/profiling")
endpoint.GET("/group/list", auth.MWAuthRequired(), s.getGroupList)
endpoint.POST("/group/start", auth.MWAuthRequired(), s.handleStartGroup)
diff --git a/pkg/apiserver/queryeditor/service.go b/pkg/apiserver/queryeditor/service.go
index 6c3e3c6db0..8665fb9a09 100644
--- a/pkg/apiserver/queryeditor/service.go
+++ b/pkg/apiserver/queryeditor/service.go
@@ -53,7 +53,7 @@ func NewService(lc fx.Lifecycle, p ServiceParams) *Service {
return service
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/query_editor")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
diff --git a/pkg/apiserver/slowquery/service.go b/pkg/apiserver/slowquery/service.go
index 6df8d5d6c2..624d57de9d 100644
--- a/pkg/apiserver/slowquery/service.go
+++ b/pkg/apiserver/slowquery/service.go
@@ -47,7 +47,7 @@ func NewService(p ServiceParams) *Service {
return &Service{params: p}
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/slow_query")
{
endpoint.GET("/download", s.downloadHandler)
diff --git a/pkg/apiserver/statement/service.go b/pkg/apiserver/statement/service.go
index 7b25b3b16e..fce356bc85 100644
--- a/pkg/apiserver/statement/service.go
+++ b/pkg/apiserver/statement/service.go
@@ -48,7 +48,7 @@ func NewService(p ServiceParams) *Service {
return &Service{params: p}
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/statements")
{
endpoint.GET("/download", s.downloadHandler)
diff --git a/pkg/apiserver/user/auth.go b/pkg/apiserver/user/auth.go
index 64ca155d3f..39bf30757b 100644
--- a/pkg/apiserver/user/auth.go
+++ b/pkg/apiserver/user/auth.go
@@ -245,7 +245,7 @@ func (s *AuthService) authSharingCodeForm(f *authenticateForm) (*utils.SessionUs
return session, nil
}
-func Register(r *gin.RouterGroup, s *AuthService) {
+func RegisterRouter(r *gin.RouterGroup, s *AuthService) {
endpoint := r.Group("/user")
endpoint.POST("/login", s.loginHandler)
endpoint.POST("/share", s.MWAuthRequired(), s.shareSessionHandler)
diff --git a/pkg/keyvisual/service.go b/pkg/keyvisual/service.go
index ca8005bda0..aeb4e4fb53 100644
--- a/pkg/keyvisual/service.go
+++ b/pkg/keyvisual/service.go
@@ -113,7 +113,7 @@ func NewService(
return s
}
-func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
+func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/keyvisual")
endpoint.Use(auth.MWAuthRequired())
diff --git a/ui/dashboardApp/layout/main/Sider/index.tsx b/ui/dashboardApp/layout/main/Sider/index.tsx
index b436e6f486..c00ec4523f 100644
--- a/ui/dashboardApp/layout/main/Sider/index.tsx
+++ b/ui/dashboardApp/layout/main/Sider/index.tsx
@@ -89,9 +89,9 @@ function Sider({
const menuItems = [
useAppMenuItem(registry, 'overview'),
useAppMenuItem(registry, 'cluster_info'),
- useAppMenuItem(registry, 'keyviz'),
useAppMenuItem(registry, 'statement'),
useAppMenuItem(registry, 'slow_query'),
+ useAppMenuItem(registry, 'keyviz'),
useAppMenuItem(registry, 'system_report'),
useAppMenuItem(registry, 'diagnose'),
useAppMenuItem(registry, 'search_logs'),
diff --git a/ui/dashboardApp/layout/signin/index.tsx b/ui/dashboardApp/layout/signin/index.tsx
index 9e291de79d..c4bfe6208c 100644
--- a/ui/dashboardApp/layout/signin/index.tsx
+++ b/ui/dashboardApp/layout/signin/index.tsx
@@ -215,7 +215,7 @@ function TiDBSignInForm({ successRoute, onClickAlternative }) {
} disabled />