删除 fork 独有的实时日志相关功能(上游 Wei-Shaw/sub2api 不存在):
A. OpsLogBroadcaster + SSE 日志流(前端有用但用户不需要):
- backend/internal/service/ops_log_broadcaster{,_test}.go
- backend/internal/handler/ops_log_stream_middleware.go
- backend/internal/handler/admin/ops_log_stream_handler.go
- backend/internal/server/routes/admin.go: GET /admin/ops/logs/{stream,recent}
- backend/internal/server/routes/{gateway,windsurf_gateway}.go: opsLogStream middleware
- backend/internal/service/wire.go: ProvideOpsLogBroadcaster
- frontend/src/views/admin/ops/OpsLogStreamView.vue
- frontend/src/api/admin/ops.ts: subscribeOpsLogStream, getRecentOpsLogs,
OpsLogEntry/OpsLogFilter/OpsLogRecentResponse 类型
- frontend/src/router/index.ts: AdminOpsLogStream 路由
- frontend/src/components/layout/AppSidebar.vue: 侧边栏入口
- frontend/src/i18n/locales/{en,zh}.ts: nav.opsLogStream + admin.ops.logStream 全部文案
B. RequestEventBus + WS 请求事件流(前端零调用 dead code):
- backend/internal/service/request_event_bus{,_test}.go
- backend/internal/handler/admin/ops_ws_requests_handler.go
- backend/internal/server/routes/admin.go: GET /admin/ops/ws/requests
- backend/internal/handler/gateway_handler.go: RequestEventBus 字段/参数 +
reqStartTime + reqEventAccountID/reqEventStatus 跟踪 + defer Publish
- backend/internal/service/wire.go: NewRequestEventBus
- backend/internal/handler/admin/ops_handler.go: OpsHandler 中
requestEventBus + logBroadcaster 字段,简化 NewOpsHandler 签名
保留:
- /admin/ops/ws/qps (前端 QPS 监控仍在用)
- /admin/ops/realtime-traffic (前端在用)
- OpsErrorLoggerMiddleware (与本次无关)
签名变更:
- NewOpsHandler(opsService) — 移除 requestEventBus, logBroadcaster
- NewGatewayHandler(...): 移除 requestEventBus 末位参数
- ProvideRouter / SetupRouter / registerRoutes / RegisterGatewayRoutes /
RegisterWindsurfGatewayRoutes: 移除 opsLogBroadcaster 参数
- 同步更新 wire_gen.go + 测试调用点
验证:
- 后端 go build/vet 通过
- 前端 pnpm run build 通过 (9.48s)
- 测试: 2 个 baseline 既存失败 (TestProxyImportData...,
TestWindsurfTierAccessService_Snapshot_HappyPath) 与本次无关
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type testSettingRepo struct {
|
|
values map[string]string
|
|
}
|
|
|
|
func newTestSettingRepo() *testSettingRepo {
|
|
return &testSettingRepo{values: map[string]string{}}
|
|
}
|
|
|
|
func (s *testSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) {
|
|
v, err := s.GetValue(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &service.Setting{Key: key, Value: v}, nil
|
|
}
|
|
func (s *testSettingRepo) GetValue(ctx context.Context, key string) (string, error) {
|
|
v, ok := s.values[key]
|
|
if !ok {
|
|
return "", service.ErrSettingNotFound
|
|
}
|
|
return v, nil
|
|
}
|
|
func (s *testSettingRepo) Set(ctx context.Context, key, value string) error {
|
|
s.values[key] = value
|
|
return nil
|
|
}
|
|
func (s *testSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
|
out := make(map[string]string, len(keys))
|
|
for _, k := range keys {
|
|
if v, ok := s.values[k]; ok {
|
|
out[k] = v
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
func (s *testSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error {
|
|
for k, v := range settings {
|
|
s.values[k] = v
|
|
}
|
|
return nil
|
|
}
|
|
func (s *testSettingRepo) GetAll(ctx context.Context) (map[string]string, error) {
|
|
out := make(map[string]string, len(s.values))
|
|
for k, v := range s.values {
|
|
out[k] = v
|
|
}
|
|
return out, nil
|
|
}
|
|
func (s *testSettingRepo) Delete(ctx context.Context, key string) error {
|
|
delete(s.values, key)
|
|
return nil
|
|
}
|
|
|
|
func newOpsRuntimeRouter(handler *OpsHandler, withUser bool) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
if withUser {
|
|
r.Use(func(c *gin.Context) {
|
|
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 7})
|
|
c.Next()
|
|
})
|
|
}
|
|
r.GET("/runtime/logging", handler.GetRuntimeLogConfig)
|
|
r.PUT("/runtime/logging", handler.UpdateRuntimeLogConfig)
|
|
r.POST("/runtime/logging/reset", handler.ResetRuntimeLogConfig)
|
|
return r
|
|
}
|
|
|
|
func newRuntimeOpsService(t *testing.T) *service.OpsService {
|
|
t.Helper()
|
|
if err := logger.Init(logger.InitOptions{
|
|
Level: "info",
|
|
Format: "json",
|
|
ServiceName: "sub2api",
|
|
Environment: "test",
|
|
Output: logger.OutputOptions{
|
|
ToStdout: false,
|
|
ToFile: false,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("init logger: %v", err)
|
|
}
|
|
|
|
settingRepo := newTestSettingRepo()
|
|
cfg := &config.Config{
|
|
Ops: config.OpsConfig{Enabled: true},
|
|
Log: config.LogConfig{
|
|
Level: "info",
|
|
Caller: true,
|
|
StacktraceLevel: "error",
|
|
Sampling: config.LogSamplingConfig{
|
|
Enabled: false,
|
|
Initial: 100,
|
|
Thereafter: 100,
|
|
},
|
|
},
|
|
}
|
|
return service.NewOpsService(nil, settingRepo, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
|
|
}
|
|
|
|
func TestOpsRuntimeLoggingHandler_GetConfig(t *testing.T) {
|
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
|
r := newOpsRuntimeRouter(h, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/runtime/logging", nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d, want 200", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestOpsRuntimeLoggingHandler_UpdateUnauthorized(t *testing.T) {
|
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
|
r := newOpsRuntimeRouter(h, false)
|
|
|
|
body := `{"level":"debug","enable_sampling":false,"sampling_initial":100,"sampling_thereafter":100,"caller":true,"stacktrace_level":"error","retention_days":30}`
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/runtime/logging", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status=%d, want 401", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestOpsRuntimeLoggingHandler_UpdateAndResetSuccess(t *testing.T) {
|
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
|
r := newOpsRuntimeRouter(h, true)
|
|
|
|
payload := map[string]any{
|
|
"level": "debug",
|
|
"enable_sampling": false,
|
|
"sampling_initial": 100,
|
|
"sampling_thereafter": 100,
|
|
"caller": true,
|
|
"stacktrace_level": "error",
|
|
"retention_days": 30,
|
|
}
|
|
raw, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/runtime/logging", bytes.NewReader(raw))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("update status=%d, want 200, body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPost, "/runtime/logging/reset", nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("reset status=%d, want 200, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|