mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-09 21:38:19 +01:00
s3tables, iceberg: log client refusals at V(1), not ERROR (#11896)
* 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>
This commit is contained in:
1 parent
780760fccc
commit
97f8ca13ff
11 files changed
+313
-23
No files matched your search
@@ -397,7 +397,7 @@ func (s *Server) handleUpdateTable(w http.ResponseWriter, r *http.Request) {
|
||||
if cleanupErr := s.deleteMetadataFile(r.Context(), metadataBucket, metadataPath, metadataFileName); cleanupErr != nil {
|
||||
glog.V(1).Infof("Iceberg: failed to cleanup metadata file %s after update failure: %v", newMetadataLocation, cleanupErr)
|
||||
}
|
||||
glog.Errorf("Iceberg: CommitTable UpdateTable error: %v", err)
|
||||
logManagerError("CommitTable UpdateTable", err)
|
||||
writeManagerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (s *Server) handleCreateNamespace(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusConflict, "AlreadyExistsException", err.Error())
|
||||
return
|
||||
}
|
||||
glog.Errorf("Iceberg: CreateNamespace error: %v", err)
|
||||
logManagerError("CreateNamespace", err)
|
||||
writeManagerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (s *Server) handleCommitTransaction(w http.ResponseWriter, r *http.Request)
|
||||
writeError(w, http.StatusConflict, "CommitFailedException", "Version token mismatch")
|
||||
return
|
||||
}
|
||||
glog.Errorf("Iceberg: CommitTransaction UpdateTable error: %v", err)
|
||||
logManagerError("CommitTransaction UpdateTable", err)
|
||||
writeManagerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package iceberg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
func TestLogManagerErrorRefusalsAreNotErrors(t *testing.T) {
|
||||
var refusals, faults []string
|
||||
oldR, oldF := logManagerRefusal, logManagerFault
|
||||
logManagerRefusal = func(f string, a ...interface{}) { refusals = append(refusals, fmt.Sprintf(f, a...)) }
|
||||
logManagerFault = func(f string, a ...interface{}) { faults = append(faults, fmt.Sprintf(f, a...)) }
|
||||
defer func() { logManagerRefusal, logManagerFault = oldR, oldF }()
|
||||
for _, c := range []struct {
|
||||
err error
|
||||
fault bool
|
||||
}{
|
||||
{&s3tables.S3TablesError{Type: s3tables.ErrCodeAccessDenied, Message: "not authorized to create namespace in this bucket"}, false},
|
||||
{&s3tables.S3TablesError{Type: s3tables.ErrCodeNoSuchNamespace, Message: "no such namespace"}, false},
|
||||
{&s3tables.S3TablesError{Type: s3tables.ErrCodeNamespaceAlreadyExists, Message: "exists"}, false},
|
||||
{errors.New("filer: connection refused"), true}, // positive control: a real fault stays at ERROR
|
||||
} {
|
||||
refusals, faults = nil, nil
|
||||
logManagerError("CreateNamespace", c.err)
|
||||
wantFaults, wantRefusals := 0, 1
|
||||
if c.fault {
|
||||
wantFaults, wantRefusals = 1, 0
|
||||
}
|
||||
if len(faults) != wantFaults || len(refusals) != wantRefusals {
|
||||
t.Errorf("%v: %d ERROR and %d refusal log(s), want %d and %d (refusals %v, faults %v)", c.err, len(faults), len(refusals), wantFaults, wantRefusals, refusals, faults)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-16
@@ -158,7 +158,9 @@ func nameValidationError(err error) bool {
|
||||
// errors (400) and must carry the reason -- a variant field without
|
||||
// format-version 3 otherwise reads as a bare 500 with "variant is not supported
|
||||
// until v3" only in the log. Anything else is a server fault (500).
|
||||
func writeManagerError(w http.ResponseWriter, err error) {
|
||||
// classifyManagerError maps a storage-layer error to the HTTP status, the
|
||||
// Iceberg error type and the message the catalog answers with.
|
||||
func classifyManagerError(err error) (int, string, string) {
|
||||
switch {
|
||||
case nameValidationError(err),
|
||||
errors.Is(err, iceberg.ErrInvalidSchema),
|
||||
@@ -166,8 +168,7 @@ func writeManagerError(w http.ResponseWriter, err error) {
|
||||
errors.Is(err, iceberg.ErrInvalidTypeString),
|
||||
errors.Is(err, iceberg.ErrInvalidTransform),
|
||||
errors.Is(err, iceberg.ErrInvalidArgument):
|
||||
writeError(w, http.StatusBadRequest, "BadRequestException", err.Error())
|
||||
return
|
||||
return http.StatusBadRequest, "BadRequestException", err.Error()
|
||||
}
|
||||
// Storage-layer failures are mostly the client's, not the server's. Reporting
|
||||
// a missing namespace or a name conflict as a 500 makes the catalog look
|
||||
@@ -179,31 +180,49 @@ func writeManagerError(w http.ResponseWriter, err error) {
|
||||
// The storage-layer message names the resolved bucket, which for a
|
||||
// client that sent no warehouse at all is the default one it never
|
||||
// asked for, so say how to select a real table bucket.
|
||||
writeError(w, http.StatusNotFound, "NoSuchNamespaceException",
|
||||
fmt.Sprintf("%s: each table bucket is a separate catalog, select one with warehouse=s3://<table-bucket>/ or /v1/<table-bucket>/", tableErr.Message))
|
||||
return http.StatusNotFound, "NoSuchNamespaceException",
|
||||
fmt.Sprintf("%s: each table bucket is a separate catalog, select one with warehouse=s3://<table-bucket>/ or /v1/<table-bucket>/", tableErr.Message)
|
||||
case s3tables.ErrCodeNoSuchNamespace:
|
||||
writeError(w, http.StatusNotFound, "NoSuchNamespaceException", tableErr.Message)
|
||||
return http.StatusNotFound, "NoSuchNamespaceException", tableErr.Message
|
||||
case s3tables.ErrCodeNoSuchTable:
|
||||
writeError(w, http.StatusNotFound, "NoSuchTableException", tableErr.Message)
|
||||
return http.StatusNotFound, "NoSuchTableException", tableErr.Message
|
||||
case s3tables.ErrCodeNoSuchView:
|
||||
writeError(w, http.StatusNotFound, "NoSuchViewException", tableErr.Message)
|
||||
return http.StatusNotFound, "NoSuchViewException", tableErr.Message
|
||||
case s3tables.ErrCodeNamespaceAlreadyExists, s3tables.ErrCodeBucketAlreadyExists,
|
||||
s3tables.ErrCodeTableAlreadyExists, s3tables.ErrCodeViewAlreadyExists:
|
||||
writeError(w, http.StatusConflict, "AlreadyExistsException", tableErr.Message)
|
||||
return http.StatusConflict, "AlreadyExistsException", tableErr.Message
|
||||
case s3tables.ErrCodeNamespaceNotEmpty, s3tables.ErrCodeBucketNotEmpty:
|
||||
writeError(w, http.StatusConflict, "AlreadyExistsException", tableErr.Message)
|
||||
return http.StatusConflict, "AlreadyExistsException", tableErr.Message
|
||||
case s3tables.ErrCodeConflict:
|
||||
writeError(w, http.StatusConflict, "CommitFailedException", tableErr.Message)
|
||||
return http.StatusConflict, "CommitFailedException", tableErr.Message
|
||||
case s3tables.ErrCodeAccessDenied:
|
||||
writeError(w, http.StatusForbidden, "ForbiddenException", tableErr.Message)
|
||||
return http.StatusForbidden, "ForbiddenException", tableErr.Message
|
||||
case s3tables.ErrCodeInvalidRequest, s3tables.ErrCodeInvalidIcebergLayout:
|
||||
writeError(w, http.StatusBadRequest, "BadRequestException", tableErr.Message)
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "InternalServerError", err.Error())
|
||||
return http.StatusBadRequest, "BadRequestException", tableErr.Message
|
||||
}
|
||||
}
|
||||
return http.StatusInternalServerError, "InternalServerError", err.Error()
|
||||
}
|
||||
|
||||
func writeManagerError(w http.ResponseWriter, err error) {
|
||||
status, typ, msg := classifyManagerError(err)
|
||||
writeError(w, status, typ, msg)
|
||||
}
|
||||
|
||||
// A storage-layer error the catalog answers with a 4xx (access denied, not
|
||||
// found, already exists, not empty, a conflict) is the API refusing a request
|
||||
// as intended, not a server fault; it is logged at V(1). A 5xx stays at ERROR.
|
||||
var (
|
||||
logManagerRefusal = func(format string, args ...interface{}) { glog.V(1).Infof(format, args...) }
|
||||
logManagerFault = glog.Errorf
|
||||
)
|
||||
|
||||
func logManagerError(op string, err error) {
|
||||
if status, _, _ := classifyManagerError(err); status >= 400 && status < 500 {
|
||||
logManagerRefusal("Iceberg: %s refused (%d): %v", op, status, err)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "InternalServerError", err.Error())
|
||||
logManagerFault("Iceberg: %s error: %v", op, err)
|
||||
}
|
||||
|
||||
// resolveWarehouseBucket maps a client-supplied warehouse value to a table
|
||||
|
||||
Reference in new issue
Block a user