diff --git a/help_test.go b/help_test.go index 068e494..70878c0 100644 --- a/help_test.go +++ b/help_test.go @@ -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 @@ -36,7 +38,7 @@ 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. @@ -44,6 +46,36 @@ func (c *mockRealtimeClient) Do(req *http.Request) (*http.Response, error) { 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 +} diff --git a/monitor.go b/monitor.go index 24f467f..ba00dc1 100644 --- a/monitor.go +++ b/monitor.go @@ -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)) @@ -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 = "" } - 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 } } diff --git a/monitor_manager_test.go b/monitor_manager_test.go index bf643ac..446ebef 100644 --- a/monitor_manager_test.go +++ b/monitor_manager_test.go @@ -11,7 +11,7 @@ import ( func TestMonitorManager(t *testing.T) { var ( - client = fixedResponseClient{"{}"} + client = fixedResponseClient{200, "{}"} token = "irrelevant-token" cache = newNameCache() metrics = prometheusMetrics{} diff --git a/monitor_test.go b/monitor_test.go index ae1c071..2b90e7d 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -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": [ { diff --git a/service_queryer_test.go b/service_queryer_test.go index cd19ee1..4d207b8 100644 --- a/service_queryer_test.go +++ b/service_queryer_test.go @@ -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 {