Skip to content

Commit

Permalink
Don't spam rt.fastly.com if -token is bad. (#25)
Browse files Browse the repository at this point in the history
* Don't spam rt.fastly.com in case of bad token

* User-Agent: Fastly-Exporter (version)
  • Loading branch information
peterbourgon authored Nov 28, 2018
1 parent bffc479 commit 34afc09
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 7 deletions.
36 changes: 34 additions & 2 deletions help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ func (m *mockManager) update(ids []string) {
}

type fixedResponseClient struct {
code int
response string
}

func (c fixedResponseClient) Do(req *http.Request) (*http.Response, error) {
rec := httptest.NewRecorder()
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(c.code)
fmt.Fprint(w, c.response)
}).ServeHTTP(rec, req)
return rec.Result(), nil
Expand All @@ -36,14 +38,44 @@ type mockRealtimeClient struct {
func (c *mockRealtimeClient) Do(req *http.Request) (*http.Response, error) {
// First request immediately returns real data.
if atomic.AddUint64(&(c.served), 1) == 1 {
return fixedResponseClient{c.response}.Do(req)
return fixedResponseClient{200, c.response}.Do(req)
}

// Subsequent requests block a bit and then return empty JSON.
select {
case <-req.Context().Done():
return nil, req.Context().Err()
case <-time.After(time.Second):
return fixedResponseClient{"{}"}.Do(req)
return fixedResponseClient{200, "{}"}.Do(req)
}
}

type countingRealtimeClient struct {
code int
response string
served uint64
}

func (c *countingRealtimeClient) Do(req *http.Request) (*http.Response, error) {
atomic.AddUint64(&(c.served), 1)
return fixedResponseClient{c.code, c.response}.Do(req)
}

type userAgentCapturingClient struct {
userAgent atomic.Value
}

func (c *userAgentCapturingClient) Do(req *http.Request) (*http.Response, error) {
c.userAgent.Store(req.Header.Get("User-Agent"))
return fixedResponseClient{200, "{}"}.Do(req)
}

func within(d time.Duration, f func() bool) bool {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
if f() { // 🔥
return true
}
}
return false
}
18 changes: 15 additions & 3 deletions monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func monitor(ctx context.Context, client httpClient, token string, serviceID str
if err != nil {
return err // fatal for sure
}
req.Header.Set("User-Agent", "Fastly-Exporter ("+version+")")
req.Header.Set("Fastly-Key", token)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req.WithContext(ctx))
Expand All @@ -40,17 +41,28 @@ func monitor(ctx context.Context, client httpClient, token string, serviceID str
}
var rt realtimeResponse
if err := json.NewDecoder(resp.Body).Decode(&rt); err != nil {
resp.Body.Close()
level.Error(logger).Log("err", err)
contextSleep(ctx, time.Second)
continue
}
resp.Body.Close()
rterr := rt.Error
if rterr == "" {
rterr = "<none>"
}
level.Debug(logger).Log("response_ts", rt.Timestamp, "err", rterr)
process(rt, serviceID, serviceName, metrics)
postprocess()
switch resp.StatusCode {
case http.StatusOK:
level.Debug(logger).Log("status_code", resp.StatusCode, "response_ts", rt.Timestamp, "err", rterr)
process(rt, serviceID, serviceName, metrics)
postprocess()
case http.StatusUnauthorized, http.StatusForbidden:
level.Error(logger).Log("status_code", resp.StatusCode, "response_ts", rt.Timestamp, "err", rterr, "msg", "-token is likely invalid")
contextSleep(ctx, 15*time.Second)
default:
level.Error(logger).Log("status_code", resp.StatusCode, "response_ts", rt.Timestamp, "err", rterr)
contextSleep(ctx, 5*time.Second)
}
ts = rt.Timestamp
}
}
Expand Down
2 changes: 1 addition & 1 deletion monitor_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func TestMonitorManager(t *testing.T) {
var (
client = fixedResponseClient{"{}"}
client = fixedResponseClient{200, "{}"}
token = "irrelevant-token"
cache = newNameCache()
metrics = prometheusMetrics{}
Expand Down
67 changes: 67 additions & 0 deletions monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,73 @@ func TestMonitorFixture(t *testing.T) {
}
}

func TestMonitorBadToken(t *testing.T) {
// This test should ensure we don't spam rt.fastly.com in a hot loop
// when we provide a bad token and the API returns 403 Forbidden.

var (
ctx, cancel = context.WithCancel(context.Background())
done = make(chan struct{})
client = &countingRealtimeClient{403, `{"Error": "unauthorized"}`, 0}
token = "presumably-bad-token"
serviceID = "some-service-id"
cache = newNameCache()
metrics = prometheusMetrics{}
postprocess = func() {}
logger = log.NewNopLogger()
)

go func() {
monitor(ctx, client, token, serviceID, cache, metrics, postprocess, logger)
close(done)
}()

defer func() {
cancel()
<-done
}()

time.Sleep(time.Second)
if want, have := uint64(1), atomic.LoadUint64(&(client.served)); want != have {
t.Fatalf("request count: want %d, have %d", want, have)
}
}

func TestUserAgent(t *testing.T) {
var (
ctx, cancel = context.WithCancel(context.Background())
done = make(chan struct{})
client = &userAgentCapturingClient{}
token = "presumably-bad-token"
serviceID = "some-service-id"
cache = newNameCache()
metrics = prometheusMetrics{}
postprocess = func() {}
logger = log.NewNopLogger()
)

go func() {
monitor(ctx, client, token, serviceID, cache, metrics, postprocess, logger)
close(done)
}()

defer func() {
cancel()
<-done
}()

var (
want = "Fastly-Exporter (" + version + ")"
have string
)
if !within(time.Second, func() bool {
have, _ = client.userAgent.Load().(string)
return want == have
}) {
t.Fatalf("User-Agent: want %q, have %q", want, have)
}
}

const rtResponseFixture = `{
"Data": [
{
Expand Down
2 changes: 1 addition & 1 deletion service_queryer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestServiceQueryerFixture(t *testing.T) {
cache = newNameCache()
manager = &mockManager{}
queryer = newServiceQueryer(token, ids, cache, manager)
client = fixedResponseClient{serviceResponseFixture}
client = fixedResponseClient{200, serviceResponseFixture}
)

if err := queryer.refresh(client); err != nil {
Expand Down

0 comments on commit 34afc09

Please sign in to comment.