pkg/components/client.go

235 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package components
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/dustin/go-humanize"
"github.com/go-resty/resty/v2"
"github.com/pkg/errors"
"google.golang.org/grpc/metadata"
"k8s.io/klog/v2"
)
type ctxKey int
const (
timeout = time.Second * 30
// BKAPIRequestIDHeader 蓝鲸网关的请求ID
BKAPIRequestIDHeader = "X-Bkapi-Request-Id"
userAgent = "prime/v1.0"
requestIDCtxKey = ctxKey(1)
// RequestIDHeaderKey
RequestIDHeaderKey = "X-Request-Id"
)
var (
maskKeys = map[string]struct{}{
"bk_app_secret": {},
}
clientOnce sync.Once
globalClient *resty.Client
)
// WithLabelMatchValue 设置 RequestId 值
func WithRequestIDValue(ctx context.Context, id string) context.Context {
newCtx := context.WithValue(ctx, requestIDCtxKey, id)
return metadata.AppendToOutgoingContext(newCtx, RequestIDHeaderKey, id)
}
// RequestIDValue 获取 RequestId 值
func RequestIDValue(ctx context.Context) string {
v, ok := ctx.Value(requestIDCtxKey).(string)
if !ok || v == "" {
return grpcRequestIDValue(ctx)
}
return v
}
// grpcRequestIDValue grpc 需要单独处理
func grpcRequestIDValue(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
values := md.Get(RequestIDHeaderKey)
if len(values) == 0 {
return ""
}
return values[0]
}
// SetRequestIDHeaderValue 设置 RequestId 值到头部
func SetRequestIDHeaderValue(req *http.Request, id string) {
req.Header.Set(RequestIDHeaderKey, id)
}
// restyReqToCurl curl 格式的请求日志
func restyReqToCurl(r *resty.Request) string {
headers := ""
for key, values := range r.Header {
for _, value := range values {
headers += fmt.Sprintf(" -H %q", fmt.Sprintf("%s: %s", key, value))
}
}
// 过滤掉敏感信息
rawURL := *r.RawRequest.URL
queryValue := rawURL.Query()
for key := range queryValue {
if _, ok := maskKeys[key]; ok {
queryValue.Set(key, "<masked>")
}
}
rawURL.RawQuery = queryValue.Encode()
reqMsg := fmt.Sprintf("curl -X %s '%s'%s", r.Method, rawURL.String(), headers)
if r.Body != nil {
switch body := r.Body.(type) {
case []byte:
reqMsg += fmt.Sprintf(" -d %q", body)
case string:
reqMsg += fmt.Sprintf(" -d %q", body)
case io.Reader:
reqMsg += " -d (io.Reader)"
default:
prtBodyBytes, err := json.Marshal(body)
if err != nil {
reqMsg += fmt.Sprintf(" -d %q (MarshalErr %s)", body, err)
} else {
reqMsg += fmt.Sprintf(" -d '%s'", prtBodyBytes)
}
}
}
if r.FormData.Encode() != "" {
encodeStr := r.FormData.Encode()
reqMsg += fmt.Sprintf(" -d %q", encodeStr)
rawStr, _ := url.QueryUnescape(encodeStr)
reqMsg += fmt.Sprintf(" -raw `%s`", rawStr)
}
return reqMsg
}
// restyResponseToCurl 返回日志
func restyResponseToCurl(resp *resty.Response) string {
// 最大打印 1024 个字符
body := string(resp.Body())
if len(body) > 1024 {
body = fmt.Sprintf("%s...(Total %s)", body[:1024], humanize.Bytes(uint64(len(body))))
}
respMsg := fmt.Sprintf("[%s] %s %s", resp.Status(), resp.Time(), body)
// 请求蓝鲸网关记录RequestID
bkAPIRequestID := resp.RawResponse.Header.Get(BKAPIRequestIDHeader)
if bkAPIRequestID != "" {
respMsg = fmt.Sprintf("[%s] %s bkapi_request_id=%s %s", resp.Status(), resp.Time(), bkAPIRequestID, body)
}
return respMsg
}
func restyErrHook(r *resty.Request, err error) {
klog.Infof("[%s] REQ: %s", RequestIDValue(r.RawRequest.Context()), restyReqToCurl(r))
klog.Infof("[%s] RESP: [err] %s", RequestIDValue(r.RawRequest.Context()), err)
}
func restyAfterResponseHook(c *resty.Client, r *resty.Response) error {
klog.Infof("[%s] REQ: %s", RequestIDValue(r.Request.Context()), restyReqToCurl(r.Request))
klog.Infof("[%s] RESP: %s", RequestIDValue(r.Request.Context()), restyResponseToCurl(r))
return nil
}
func restyBeforeRequestHook(c *resty.Client, r *http.Request) error {
SetRequestIDHeaderValue(r, RequestIDValue(r.Context()))
return nil
}
// GetClient : 新建Client, 设置公共参数每次新建cookies不复用
func GetClient() *resty.Client {
if globalClient == nil {
clientOnce.Do(func() {
globalClient = resty.New().
SetTimeout(timeout).
SetDebug(false). // 更多详情, 可以开启为 true
SetCookieJar(nil). // 后台API去掉 cookie 记录
SetDebugBodyLimit(1024).
OnAfterResponse(restyAfterResponseHook).
SetPreRequestHook(restyBeforeRequestHook).
OnError(restyErrHook).
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
SetHeader("User-Agent", userAgent)
})
}
return globalClient
}
// BKResult 蓝鲸返回规范的结构体
type BKResult struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// UnmarshalBKResult 反序列化为蓝鲸返回规范
func UnmarshalBKResult(resp *resty.Response, data interface{}) error {
if resp.StatusCode() != http.StatusOK {
return errors.Errorf("http code %d != 200", resp.StatusCode())
}
// 部分接口,如 usermanager 返回的content-type不是json, 需要手动Unmarshal
bkResult := &BKResult{Data: data}
if err := json.Unmarshal(resp.Body(), bkResult); err != nil {
return err
}
if err := bkResult.ValidateCode(); err != nil {
return err
}
return nil
}
// ValidateCode 返回结果是否OK
func (r *BKResult) ValidateCode() error {
code, err := refineCode(r.Code)
if err != nil {
return err
}
if code != 0 {
return errors.Errorf("resp code %d != 0, %s", code, r.Message)
}
return nil
}
// refineCode 多种返回Code统一处理
// 支持 "00", 0, "0"
func refineCode(code interface{}) (int, error) {
var resultCode int
switch code := code.(type) {
case int:
resultCode = code
case float64:
resultCode = int(code)
case string:
c, err := strconv.Atoi(code)
if err != nil {
return -1, err
}
resultCode = c
default:
return -1, errors.Errorf("conversion to int from %T not supported", code)
}
return resultCode, nil
}