1// Copyright 2016 Circonus, Inc. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package api 6 7import ( 8 "bytes" 9 "context" 10 crand "crypto/rand" 11 "crypto/tls" 12 "crypto/x509" 13 "errors" 14 "fmt" 15 "io/ioutil" 16 "log" 17 "math" 18 "math/big" 19 "math/rand" 20 "net" 21 "net/http" 22 "net/url" 23 "os" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/hashicorp/go-retryablehttp" 29) 30 31func init() { 32 n, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) 33 if err != nil { 34 rand.Seed(time.Now().UTC().UnixNano()) 35 return 36 } 37 rand.Seed(n.Int64()) 38} 39 40const ( 41 // a few sensible defaults 42 defaultAPIURL = "https://api.circonus.com/v2" 43 defaultAPIApp = "circonus-gometrics" 44 minRetryWait = 1 * time.Second 45 maxRetryWait = 15 * time.Second 46 maxRetries = 4 // equating to 1 + maxRetries total attempts 47) 48 49// TokenKeyType - Circonus API Token key 50type TokenKeyType string 51 52// TokenAppType - Circonus API Token app name 53type TokenAppType string 54 55// TokenAccountIDType - Circonus API Token account id 56type TokenAccountIDType string 57 58// CIDType Circonus object cid 59type CIDType *string 60 61// IDType Circonus object id 62type IDType int 63 64// URLType submission url type 65type URLType string 66 67// SearchQueryType search query (see: https://login.circonus.com/resources/api#searching) 68type SearchQueryType string 69 70// SearchFilterType search filter (see: https://login.circonus.com/resources/api#filtering) 71type SearchFilterType map[string][]string 72 73// TagType search/select/custom tag(s) type 74type TagType []string 75 76// Config options for Circonus API 77type Config struct { 78 // URL defines the API URL - default https://api.circonus.com/v2/ 79 URL string 80 81 // TokenKey defines the key to use when communicating with the API 82 TokenKey string 83 84 // TokenApp defines the app to use when communicating with the API 85 TokenApp string 86 87 TokenAccountID string 88 89 // CACert deprecating, use TLSConfig instead 90 CACert *x509.CertPool 91 92 // TLSConfig defines a custom tls configuration to use when communicating with the API 93 TLSConfig *tls.Config 94 95 Log *log.Logger 96 Debug bool 97} 98 99// API Circonus API 100type API struct { 101 apiURL *url.URL 102 key TokenKeyType 103 app TokenAppType 104 accountID TokenAccountIDType 105 caCert *x509.CertPool 106 tlsConfig *tls.Config 107 Debug bool 108 Log *log.Logger 109 useExponentialBackoff bool 110 useExponentialBackoffmu sync.Mutex 111} 112 113// NewClient returns a new Circonus API (alias for New) 114func NewClient(ac *Config) (*API, error) { 115 return New(ac) 116} 117 118// NewAPI returns a new Circonus API (alias for New) 119func NewAPI(ac *Config) (*API, error) { 120 return New(ac) 121} 122 123// New returns a new Circonus API 124func New(ac *Config) (*API, error) { 125 126 if ac == nil { 127 return nil, errors.New("Invalid API configuration (nil)") 128 } 129 130 key := TokenKeyType(ac.TokenKey) 131 if key == "" { 132 return nil, errors.New("API Token is required") 133 } 134 135 app := TokenAppType(ac.TokenApp) 136 if app == "" { 137 app = defaultAPIApp 138 } 139 140 acctID := TokenAccountIDType(ac.TokenAccountID) 141 142 au := string(ac.URL) 143 if au == "" { 144 au = defaultAPIURL 145 } 146 if !strings.Contains(au, "/") { 147 // if just a hostname is passed, ASSume "https" and a path prefix of "/v2" 148 au = fmt.Sprintf("https://%s/v2", ac.URL) 149 } 150 if last := len(au) - 1; last >= 0 && au[last] == '/' { 151 // strip off trailing '/' 152 au = au[:last] 153 } 154 apiURL, err := url.Parse(au) 155 if err != nil { 156 return nil, err 157 } 158 159 a := &API{ 160 apiURL: apiURL, 161 key: key, 162 app: app, 163 accountID: acctID, 164 caCert: ac.CACert, 165 tlsConfig: ac.TLSConfig, 166 Debug: ac.Debug, 167 Log: ac.Log, 168 useExponentialBackoff: false, 169 } 170 171 a.Debug = ac.Debug 172 a.Log = ac.Log 173 if a.Debug && a.Log == nil { 174 a.Log = log.New(os.Stderr, "", log.LstdFlags) 175 } 176 if a.Log == nil { 177 a.Log = log.New(ioutil.Discard, "", log.LstdFlags) 178 } 179 180 return a, nil 181} 182 183// EnableExponentialBackoff enables use of exponential backoff for next API call(s) 184// and use exponential backoff for all API calls until exponential backoff is disabled. 185func (a *API) EnableExponentialBackoff() { 186 a.useExponentialBackoffmu.Lock() 187 a.useExponentialBackoff = true 188 a.useExponentialBackoffmu.Unlock() 189} 190 191// DisableExponentialBackoff disables use of exponential backoff. If a request using 192// exponential backoff is currently running, it will stop using exponential backoff 193// on its next iteration (if needed). 194func (a *API) DisableExponentialBackoff() { 195 a.useExponentialBackoffmu.Lock() 196 a.useExponentialBackoff = false 197 a.useExponentialBackoffmu.Unlock() 198} 199 200// Get API request 201func (a *API) Get(reqPath string) ([]byte, error) { 202 return a.apiRequest("GET", reqPath, nil) 203} 204 205// Delete API request 206func (a *API) Delete(reqPath string) ([]byte, error) { 207 return a.apiRequest("DELETE", reqPath, nil) 208} 209 210// Post API request 211func (a *API) Post(reqPath string, data []byte) ([]byte, error) { 212 return a.apiRequest("POST", reqPath, data) 213} 214 215// Put API request 216func (a *API) Put(reqPath string, data []byte) ([]byte, error) { 217 return a.apiRequest("PUT", reqPath, data) 218} 219 220func backoff(interval uint) float64 { 221 return math.Floor(((float64(interval) * (1 + rand.Float64())) / 2) + .5) 222} 223 224// apiRequest manages retry strategy for exponential backoffs 225func (a *API) apiRequest(reqMethod string, reqPath string, data []byte) ([]byte, error) { 226 backoffs := []uint{2, 4, 8, 16, 32} 227 attempts := 0 228 success := false 229 230 var result []byte 231 var err error 232 233 for !success { 234 result, err = a.apiCall(reqMethod, reqPath, data) 235 if err == nil { 236 success = true 237 } 238 239 // break and return error if not using exponential backoff 240 if err != nil { 241 if !a.useExponentialBackoff { 242 break 243 } 244 if strings.Contains(err.Error(), "code 403") { 245 break 246 } 247 } 248 249 if !success { 250 var wait float64 251 if attempts >= len(backoffs) { 252 wait = backoff(backoffs[len(backoffs)-1]) 253 } else { 254 wait = backoff(backoffs[attempts]) 255 } 256 attempts++ 257 a.Log.Printf("[WARN] API call failed %s, retrying in %d seconds.\n", err.Error(), uint(wait)) 258 time.Sleep(time.Duration(wait) * time.Second) 259 } 260 } 261 262 return result, err 263} 264 265// apiCall call Circonus API 266func (a *API) apiCall(reqMethod string, reqPath string, data []byte) ([]byte, error) { 267 reqURL := a.apiURL.String() 268 269 if reqPath == "" { 270 return nil, errors.New("Invalid URL path") 271 } 272 if reqPath[:1] != "/" { 273 reqURL += "/" 274 } 275 if len(reqPath) >= 3 && reqPath[:3] == "/v2" { 276 reqURL += reqPath[3:] 277 } else { 278 reqURL += reqPath 279 } 280 281 // keep last HTTP error in the event of retry failure 282 var lastHTTPError error 283 retryPolicy := func(ctx context.Context, resp *http.Response, err error) (bool, error) { 284 if ctxErr := ctx.Err(); ctxErr != nil { 285 return false, ctxErr 286 } 287 288 if err != nil { 289 lastHTTPError = err 290 return true, err 291 } 292 // Check the response code. We retry on 500-range responses to allow 293 // the server time to recover, as 500's are typically not permanent 294 // errors and may relate to outages on the server side. This will catch 295 // invalid response codes as well, like 0 and 999. 296 // Retry on 429 (rate limit) as well. 297 if resp.StatusCode == 0 || // wtf?! 298 resp.StatusCode >= 500 || // rutroh 299 resp.StatusCode == 429 { // rate limit 300 body, readErr := ioutil.ReadAll(resp.Body) 301 if readErr != nil { 302 lastHTTPError = fmt.Errorf("- response: %d %s", resp.StatusCode, readErr.Error()) 303 } else { 304 lastHTTPError = fmt.Errorf("- response: %d %s", resp.StatusCode, strings.TrimSpace(string(body))) 305 } 306 return true, nil 307 } 308 return false, nil 309 } 310 311 dataReader := bytes.NewReader(data) 312 313 req, err := retryablehttp.NewRequest(reqMethod, reqURL, dataReader) 314 if err != nil { 315 return nil, fmt.Errorf("[ERROR] creating API request: %s %+v", reqURL, err) 316 } 317 req.Header.Add("Accept", "application/json") 318 req.Header.Add("X-Circonus-Auth-Token", string(a.key)) 319 req.Header.Add("X-Circonus-App-Name", string(a.app)) 320 if string(a.accountID) != "" { 321 req.Header.Add("X-Circonus-Account-ID", string(a.accountID)) 322 } 323 324 client := retryablehttp.NewClient() 325 if a.apiURL.Scheme == "https" { 326 var tlscfg *tls.Config 327 if a.tlsConfig != nil { // preference full custom tls config 328 tlscfg = a.tlsConfig 329 } else if a.caCert != nil { 330 tlscfg = &tls.Config{RootCAs: a.caCert} 331 } 332 client.HTTPClient.Transport = &http.Transport{ 333 Proxy: http.ProxyFromEnvironment, 334 Dial: (&net.Dialer{ 335 Timeout: 30 * time.Second, 336 KeepAlive: 30 * time.Second, 337 }).Dial, 338 TLSHandshakeTimeout: 10 * time.Second, 339 TLSClientConfig: tlscfg, 340 DisableKeepAlives: true, 341 MaxIdleConnsPerHost: -1, 342 DisableCompression: true, 343 } 344 } else { 345 client.HTTPClient.Transport = &http.Transport{ 346 Proxy: http.ProxyFromEnvironment, 347 Dial: (&net.Dialer{ 348 Timeout: 30 * time.Second, 349 KeepAlive: 30 * time.Second, 350 }).Dial, 351 TLSHandshakeTimeout: 10 * time.Second, 352 DisableKeepAlives: true, 353 MaxIdleConnsPerHost: -1, 354 DisableCompression: true, 355 } 356 } 357 358 a.useExponentialBackoffmu.Lock() 359 eb := a.useExponentialBackoff 360 a.useExponentialBackoffmu.Unlock() 361 362 if eb { 363 // limit to one request if using exponential backoff 364 client.RetryWaitMin = 1 365 client.RetryWaitMax = 2 366 client.RetryMax = 0 367 } else { 368 client.RetryWaitMin = minRetryWait 369 client.RetryWaitMax = maxRetryWait 370 client.RetryMax = maxRetries 371 } 372 373 // retryablehttp only groks log or no log 374 if a.Debug { 375 client.Logger = a.Log 376 } else { 377 client.Logger = log.New(ioutil.Discard, "", log.LstdFlags) 378 } 379 380 client.CheckRetry = retryPolicy 381 382 resp, err := client.Do(req) 383 if err != nil { 384 if lastHTTPError != nil { 385 return nil, lastHTTPError 386 } 387 return nil, fmt.Errorf("[ERROR] %s: %+v", reqURL, err) 388 } 389 390 defer resp.Body.Close() 391 body, err := ioutil.ReadAll(resp.Body) 392 if err != nil { 393 return nil, fmt.Errorf("[ERROR] reading response %+v", err) 394 } 395 396 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 397 msg := fmt.Sprintf("API response code %d: %s", resp.StatusCode, string(body)) 398 if a.Debug { 399 a.Log.Printf("[DEBUG] %s\n", msg) 400 } 401 402 return nil, fmt.Errorf("[ERROR] %s", msg) 403 } 404 405 return body, nil 406} 407