Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: a new httpclient #1073

Merged
merged 5 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 40 additions & 64 deletions util/client/httpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,92 +8,68 @@ package httpclient

import (
"context"
"errors"
"fmt"
"crypto/tls"
"net"
"net/http"
"runtime"
"time"

"github.com/go-resty/resty/v2"
"github.com/joomcode/errorx"
"github.com/pingcap/log"
"go.uber.org/zap"

"github.com/pingcap/tidb-dashboard/util/nocopy"
)

var (
ErrNS = errorx.NewNamespace("http_client")
ErrInvalidEndpoint = ErrNS.NewType("invalid_endpoint")
ErrServerError = ErrNS.NewType("server_error")
ErrRequestFailed = ErrNS.NewType("request_failed")
)

// Client is a lightweight wrapper over resty.Client, providing default error handling and timeout settings.
// WARN: This structure is not thread-safe.
// Client caches connections for future re-use and should be reused instead of
// created as needed.
type Client struct {
nocopy.NoCopy

inner *resty.Client
kindTag string
ctx context.Context
kindTag string
transport *http.Transport
defaultCtx context.Context
defaultBaseURL string
}

func (c *Client) SetHeader(header, value string) *Client {
c.inner.Header.Set(header, value)
return c
}

// LifecycleR builds a new Request with the default lifecycle context and the default timeout.
// This function is intentionally not named as `R()` to avoid being confused with `resty.Client.R()`.
func (c *Client) LifecycleR() *Request {
return newRequestFromClient(c)
}

// ======== Below are helper functions to build the Client ========

var defaultRedirectPolicy = resty.FlexibleRedirectPolicy(5)

func New(config Config) *Client {
c := &Client{
inner: resty.New(),
kindTag: config.KindTag,
ctx: config.Context,
func newTransport(tlsConfig *tls.Config) *http.Transport {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
shhdgit marked this conversation as resolved.
Show resolved Hide resolved
TLSClientConfig: tlsConfig,
}
c.inner.SetRedirectPolicy(defaultRedirectPolicy)
c.inner.OnAfterResponse(c.handleAfterResponseHook)
c.inner.OnError(c.handleErrorHook)
c.inner.SetHostURL(config.BaseURL)
c.inner.SetTLSClientConfig(config.TLS)
return c
}

func (c *Client) handleAfterResponseHook(_ *resty.Client, r *resty.Response) error {
// Note: IsError != !IsSuccess
if !r.IsSuccess() {
// Turn all non success responses to an error.
return ErrServerError.New("%s %s (%s): Response status %d",
r.Request.Method,
r.Request.URL,
c.kindTag,
r.StatusCode())
func New(config Config) *Client {
return &Client{
kindTag: config.KindTag,
transport: newTransport(config.TLSConfig),
defaultCtx: config.DefaultCtx,
defaultBaseURL: config.DefaultBaseURL,
}
return nil
}

func (c *Client) handleErrorHook(req *resty.Request, err error) {
// Log all kind of errors
fields := []zap.Field{
zap.String("kindTag", c.kindTag),
zap.String("url", req.URL),
}
var respErr *resty.ResponseError
if errors.As(err, &respErr) && respErr.Response != nil && respErr.Response.RawResponse != nil {
fields = append(fields,
zap.String("responseStatus", respErr.Response.Status()),
zap.String("responseBody", respErr.Response.String()),
)
err = respErr.Unwrap()
func (c *Client) LR() *LazyRequest {
lReq := newRequest(c.kindTag, c.transport)
if c.defaultCtx != nil {
lReq.SetContext(c.defaultCtx)
}
fields = append(fields, zap.Error(err))
if _, hasVerboseError := err.(fmt.Formatter); !hasVerboseError { //nolint:errorlint
fields = append(fields, zap.Stack("stack"))
if c.defaultBaseURL != "" {
lReq.SetBaseURL(c.defaultBaseURL)
}
log.Warn("Request failed", fields...)
return lReq
}
57 changes: 0 additions & 57 deletions util/client/httpclient/client_test.go

This file was deleted.

16 changes: 8 additions & 8 deletions util/client/httpclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

type Config struct {
BaseURL string
Context context.Context
TLS *tls.Config
KindTag string // Used to mark what kind of HttpClient it is in error messages and logs.
KindTag string
TLSConfig *tls.Config
DefaultCtx context.Context
DefaultBaseURL string
}

type APIClientConfig struct {
Expand All @@ -38,9 +38,9 @@ func (dc APIClientConfig) IntoConfig(kindTag string) (Config, error) {
schema = "http"
}
return Config{
BaseURL: schema + "://" + u.Host,
Context: dc.Context,
TLS: dc.TLS,
KindTag: kindTag,
TLSConfig: dc.TLS,
KindTag: kindTag,
DefaultCtx: dc.Context,
DefaultBaseURL: schema + "://" + u.Host,
}, nil
}
34 changes: 34 additions & 0 deletions util/client/httpclient/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0.

package httpclient

import (
"github.com/pingcap/log"
"go.uber.org/zap"
)

// execInfo is a copy of necessary information during the execution.
// It can be used to print logs when something happens.
type execInfo struct {
kindTag string
reqURL string
reqMethod string
respStatus string
respBody string
}

func (e *execInfo) Warn(msg string, err error) {
fields := []zap.Field{
zap.String("kindTag", e.kindTag),
zap.String("url", e.reqURL),
zap.String("method", e.reqMethod),
}
if e.respStatus != "" {
fields = append(fields, zap.String("responseStatus", e.respStatus))
}
if e.respBody != "" {
fields = append(fields, zap.String("responseBody", e.respBody))
}
fields = append(fields, zap.Error(err))
log.Warn(msg, fields...)
}
Loading