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 }