Extend retry backoff; rework logging (LOGLEVEL, request lines, colors)
Backoff now covers all dial-phase errors (refused, timeout, DNS), TLS handshake failures, and 5xx responses with replayable bodies; streamed POSTs are never replayed to avoid duplicate work. First retry of an episode logs at WARN, subsequent attempts at INFO. LOGLEVEL (LOG_LEVEL kept as alias) now defaults to warn: startup logs version plus every setting; INFO adds one line per incoming request and per response with status/duration, ANSI-colored in text mode (bypasses slog's escaping so colors render in docker compose logs); NO_COLOR or LOG_FORMAT=json disables colors.
This commit is contained in:
@@ -2,6 +2,7 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -116,7 +117,91 @@ func TestRetryViaReverseProxy(t *testing.T) {
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:1/", nil)
|
||||
_, err = rt.RoundTrip(req)
|
||||
if err == nil || !isConnRefused(err) {
|
||||
if err == nil || !shouldRetry(err) {
|
||||
t.Fatalf("err = %v, want connection refused", err)
|
||||
}
|
||||
}
|
||||
|
||||
// flakyStatus returns 500 for the first fails requests, then 200.
|
||||
type flakyStatus struct {
|
||||
fails int
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *flakyStatus) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
s.calls++
|
||||
code := 200
|
||||
if s.calls <= s.fails {
|
||||
code = 500
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: code,
|
||||
Status: http.StatusText(code),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestRetryTransport5xxGet(t *testing.T) {
|
||||
fs := &flakyStatus{fails: 2}
|
||||
rt := &retryTransport{base: fs, initial: time.Millisecond, max: 2 * time.Millisecond}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://upstream/api/version", nil)
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if fs.calls != 3 {
|
||||
t.Fatalf("calls = %d, want 3", fs.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryTransport5xxPostNotRetried(t *testing.T) {
|
||||
fs := &flakyStatus{fails: 10}
|
||||
rt := &retryTransport{base: fs, initial: time.Millisecond, max: time.Millisecond}
|
||||
|
||||
// A streamed body (no GetBody) must not be replayed after a 500.
|
||||
req, _ := http.NewRequest(http.MethodPost, "http://upstream/prompt", io.NopCloser(strings.NewReader("{}")))
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 500 {
|
||||
t.Fatalf("status = %d, want 500", resp.StatusCode)
|
||||
}
|
||||
if fs.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1 (no retry for streamed POST)", fs.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetryClassification(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{"dial refused", &net.OpError{Op: "dial", Net: "tcp", Err: syscall.ECONNREFUSED}, true},
|
||||
{"dial timeout", &net.OpError{Op: "dial", Net: "tcp", Err: timeoutErr{}}, true},
|
||||
{"dns", &net.DNSError{Err: "no such host", IsNotFound: true}, true},
|
||||
{"tls record", tls.RecordHeaderError{Msg: "bad"}, true},
|
||||
{"tls alert", tls.AlertError(42), true},
|
||||
{"read error mid-request", &net.OpError{Op: "read", Net: "tcp", Err: syscall.ECONNRESET}, false},
|
||||
{"plain error", io.EOF, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := shouldRetry(c.err); got != c.want {
|
||||
t.Errorf("%s: shouldRetry = %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type timeoutErr struct{}
|
||||
|
||||
func (timeoutErr) Error() string { return "i/o timeout" }
|
||||
func (timeoutErr) Timeout() bool { return true }
|
||||
func (timeoutErr) Temporary() bool { return true }
|
||||
|
||||
Reference in New Issue
Block a user