mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 22:11:39 +02:00
* s3tables, iceberg: log client refusals at V(1), not ERROR Requests the S3 Tables and Iceberg REST APIs correctly refuse with a 4xx (access denied, not authorized, no such namespace, already exists, not empty, a commit conflict) were logged with glog.Errorf, so a client probing for what it may do, or retrying an optimistic commit, filled the ERROR log and hid real faults from anyone alerting on it. - s3tables: HandleRequest wraps the ResponseWriter in a statusRecorder (with Unwrap for http.ResponseController) and logs a handler error by the status it answered: 4xx at V(1) as "<op> refused (<status>)", 5xx or no status at ERROR as before. - s3tables: the "handleCreateNamespace called" trace moves from ERROR to V(3). - iceberg: writeManagerError's mapping is split into classifyManagerError, shared with a new logManagerError that logs 4xx-mapped errors at V(1) and everything else at ERROR; used by CreateNamespace, CommitTable and CommitTransaction. - storage: loading a remote-tier volume logged "readSuperBlock remote volume N: <nil>" at ERROR even when the read succeeded; it is now logged only when there is an error. Tests: TestRequestRefusedWith4xxIsNotLoggedAsError, TestLogRequestErrorByStatus, TestStatusRecorderKeepsTheFirstStatus, TestStatusRecorderUnwrapsForResponseController, TestNoTraceLoggedAtErrorLevel (fails on master on the CreateNamespace trace), TestLogManagerErrorRefusalsAreNotErrors, TestRemoteSuperBlockLogOnlyOnError. * s3tables: do not log a malformed CreateNamespace body at ERROR handleCreateNamespace logged a body it could not decode at ERROR before answering 400; the dispatcher already logs that refusal once at V(1). Also drop statusRecorder.Flush: it made the recorder an http.Flusher even over a writer that cannot flush, so http.NewResponseController reported success instead of ErrNotSupported. Unwrap already lets the controller reach the wrapped writer. * iceberg: require exactly one log line per classified manager error The test accepted a refusal that produced no log, or both logs. Require one V(1) refusal and no ERROR for a 4xx, and the reverse for a fault. --------- Co-authored-by: Javier de la Torre <jatorre@lakehousebox.com>
124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
)
|
|
|
|
// captureLogs swaps the two log sinks for recorders and restores them when the test ends.
|
|
func captureLogs(t *testing.T) (refusals, serverErrors *[]string) {
|
|
t.Helper()
|
|
var r, s []string
|
|
oldR, oldS := logClientRefusal, logServerError
|
|
logClientRefusal = func(f string, a ...interface{}) { r = append(r, fmt.Sprintf(f, a...)) }
|
|
logServerError = func(f string, a ...interface{}) { s = append(s, fmt.Sprintf(f, a...)) }
|
|
t.Cleanup(func() { logClientRefusal, logServerError = oldR, oldS })
|
|
return &r, &s
|
|
}
|
|
|
|
// A refusal the API answers with a 4xx is not a server error.
|
|
func TestRequestRefusedWith4xxIsNotLoggedAsError(t *testing.T) {
|
|
refusals, serverErrors := captureLogs(t)
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not json"))
|
|
req.Header.Set("X-Amz-Target", "S3Tables.ListNamespaces")
|
|
rec := httptest.NewRecorder()
|
|
h.HandleRequest(rec, req, nil)
|
|
if rec.Code < 400 || rec.Code >= 500 {
|
|
t.Fatalf("the probe request must be refused with a 4xx for this test to mean anything, got %d", rec.Code) // positive control
|
|
}
|
|
if len(*serverErrors) != 0 {
|
|
t.Fatalf("a %d refusal was logged at ERROR: %v", rec.Code, *serverErrors)
|
|
}
|
|
if len(*refusals) != 1 || !strings.Contains((*refusals)[0], "ListNamespaces refused") {
|
|
t.Fatalf("the refusal must still be logged once (at V(1)), got %v", *refusals)
|
|
}
|
|
}
|
|
|
|
func TestLogRequestErrorByStatus(t *testing.T) {
|
|
for _, c := range []struct {
|
|
status int
|
|
wantError bool
|
|
}{{http.StatusForbidden, false}, {http.StatusNotFound, false}, {http.StatusConflict, false},
|
|
{http.StatusInternalServerError, true}, {http.StatusServiceUnavailable, true}, {0, true}} {
|
|
refusals, serverErrors := captureLogs(t)
|
|
logRequestError("Op", c.status, errors.New("boom"))
|
|
if gotError := len(*serverErrors) == 1 && len(*refusals) == 0; gotError != c.wantError {
|
|
t.Errorf("status %d: ERROR=%v, want %v (refusals %v, errors %v)", c.status, gotError, c.wantError, *refusals, *serverErrors)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusRecorderKeepsTheFirstStatus(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
sw := &statusRecorder{ResponseWriter: rec}
|
|
_, _ = sw.Write([]byte("x"))
|
|
sw.WriteHeader(http.StatusInternalServerError)
|
|
if sw.status != http.StatusOK {
|
|
t.Fatalf("an implicit 200 by Write must win, got %d", sw.status)
|
|
}
|
|
sw2 := &statusRecorder{ResponseWriter: httptest.NewRecorder()}
|
|
sw2.WriteHeader(http.StatusForbidden)
|
|
if sw2.status != http.StatusForbidden {
|
|
t.Fatalf("got %d", sw2.status)
|
|
}
|
|
}
|
|
|
|
func TestStatusRecorderUnwrapsForResponseController(t *testing.T) {
|
|
sw := &statusRecorder{ResponseWriter: httptest.NewRecorder()}
|
|
if err := http.NewResponseController(sw).Flush(); err != nil {
|
|
t.Fatalf("ResponseController through the recorder: %v", err)
|
|
}
|
|
if sw.Unwrap() == nil {
|
|
t.Fatal("Unwrap must return the wrapped writer")
|
|
}
|
|
}
|
|
|
|
// A malformed CreateNamespace body is answered with a 400; neither the
|
|
// dispatcher nor the handler itself may log it at ERROR.
|
|
func TestMalformedCreateNamespaceIsNotLoggedAsError(t *testing.T) {
|
|
refusals, serverErrors := captureLogs(t)
|
|
|
|
before := glog.Stats.Error.Lines()
|
|
glog.Errorf("S3Tables test: positive control for the ERROR line counter")
|
|
if glog.Stats.Error.Lines() != before+1 {
|
|
t.Fatal("the ERROR line counter did not see a direct glog.Errorf") // positive control
|
|
}
|
|
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not json"))
|
|
req.Header.Set("X-Amz-Target", "S3Tables.CreateNamespace")
|
|
rec := httptest.NewRecorder()
|
|
before = glog.Stats.Error.Lines()
|
|
h.HandleRequest(rec, req, nil)
|
|
errorLines := glog.Stats.Error.Lines() - before
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("a malformed body must be refused with 400, got %d", rec.Code)
|
|
}
|
|
if errorLines != 0 || len(*serverErrors) != 0 {
|
|
t.Fatalf("a 400 refusal was logged at ERROR: %d direct line(s), dispatcher %v", errorLines, *serverErrors)
|
|
}
|
|
if len(*refusals) != 1 || !strings.Contains((*refusals)[0], "CreateNamespace refused") {
|
|
t.Fatalf("the refusal must still be logged once (at V(1)), got %v", *refusals)
|
|
}
|
|
}
|
|
|
|
// The recorder must not claim to flush a writer that cannot.
|
|
func TestStatusRecorderFlushFollowsTheWrappedWriter(t *testing.T) {
|
|
type plainWriter struct{ http.ResponseWriter }
|
|
sw := &statusRecorder{ResponseWriter: plainWriter{httptest.NewRecorder()}}
|
|
if err := http.NewResponseController(sw).Flush(); !errors.Is(err, http.ErrNotSupported) {
|
|
t.Fatalf("Flush through a recorder over a non-flushing writer: %v, want ErrNotSupported", err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
if err := http.NewResponseController(&statusRecorder{ResponseWriter: rec}).Flush(); err != nil || !rec.Flushed {
|
|
t.Fatalf("Flush through a recorder over a flushing writer: err=%v flushed=%v", err, rec.Flushed) // positive control
|
|
}
|
|
}
|