Skip to content

Commit

Permalink
remove hardcoded list of rate limited endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlyF committed Dec 31, 2019
1 parent 1406c4c commit 678f9cf
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 81 deletions.
33 changes: 8 additions & 25 deletions ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,22 @@ package datadog
import (
"fmt"
"net/http"
"strings"
"net/url"
)

// The list of Rate Limited Endpoints of the Datadog API.
// https://docs.datadoghq.com/api/?lang=bash#rate-limiting
var (
rateLimitedEndpoints = map[string]string{
"/v1/query": "GET",
"/v1/input": "GET",
"/v1/metrics": "GET",
"/v1/events": "POST",
"/v1/logs-queries/list": "POST",
"/v1/graph/snapshot": "GET",
"/v1/logs/config/indexes": "GET",
func (client *Client) updateRateLimits(resp *http.Response, api *url.URL) error {
if resp == nil || resp.Header == nil || api.Path == "" {
return fmt.Errorf("malformed HTTP content.")
}
)

func isRateLimited(method string, endpoint string) (limited bool, shortEndpoint string) {
for e, m := range rateLimitedEndpoints {
if strings.HasPrefix(endpoint, e) && m == method {
return true, e
}
}
return false, ""
}

func (client *Client) updateRateLimits(resp *http.Response, api string) error {
if resp == nil || resp.Header == nil {
return fmt.Errorf("could not parse headers from the HTTP response.")
if resp.Header.Get("X-RateLimit-Remaining") == "" {
// The endpoint is not Rate Limited.
return nil
}
client.m.Lock()
defer client.m.Unlock()
client.rateLimitingStats[api] = RateLimit{
client.rateLimitingStats[api.Path] = RateLimit{
Limit: resp.Header.Get("X-RateLimit-Limit"),
Reset: resp.Header.Get("X-RateLimit-Reset"),
Period: resp.Header.Get("X-RateLimit-Period"),
Expand Down
65 changes: 17 additions & 48 deletions ratelimit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,91 +4,60 @@ import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"net/url"
"testing"
)

func Test_isRateLimited(t *testing.T) {
tests := []struct {
desc string
endpoint string
method string
isRateLimited bool
endpointFormated string
}{
{
desc: "is rate limited",
endpoint: "/v1/query?&query=avg:system.cpu.user{*}by{host}",
method: "GET",
isRateLimited: true,
endpointFormated: "/v1/query",
},
{
desc: "is not rate limited",
endpoint: "/v1/series?api_key=12",
method: "POST",
isRateLimited: false,
endpointFormated: "",
},
{
desc: "is rate limited but wrong method",
endpoint: "/v1/query?&query=avg:system.cpu.user{*}by{host}",
method: "POST",
isRateLimited: false,
endpointFormated: "",
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("#%d %s", i, tt.desc), func(t *testing.T) {
limited, edpt := isRateLimited(tt.method, tt.endpoint)
assert.Equal(t, limited, tt.isRateLimited)
assert.Equal(t, edpt, tt.endpointFormated)
})
}
}

func Test_updateRateLimits(t *testing.T) {
// fake client to ensure that we are race free.
client := Client{
rateLimitingStats: make(map[string]RateLimit),
}
tests := []struct {
desc string
api string
api *url.URL
resp *http.Response
header RateLimit
error error
}{
{
"nominal case query",
"/v1/query",
&url.URL{Path: "/v1/query"},
makeHeader(RateLimit{"1", "2", "3", "4"}),
RateLimit{"1", "2", "3", "4"},
nil,
},
{
"nominal case logs",
"/v1/logs-queries/list",
&url.URL{Path: "/v1/logs-queries/list"},
makeHeader(RateLimit{"2", "2", "1", "5"}),
RateLimit{"2", "2", "1", "5"},
nil,
},
{
"no response",
"",
&url.URL{Path: ""},
nil,
RateLimit{},
fmt.Errorf("could not parse headers from the HTTP response."),
fmt.Errorf("malformed HTTP content."),
},
{
"no header",
"/v2/error",
&url.URL{Path: "/v2/error"},
makeEmptyHeader(),
RateLimit{},
fmt.Errorf("could not parse headers from the HTTP response."),
fmt.Errorf("malformed HTTP content."),
},
{
"not rate limited",
&url.URL{Path: "/v2/error"},
makeHeader(RateLimit{}),
RateLimit{},
nil,
},
{
"update case query",
"/v1/query",
&url.URL{Path: "/v1/query"},
makeHeader(RateLimit{"2", "4", "6", "4"}),
RateLimit{"2", "4", "6", "4"},
nil,
Expand All @@ -99,7 +68,7 @@ func Test_updateRateLimits(t *testing.T) {
t.Run(fmt.Sprintf("#%d %s", i, tt.desc), func(t *testing.T) {
err := client.updateRateLimits(tt.resp, tt.api)
assert.Equal(t, tt.error, err)
assert.Equal(t, tt.header, client.rateLimitingStats[tt.api])
assert.Equal(t, tt.header, client.rateLimitingStats[tt.api.Path])
})
}
}
Expand Down
12 changes: 4 additions & 8 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ func (client *Client) doJsonRequestUnredacted(method, api string,
if err != nil {
return err
}

// Perform the request and retry it if it's not a POST or PUT request
var resp *http.Response
if method == "POST" || method == "PUT" {
Expand Down Expand Up @@ -154,13 +153,10 @@ func (client *Client) doJsonRequestUnredacted(method, api string,
body = []byte{'{', '}'}
}

limited, short := isRateLimited(method, api)
if limited {
err := client.updateRateLimits(resp, short)
if err != nil {
// Inability to update the rate limiting stats should not be a blocking error.
fmt.Printf("Error Updating the Rate Limit statistics: %s", err.Error())
}
err = client.updateRateLimits(resp, req.URL)
if err != nil {
// Inability to update the rate limiting stats should not be a blocking error.
fmt.Printf("Error Updating the Rate Limit statistics: %s", err.Error())
}

// Try to parse common response fields to check whether there's an error reported in a response.
Expand Down

0 comments on commit 678f9cf

Please sign in to comment.