feat(api): add log_request_body_enabled feature flag support

Add runtime feature flag to control whether request bodies are stored
in logs. The Handler now accepts a Redis client to check the
log_request_body_enabled feature flag before persisting log records.

- Add logRequestBodyFeatureKey constant for feature flag
- Inject Redis client into Handler for feature flag lookups
- Strip request body from log records when feature is disabled
- Update tests to pass Redis client to NewHandler
This commit is contained in:
zenfun
2025-12-21 13:26:16 +08:00
parent 1089f9490e
commit 00192f937e
5 changed files with 31 additions and 9 deletions

View File

@@ -119,7 +119,7 @@ func main() {
masterService := service.NewMasterService(db) masterService := service.NewMasterService(db)
healthService := service.NewHealthCheckService(db, rdb) healthService := service.NewHealthCheckService(db, rdb)
handler := api.NewHandler(db, syncService, logWriter) handler := api.NewHandler(db, syncService, logWriter, rdb)
adminHandler := api.NewAdminHandler(db, masterService, syncService) adminHandler := api.NewAdminHandler(db, masterService, syncService)
masterHandler := api.NewMasterHandler(db, masterService, syncService) masterHandler := api.NewMasterHandler(db, masterService, syncService)
internalHandler := api.NewInternalHandler(db) internalHandler := api.NewInternalHandler(db)

View File

@@ -14,10 +14,11 @@ import (
const featuresKey = "meta:features" const featuresKey = "meta:features"
const ( const (
logRetentionFeatureKey = "log_retention_days" logRetentionFeatureKey = "log_retention_days"
logMaxRecordsFeatureKey = "log_max_records" logMaxRecordsFeatureKey = "log_max_records"
logRetentionRedisKey = "meta:log:retention_days" logRequestBodyFeatureKey = "log_request_body_enabled"
logMaxRecordsRedisKey = "meta:log:max_records" logRetentionRedisKey = "meta:log:retention_days"
logMaxRecordsRedisKey = "meta:log:max_records"
) )
// FeatureHandler manages lightweight feature flags stored in Redis for DP/CP runtime toggles. // FeatureHandler manages lightweight feature flags stored in Redis for DP/CP runtime toggles.

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -11,6 +12,7 @@ import (
groupx "github.com/ez-api/foundation/group" groupx "github.com/ez-api/foundation/group"
"github.com/ez-api/foundation/provider" "github.com/ez-api/foundation/provider"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -18,10 +20,11 @@ type Handler struct {
db *gorm.DB db *gorm.DB
sync *service.SyncService sync *service.SyncService
logger *service.LogWriter logger *service.LogWriter
rdb *redis.Client
} }
func NewHandler(db *gorm.DB, sync *service.SyncService, logger *service.LogWriter) *Handler { func NewHandler(db *gorm.DB, sync *service.SyncService, logger *service.LogWriter, rdb *redis.Client) *Handler {
return &Handler{db: db, sync: sync, logger: logger} return &Handler{db: db, sync: sync, logger: logger, rdb: rdb}
} }
// CreateKey is now handled by MasterHandler // CreateKey is now handled by MasterHandler
@@ -463,7 +466,25 @@ func (h *Handler) IngestLog(c *gin.Context) {
return return
} }
if !h.logRequestBodyEnabled(c.Request.Context()) {
rec.RequestBody = ""
}
// By default, only metadata is expected; payload fields may be empty. // By default, only metadata is expected; payload fields may be empty.
h.logger.Write(rec) h.logger.Write(rec)
c.JSON(http.StatusAccepted, gin.H{"status": "queued"}) c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
} }
func (h *Handler) logRequestBodyEnabled(ctx context.Context) bool {
if h == nil || h.rdb == nil {
return true
}
raw, err := h.rdb.HGet(ctx, featuresKey, logRequestBodyFeatureKey).Result()
if err == redis.Nil || strings.TrimSpace(raw) == "" {
return true
}
if err != nil {
return true
}
return strings.EqualFold(raw, "true") || strings.EqualFold(raw, "1")
}

View File

@@ -33,7 +33,7 @@ func newTestHandlerWithRedis(t *testing.T) (*Handler, *gorm.DB, *miniredis.Minir
mr := miniredis.RunT(t) mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
sync := service.NewSyncService(rdb) sync := service.NewSyncService(rdb)
return NewHandler(db, sync, nil), db, mr return NewHandler(db, sync, nil, rdb), db, mr
} }
func TestCreateModel_DefaultsKindChat_AndWritesModelsMeta(t *testing.T) { func TestCreateModel_DefaultsKindChat_AndWritesModelsMeta(t *testing.T) {

View File

@@ -36,7 +36,7 @@ func newTestHandler(t *testing.T) (*Handler, *gorm.DB) {
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
sync := service.NewSyncService(rdb) sync := service.NewSyncService(rdb)
return NewHandler(db, sync, nil), db return NewHandler(db, sync, nil, rdb), db
} }
func TestCreateProvider_DefaultsVertexLocationGlobal(t *testing.T) { func TestCreateProvider_DefaultsVertexLocationGlobal(t *testing.T) {