github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/restclient/ffresty.go (about)

     1  // Copyright © 2021 Kaleido, Inc.
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  //
     5  // Licensed under the Apache License, Version 2.0 (the "License");
     6  // you may not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  package restclient
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/go-resty/resty/v2"
    29  	"github.com/kaleido-io/firefly/internal/config"
    30  	"github.com/kaleido-io/firefly/internal/i18n"
    31  	"github.com/kaleido-io/firefly/internal/log"
    32  	"github.com/kaleido-io/firefly/pkg/fftypes"
    33  )
    34  
    35  type retryCtxKey struct{}
    36  
    37  type retryCtx struct {
    38  	id       string
    39  	start    time.Time
    40  	attempts uint
    41  }
    42  
    43  // OnAfterResponse when using SetDoNotParseResponse(true) for streming binary replies,
    44  // the caller should invoke ffrest.OnAfterResponse on the response manually.
    45  // The middleware is disabled on this path :-(
    46  // See: https://github.com/go-resty/resty/blob/d01e8d1bac5ba1fed0d9e03c4c47ca21e94a7e8e/client.go#L912-L948
    47  func OnAfterResponse(c *resty.Client, resp *resty.Response) {
    48  	if c == nil || resp == nil {
    49  		return
    50  	}
    51  	rctx := resp.Request.Context()
    52  	rc := rctx.Value(retryCtxKey{}).(*retryCtx)
    53  	elapsed := float64(time.Since(rc.start)) / float64(time.Millisecond)
    54  	log.L(rctx).Infof("<== %s %s [%d] (%.2fms)", resp.Request.Method, resp.Request.URL, resp.StatusCode(), elapsed)
    55  }
    56  
    57  // New creates a new Resty client, using static configuration (from the config file)
    58  // from a given nested prefix in the static configuration
    59  //
    60  // You can use the normal Resty builder pattern, to set per-instance configuration
    61  // as required.
    62  func New(ctx context.Context, staticConfig config.Prefix) *resty.Client {
    63  
    64  	var client *resty.Client
    65  
    66  	iHTTPClient := staticConfig.Get(HTTPCustomClient)
    67  	if iHTTPClient != nil {
    68  		if httpClient, ok := iHTTPClient.(*http.Client); ok {
    69  			client = resty.NewWithClient(httpClient)
    70  		}
    71  	}
    72  	if client == nil {
    73  		client = resty.New()
    74  	}
    75  
    76  	url := strings.TrimSuffix(staticConfig.GetString(HTTPConfigURL), "/")
    77  	if url != "" {
    78  		client.SetHostURL(url)
    79  		log.L(ctx).Debugf("Created REST client to %s", url)
    80  	}
    81  
    82  	client.SetTimeout(staticConfig.GetDuration(HTTPConfigRequestTimeout))
    83  
    84  	client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
    85  		rctx := req.Context()
    86  		rc := rctx.Value(retryCtxKey{})
    87  		if rc == nil {
    88  			// First attempt
    89  			r := &retryCtx{
    90  				id:    fftypes.ShortID(),
    91  				start: time.Now(),
    92  			}
    93  			rctx = context.WithValue(rctx, retryCtxKey{}, r)
    94  			// Create a request logger from the root logger passed into the client
    95  			l := log.L(ctx).WithField("breq", r.id)
    96  			rctx = log.WithLogger(rctx, l)
    97  			req.SetContext(rctx)
    98  		}
    99  		log.L(rctx).Infof("==> %s %s%s", req.Method, url, req.URL)
   100  		return nil
   101  	})
   102  
   103  	// Note that callers using SetNotParseResponse will need to invoke this themselves
   104  
   105  	client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error { OnAfterResponse(c, r); return nil })
   106  
   107  	headers := staticConfig.GetObject(HTTPConfigHeaders)
   108  	for k, v := range headers {
   109  		if vs, ok := v.(string); ok {
   110  			client.SetHeader(k, vs)
   111  		}
   112  	}
   113  	authUsername := staticConfig.GetString((HTTPConfigAuthUsername))
   114  	authPassword := staticConfig.GetString((HTTPConfigAuthPassword))
   115  	if authUsername != "" && authPassword != "" {
   116  		client.SetHeader("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", authUsername, authPassword)))))
   117  	}
   118  
   119  	if staticConfig.GetBool(HTTPConfigRetryEnabled) {
   120  		retryCount := staticConfig.GetInt(HTTPConfigRetryCount)
   121  		minTimeout := staticConfig.GetDuration(HTTPConfigRetryInitDelay)
   122  		maxTimeout := staticConfig.GetDuration(HTTPConfigRetryMaxDelay)
   123  		client.
   124  			SetRetryCount(retryCount).
   125  			SetRetryWaitTime(minTimeout).
   126  			SetRetryMaxWaitTime(maxTimeout).
   127  			AddRetryCondition(func(r *resty.Response, err error) bool {
   128  				if r == nil || r.IsSuccess() {
   129  					return false
   130  				}
   131  				rctx := r.Request.Context()
   132  				rc := rctx.Value(retryCtxKey{}).(*retryCtx)
   133  				log.L(rctx).Infof("retry %d/%d (min=%dms/max=%dms) status=%d", rc.attempts, retryCount, minTimeout.Milliseconds(), maxTimeout.Milliseconds(), r.StatusCode())
   134  				rc.attempts++
   135  				return true
   136  			})
   137  	}
   138  
   139  	return client
   140  }
   141  
   142  func WrapRestErr(ctx context.Context, res *resty.Response, err error, key i18n.MessageKey) error {
   143  	var respData string
   144  	if res != nil {
   145  		if res.RawBody() != nil {
   146  			defer func() { _ = res.RawBody().Close() }()
   147  			if r, err := ioutil.ReadAll(res.RawBody()); err == nil {
   148  				respData = string(r)
   149  			}
   150  		}
   151  		if respData == "" {
   152  			respData = res.String()
   153  		}
   154  		if len(respData) > 256 {
   155  			respData = respData[0:256] + "..."
   156  		}
   157  	}
   158  	if err != nil {
   159  		return i18n.WrapError(ctx, err, key, respData)
   160  	}
   161  	return i18n.NewError(ctx, key, respData)
   162  }