github.1485827954.workers.dev/newrelic/newrelic-client-go@v1.1.0/internal/http/client.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"reflect"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	retryablehttp "github.com/hashicorp/go-retryablehttp"
    15  
    16  	"github.com/newrelic/newrelic-client-go/internal/version"
    17  	"github.com/newrelic/newrelic-client-go/pkg/config"
    18  	nrErrors "github.com/newrelic/newrelic-client-go/pkg/errors"
    19  	"github.com/newrelic/newrelic-client-go/pkg/logging"
    20  )
    21  
    22  const (
    23  	defaultNewRelicRequestingServiceHeader = "NewRelic-Requesting-Services"
    24  	defaultServiceName                     = "newrelic-client-go"
    25  	defaultTimeout                         = time.Second * 30
    26  	defaultRetryMax                        = 3
    27  )
    28  
    29  var (
    30  	defaultUserAgent = fmt.Sprintf("newrelic/%s/%s (https://github.com/newrelic/%s)", defaultServiceName, version.Version, defaultServiceName)
    31  )
    32  
    33  // Client represents a client for communicating with the New Relic APIs.
    34  type Client struct {
    35  	// client represents the underlying HTTP client.
    36  	client *retryablehttp.Client
    37  
    38  	// config is the HTTP client configuration.
    39  	config config.Config
    40  
    41  	// authStrategy allows us to use multiple authentication methods for API calls
    42  	authStrategy RequestAuthorizer
    43  
    44  	// compressor is used to compress the body of a request, and set the content-encoding header
    45  	compressor RequestCompressor
    46  
    47  	errorValue ErrorResponse
    48  
    49  	logger logging.Logger
    50  }
    51  
    52  // NewClient is used to create a new instance of Client.
    53  func NewClient(cfg config.Config) Client {
    54  	c := http.Client{
    55  		Timeout: defaultTimeout,
    56  	}
    57  
    58  	if cfg.Timeout != nil {
    59  		c.Timeout = *cfg.Timeout
    60  	}
    61  
    62  	if cfg.HTTPTransport != nil {
    63  		c.Transport = cfg.HTTPTransport
    64  	} else {
    65  		c.Transport = http.DefaultTransport
    66  	}
    67  
    68  	if cfg.UserAgent == "" {
    69  		cfg.UserAgent = defaultUserAgent
    70  	}
    71  
    72  	// Either set or append the library name
    73  	if cfg.ServiceName == "" {
    74  		cfg.ServiceName = defaultServiceName
    75  	} else {
    76  		cfg.ServiceName = fmt.Sprintf("%s|%s", cfg.ServiceName, defaultServiceName)
    77  	}
    78  
    79  	r := retryablehttp.NewClient()
    80  	r.HTTPClient = &c
    81  	r.RetryMax = defaultRetryMax
    82  	r.CheckRetry = RetryPolicy
    83  
    84  	// Disable logging in go-retryablehttp since we are logging requests directly here
    85  	r.Logger = nil
    86  
    87  	// Use the logger from the configuration or use a default NewStructuredLogger.
    88  	var logger logging.Logger
    89  	if cfg.Logger != nil {
    90  		logger = cfg.Logger
    91  	} else {
    92  		logger = logging.NewLogrusLogger()
    93  	}
    94  
    95  	client := Client{
    96  		authStrategy: &ClassicV2Authorizer{},
    97  		client:       r,
    98  		config:       cfg,
    99  		errorValue:   &DefaultErrorResponse{},
   100  		logger:       logger,
   101  	}
   102  
   103  	switch cfg.Compression {
   104  	case config.Compression.Gzip:
   105  		client.compressor = &GzipCompressor{}
   106  	default:
   107  		client.compressor = &NoneCompressor{}
   108  	}
   109  
   110  	return client
   111  }
   112  
   113  // SetAuthStrategy is used to set the default auth strategy for this client
   114  // which can be overridden per request
   115  func (c *Client) SetAuthStrategy(da RequestAuthorizer) {
   116  	c.authStrategy = da
   117  }
   118  
   119  // SetRequestCompressor is used to enable compression on the request using
   120  // the RequestCompressor specified
   121  func (c *Client) SetRequestCompressor(compressor RequestCompressor) {
   122  	c.compressor = compressor
   123  }
   124  
   125  // SetErrorValue is used to unmarshal error body responses in JSON format.
   126  func (c *Client) SetErrorValue(v ErrorResponse) *Client {
   127  	c.errorValue = v
   128  	return c
   129  }
   130  
   131  // Get represents an HTTP GET request to a New Relic API.
   132  // The queryParams argument can be used to add query string parameters to the requested URL.
   133  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   134  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   135  func (c *Client) Get(
   136  	url string,
   137  	queryParams interface{},
   138  	respBody interface{},
   139  ) (*http.Response, error) {
   140  	return c.GetWithContext(context.Background(), url, queryParams, respBody)
   141  }
   142  
   143  // GetWithContext represents an HTTP GET request to a New Relic API.
   144  // The queryParams argument can be used to add query string parameters to the requested URL.
   145  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   146  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   147  func (c *Client) GetWithContext(
   148  	ctx context.Context,
   149  	url string,
   150  	queryParams interface{},
   151  	respBody interface{},
   152  ) (*http.Response, error) {
   153  	req, err := c.NewRequest(http.MethodGet, url, queryParams, nil, respBody)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	req.WithContext(ctx)
   159  
   160  	return c.Do(req)
   161  }
   162  
   163  // Post represents an HTTP POST request to a New Relic API.
   164  // The queryParams argument can be used to add query string parameters to the requested URL.
   165  // The reqBody argument will be marshaled to JSON from the type provided and included in the request body.
   166  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   167  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   168  func (c *Client) Post(
   169  	url string,
   170  	queryParams interface{},
   171  	reqBody interface{},
   172  	respBody interface{},
   173  ) (*http.Response, error) {
   174  	return c.PostWithContext(context.Background(), url, queryParams, reqBody, respBody)
   175  }
   176  
   177  // PostWithContext represents an HTTP POST request to a New Relic API.
   178  // The queryParams argument can be used to add query string parameters to the requested URL.
   179  // The reqBody argument will be marshaled to JSON from the type provided and included in the request body.
   180  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   181  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   182  func (c *Client) PostWithContext(
   183  	ctx context.Context,
   184  	url string,
   185  	queryParams interface{},
   186  	reqBody interface{},
   187  	respBody interface{},
   188  ) (*http.Response, error) {
   189  	req, err := c.NewRequest(http.MethodPost, url, queryParams, reqBody, respBody)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	req.WithContext(ctx)
   195  
   196  	return c.Do(req)
   197  }
   198  
   199  // Put represents an HTTP PUT request to a New Relic API.
   200  // The queryParams argument can be used to add query string parameters to the requested URL.
   201  // The reqBody argument will be marshaled to JSON from the type provided and included in the request body.
   202  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   203  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   204  func (c *Client) Put(
   205  	url string,
   206  	queryParams interface{},
   207  	reqBody interface{},
   208  	respBody interface{},
   209  ) (*http.Response, error) {
   210  	return c.PutWithContext(context.Background(), url, queryParams, reqBody, respBody)
   211  }
   212  
   213  // PutWithContext represents an HTTP PUT request to a New Relic API.
   214  // The queryParams argument can be used to add query string parameters to the requested URL.
   215  // The reqBody argument will be marshaled to JSON from the type provided and included in the request body.
   216  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   217  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   218  func (c *Client) PutWithContext(
   219  	ctx context.Context,
   220  	url string,
   221  	queryParams interface{},
   222  	reqBody interface{},
   223  	respBody interface{},
   224  ) (*http.Response, error) {
   225  	req, err := c.NewRequest(http.MethodPut, url, queryParams, reqBody, respBody)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	req.WithContext(ctx)
   231  
   232  	return c.Do(req)
   233  }
   234  
   235  // Delete represents an HTTP DELETE request to a New Relic API.
   236  // The queryParams argument can be used to add query string parameters to the requested URL.
   237  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   238  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   239  func (c *Client) Delete(
   240  	url string,
   241  	queryParams interface{},
   242  	respBody interface{},
   243  ) (*http.Response, error) {
   244  	return c.DeleteWithContext(context.Background(), url, queryParams, respBody)
   245  }
   246  
   247  // DeleteWithContext represents an HTTP DELETE request to a New Relic API.
   248  // The queryParams argument can be used to add query string parameters to the requested URL.
   249  // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
   250  // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
   251  func (c *Client) DeleteWithContext(
   252  	ctx context.Context,
   253  	url string,
   254  	queryParams interface{},
   255  	respBody interface{},
   256  ) (*http.Response, error) {
   257  	req, err := c.NewRequest(http.MethodDelete, url, queryParams, nil, respBody)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	req.WithContext(ctx)
   263  
   264  	return c.Do(req)
   265  }
   266  
   267  // logNice removes newlines, tabs, and \" from the body of a nerdgraph request.
   268  // This allows for easier debugging and testing the content straight from the
   269  // log file.
   270  func logNice(body string) string {
   271  	var newBody string
   272  	newBody = strings.ReplaceAll(body, "\n", " ")
   273  	newBody = strings.ReplaceAll(newBody, "\t", " ")
   274  	newBody = strings.ReplaceAll(newBody, "\\\"", `"`)
   275  	re := regexp.MustCompile(` +`)
   276  	newBody = re.ReplaceAllString(newBody, " ")
   277  
   278  	return newBody
   279  }
   280  
   281  // obfuscate receives a string, and replaces everything after the first 8
   282  // characters with an asterisk before returning the result.
   283  func obfuscate(input string) string {
   284  	result := make([]string, len(input))
   285  	parts := strings.Split(input, "")
   286  
   287  	for i, x := range parts {
   288  		if i < 8 {
   289  			result[i] = x
   290  		} else {
   291  			result[i] = "*"
   292  		}
   293  	}
   294  
   295  	return strings.Join(result, "")
   296  }
   297  
   298  func logCleanHeaderMarshalJSON(header http.Header) ([]byte, error) {
   299  	h := http.Header{}
   300  
   301  	for k, values := range header {
   302  		if _, ok := h[k]; ok {
   303  			h[k] = make([]string, len(values))
   304  		}
   305  
   306  		switch k {
   307  		case "Api-Key", "X-Api-Key", "X-Insert-Key":
   308  			newValues := []string{}
   309  			for _, v := range values {
   310  				newValues = append(newValues, obfuscate(v))
   311  			}
   312  
   313  			if len(newValues) > 0 {
   314  				h[k] = newValues
   315  			} else {
   316  				h[k] = values
   317  			}
   318  		default:
   319  			h[k] = values
   320  		}
   321  	}
   322  
   323  	return json.Marshal(h)
   324  }
   325  
   326  // Do initiates an HTTP request as configured by the passed Request struct.
   327  func (c *Client) Do(req *Request) (*http.Response, error) {
   328  	var resp *http.Response
   329  	var errorValue ErrorResponse
   330  	var body []byte
   331  
   332  	c.logger.Debug("performing request", "method", req.method, "url", req.url)
   333  
   334  	for i := 0; ; i++ {
   335  		var shouldRetry bool
   336  		var err error
   337  		errorValue = req.errorValue.New()
   338  		resp, body, shouldRetry, err = c.innerDo(req, errorValue, i)
   339  
   340  		if serr, ok := err.(*nrErrors.MaxRetriesReached); ok {
   341  			return nil, serr
   342  		}
   343  
   344  		if shouldRetry {
   345  			continue
   346  		}
   347  
   348  		if err != nil {
   349  			return nil, err
   350  		}
   351  
   352  		break
   353  	}
   354  
   355  	if !isResponseSuccess(resp) {
   356  		if errorValue.IsUnauthorized(resp) {
   357  			return nil, nrErrors.NewUnauthorizedError()
   358  		}
   359  
   360  		if errorValue.IsPaymentRequired(resp) {
   361  			return nil, nrErrors.NewPaymentRequiredError()
   362  		}
   363  
   364  		return nil, nrErrors.NewUnexpectedStatusCode(resp.StatusCode, errorValue.Error())
   365  	}
   366  
   367  	if errorValue.IsNotFound() {
   368  		return nil, nrErrors.NewNotFound("resource not found")
   369  	}
   370  
   371  	// Ignore deprecation errors
   372  	if !errorValue.IsDeprecated() {
   373  		if errorValue.Error() != "" {
   374  			return nil, errorValue
   375  		}
   376  	}
   377  
   378  	if req.value == nil {
   379  		return resp, nil
   380  	}
   381  
   382  	jsonErr := json.Unmarshal(body, req.value)
   383  	if jsonErr != nil {
   384  		return nil, jsonErr
   385  	}
   386  
   387  	return resp, nil
   388  }
   389  
   390  func (c *Client) innerDo(req *Request, errorValue ErrorResponse, i int) (*http.Response, []byte, bool, error) {
   391  	r, err := req.makeRequest()
   392  	if err != nil {
   393  		return nil, nil, false, err
   394  	}
   395  
   396  	logHeaders, err := logCleanHeaderMarshalJSON(r.Header)
   397  	if err != nil {
   398  		return nil, nil, false, err
   399  	}
   400  
   401  	if req.reqBody != nil {
   402  		switch reflect.TypeOf(req.reqBody).String() {
   403  		case "*http.graphQLRequest":
   404  			x := req.reqBody.(*graphQLRequest)
   405  
   406  			logVariables, marshalErr := json.Marshal(x.Variables)
   407  			if marshalErr != nil {
   408  				return nil, nil, false, marshalErr
   409  			}
   410  
   411  			c.logger.Trace("request details",
   412  				"headers", logNice(string(logHeaders)),
   413  				"query", logNice(x.Query),
   414  				"variables", string(logVariables),
   415  			)
   416  		case "string":
   417  			c.logger.Trace("request details", "headers", string(logHeaders), "body", logNice(req.reqBody.(string)))
   418  		}
   419  	} else {
   420  		c.logger.Trace("request details", "headers", string(logHeaders))
   421  	}
   422  
   423  	if i > 0 {
   424  		c.logger.Debug(fmt.Sprintf("retrying request (attempt %d)", i), "method", req.method, "url", r.URL)
   425  	}
   426  
   427  	resp, retryErr := c.client.Do(r)
   428  	if retryErr != nil {
   429  		return resp, nil, false, retryErr
   430  	}
   431  
   432  	defer resp.Body.Close()
   433  
   434  	if resp.StatusCode == http.StatusNotFound {
   435  		return resp, nil, false, &nrErrors.NotFound{}
   436  	}
   437  
   438  	body, readErr := ioutil.ReadAll(resp.Body)
   439  
   440  	if readErr != nil {
   441  		return resp, body, false, readErr
   442  	}
   443  
   444  	logHeaders, err = json.Marshal(resp.Header)
   445  	if err != nil {
   446  		return resp, body, false, err
   447  	}
   448  
   449  	c.logger.Trace("request completed", "method", req.method, "url", r.URL, "status_code", resp.StatusCode, "headers", string(logHeaders), "body", string(body))
   450  
   451  	_ = json.Unmarshal(body, &errorValue)
   452  
   453  	if errorValue.IsNotFound() {
   454  		return resp, body, false, nrErrors.NewNotFound(errorValue.Error())
   455  	}
   456  
   457  	if errorValue.IsPaymentRequired(resp) {
   458  		return resp, body, false, nrErrors.NewPaymentRequiredError()
   459  	}
   460  
   461  	if !errorValue.IsRetryableError() {
   462  		return resp, body, false, nil
   463  	}
   464  
   465  	remain := c.client.RetryMax - i
   466  	if remain <= 0 {
   467  		c.logger.Debug(fmt.Sprintf("giving up after %d attempts", c.client.RetryMax), "method", req.method, "url", r.URL)
   468  		return resp, body, false, nrErrors.NewMaxRetriesReached(errorValue.Error())
   469  	}
   470  
   471  	wait := c.client.Backoff(c.client.RetryWaitMin, c.client.RetryWaitMax, i, resp)
   472  
   473  	time.Sleep(wait)
   474  
   475  	return resp, body, true, nil
   476  }
   477  
   478  // Ensures the response status code falls within the
   479  // status codes that are commonly considered successful.
   480  func isResponseSuccess(resp *http.Response) bool {
   481  	statusCode := resp.StatusCode
   482  
   483  	return statusCode >= http.StatusOK && statusCode <= 299
   484  }
   485  
   486  // NerdGraphQuery runs a Nerdgraph query.
   487  func (c *Client) NerdGraphQuery(query string, vars map[string]interface{}, respBody interface{}) error {
   488  	return c.NerdGraphQueryWithContext(context.Background(), query, vars, respBody)
   489  }
   490  
   491  // NerdGraphQueryWithContext runs a Nerdgraph query.
   492  func (c *Client) NerdGraphQueryWithContext(ctx context.Context, query string, vars map[string]interface{}, respBody interface{}) error {
   493  	req, err := c.NewNerdGraphRequest(query, vars, respBody)
   494  	if err != nil {
   495  		return err
   496  	}
   497  
   498  	req.WithContext(ctx)
   499  
   500  	_, err = c.Do(req)
   501  	if err != nil {
   502  		return err
   503  	}
   504  
   505  	return nil
   506  }
   507  
   508  // NewNerdGraphRequest runs a Nerdgraph request object.
   509  func (c *Client) NewNerdGraphRequest(query string, vars map[string]interface{}, respBody interface{}) (*Request, error) {
   510  	graphqlReqBody := &graphQLRequest{
   511  		Query:     query,
   512  		Variables: vars,
   513  	}
   514  
   515  	graphqlRespBody := &graphQLResponse{
   516  		Data: respBody,
   517  	}
   518  
   519  	req, err := c.NewRequest(http.MethodPost, c.config.Region().NerdGraphURL(), nil, graphqlReqBody, graphqlRespBody)
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  
   524  	req.SetAuthStrategy(&NerdGraphAuthorizer{})
   525  	req.SetErrorValue(&GraphQLErrorResponse{})
   526  
   527  	return req, nil
   528  }