Skip to content

Commit

Permalink
feat: debug api (#898)
Browse files Browse the repository at this point in the history
  • Loading branch information
shhdgit authored May 12, 2021
1 parent 7f9085e commit 2c9d3ab
Show file tree
Hide file tree
Showing 20 changed files with 1,178 additions and 46 deletions.
2 changes: 2 additions & 0 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/pingcap/tidb-dashboard/pkg/apiserver/clusterinfo"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/configuration"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/diagnose"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/info"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/logsearch"
Expand Down Expand Up @@ -133,6 +134,7 @@ func (s *Service) Start(ctx context.Context) error {
profiling.Module,
statement.Module,
slowquery.Module,
debugapi.Module,
fx.Populate(&s.apiHandlerEngine),
fx.Invoke(
user.RegisterRouter,
Expand Down
106 changes: 106 additions & 0 deletions pkg/apiserver/debugapi/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2021 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 debugapi

import (
"fmt"

"go.uber.org/fx"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/tidb"
"github.com/pingcap/tidb-dashboard/pkg/tiflash"
"github.com/pingcap/tidb-dashboard/pkg/tikv"
)

type Client interface {
Send(request *Request) ([]byte, error)
Get(request *Request) ([]byte, error)
}

type ClientMap map[model.NodeKind]Client

func newClientMap(tidbImpl tidbImplement, tikvImpl tikvImplement, tiflashImpl tiflashImplement, pdImpl pdImplement) *ClientMap {
clientMap := ClientMap{
model.NodeKindTiDB: &tidbImpl,
model.NodeKindTiKV: &tikvImpl,
model.NodeKindTiFlash: &tiflashImpl,
model.NodeKindPD: &pdImpl,
}
return &clientMap
}

func defaultSendRequest(client Client, req *Request) ([]byte, error) {
switch req.Method {
case EndpointMethodGet:
return client.Get(req)
default:
return nil, fmt.Errorf("invalid request method `%s`, host: %s, path: %s", req.Method, req.Host, req.Path)
}
}

type tidbImplement struct {
fx.In
Client *tidb.Client
}

func (impl *tidbImplement) Get(req *Request) ([]byte, error) {
return impl.Client.WithEnforcedStatusAPIAddress(req.Host, req.Port).SendGetRequest(req.Path)
}

func (impl *tidbImplement) Send(req *Request) ([]byte, error) {
return defaultSendRequest(impl, req)
}

// TODO: tikv/tiflash/pd forwarder impl

type tikvImplement struct {
fx.In
Client *tikv.Client
}

func (impl *tikvImplement) Get(req *Request) ([]byte, error) {
return impl.Client.SendGetRequest(req.Host, req.Port, req.Path)
}

func (impl *tikvImplement) Send(req *Request) ([]byte, error) {
return defaultSendRequest(impl, req)
}

type tiflashImplement struct {
fx.In
Client *tiflash.Client
}

func (impl *tiflashImplement) Get(req *Request) ([]byte, error) {
return impl.Client.SendGetRequest(req.Host, req.Port, req.Path)
}

func (impl *tiflashImplement) Send(req *Request) ([]byte, error) {
return defaultSendRequest(impl, req)
}

type pdImplement struct {
fx.In
Client *pd.Client
}

func (impl *pdImplement) Get(req *Request) ([]byte, error) {
return impl.Client.SendGetRequest(req.Path)
}

func (impl *pdImplement) Send(req *Request) ([]byte, error) {
return defaultSendRequest(impl, req)
}
172 changes: 172 additions & 0 deletions pkg/apiserver/debugapi/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2021 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 debugapi

import (
"net/http"
"net/url"
"regexp"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/model"
)

var (
ErrMissingRequiredParam = ErrNS.NewType("missing_require_parameter")
ErrInvalidParam = ErrNS.NewType("invalid_parameter")
)

type EndpointAPIModel struct {
ID string `json:"id"`
Component model.NodeKind `json:"component"`
Path string `json:"path"`
Method EndpointMethod `json:"method"`
PathParams []EndpointAPIParam `json:"path_params"` // e.g. /stats/dump/{db}/{table} -> db, table
QueryParams []EndpointAPIParam `json:"query_params"` // e.g. /debug/pprof?seconds=1 -> seconds
}

type EndpointMethod string

const (
EndpointMethodGet EndpointMethod = http.MethodGet
)

type Request struct {
Method EndpointMethod
Host string
Port int
Path string
Query string
}

func (e *EndpointAPIModel) NewRequest(host string, port int, value map[string]string) (*Request, error) {
req := &Request{
Method: e.Method,
Host: host,
Port: port,
}

pathValues, err := transformValues(e.PathParams, value)
if err != nil {
return nil, err
}
path, err := e.PopulatePath(pathValues)
if err != nil {
return nil, err
}
req.Path = path

queryValues, err := transformValues(e.QueryParams, value)
if err != nil {
return nil, err
}
query, err := e.EncodeQuery(queryValues)
if err != nil {
return nil, err
}
req.Query = query

return req, nil
}

var paramRegexp *regexp.Regexp = regexp.MustCompile(`\{(\w+)\}`)

func (e *EndpointAPIModel) PopulatePath(valMap map[string]string) (string, error) {
var returnErr error
replacedPath := e.Path
replacedPath = paramRegexp.ReplaceAllStringFunc(replacedPath, func(s string) string {
if returnErr != nil {
return s
}

key := paramRegexp.ReplaceAllString(s, "${1}")
val, ok := valMap[key]
// means the param can be found in the endpoint path, but not in the param value map
if !ok {
returnErr = ErrMissingRequiredParam.New("missing required path param, path: %s, param: %s", e.Path, key)
return s
}

return val
})
return replacedPath, returnErr
}

func (e *EndpointAPIModel) EncodeQuery(valMap map[string]string) (string, error) {
query := url.Values{}
for _, q := range e.QueryParams {
// cuz paramValues was generated by e.Query, paramValues can always find param by q.Name
val, ok := valMap[q.Name]
if q.Required && !ok {
return "", ErrMissingRequiredParam.New("missing required query param: %s", q.Name)
}
query.Add(q.Name, val)
}
return query.Encode(), nil
}

func transformValues(params []EndpointAPIParam, values map[string]string) (map[string]string, error) {
pvMap := map[string]string{}
for _, p := range params {
v, ok := values[p.Name]
if !ok {
continue
}
tVal, err := p.Transform(v)
if err != nil {
return nil, ErrInvalidParam.WrapWithNoMessage(err)
}
pvMap[p.Name] = tVal
}
return pvMap, nil
}

type EndpointAPIParam struct {
Name string `json:"name"`
Required bool `json:"required"`
// represents what param is
Model EndpointAPIParamModel `json:"model"`
PreModelTransformer ModelTransformer `json:"-"`
PostModelTransformer ModelTransformer `json:"-"`
}

// Transform incoming param's value by transformer at endpoint / model definition
func (p *EndpointAPIParam) Transform(value string) (string, error) {
transfomers := []ModelTransformer{
p.PreModelTransformer,
p.Model.Transformer,
p.PostModelTransformer,
}

for _, t := range transfomers {
if t == nil {
continue
}
v, err := t(value)
if err != nil {
return "", err
}
value = v
}

return value, nil
}

// Transformer can transform the incoming param's value in special scenarios
// Also, now are used as validation function
type ModelTransformer func(value string) (string, error)

type EndpointAPIParamModel struct {
Type string `json:"type"`
Transformer ModelTransformer `json:"-"`
}
Loading

0 comments on commit 2c9d3ab

Please sign in to comment.