fix(accounts): attribute connectivity test failure logs

This commit is contained in:
Achord Chan
2026-10-06 13:59:37 +08:00
parent b8dece9000
commit 5ef517d0fc
3 changed files with 145 additions and 1 deletions
@@ -0,0 +1,48 @@
package service
import (
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/util/logredact"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
const accountTestBackgroundKey = "account_test_background"
// Bind attribution before lookup so missing accounts are identifiable too. The
// request logger retains HTTP request IDs or the caller's background correlation.
func initAccountTestLogger(c *gin.Context, accountID int64, modelID, mode string) {
source := "http"
if c.GetBool(accountTestBackgroundKey) {
source = "background"
}
ctx := c.Request.Context()
l := logger.FromContext(ctx).With(
zap.String("component", "service.account_test"),
zap.Int64("account_id", accountID),
zap.String("test_id", uuid.NewString()),
zap.String("test_source", source),
zap.String("requested_model", modelID),
zap.String("test_mode", normalizeAccountTestMode(mode)),
)
c.Request = c.Request.WithContext(logger.IntoContext(ctx, l))
}
func bindAccountTestPlatform(c *gin.Context, account *Account) {
ctx := c.Request.Context()
l := logger.FromContext(ctx).With(
zap.String("platform", account.Platform),
zap.String("account_type", account.Type),
)
c.Request = c.Request.WithContext(logger.IntoContext(ctx, l))
}
func logAccountTestError(c *gin.Context, message string) {
// Redact known credential fields before bounding text; truncating first can
// cut a quoted credential in half and prevent the redactor from matching it.
message = logredact.RedactText(message, "api_key", "authorization")
logger.FromContext(c.Request.Context()).Warn("account_test.failed",
zap.String("error", truncateString(message, 2048)),
)
}
@@ -0,0 +1,93 @@
//go:build unit
package service
import (
"context"
"errors"
"net/http/httptest"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
type accountTestLoggingRepo struct {
AccountRepository
account *Account
}
func (r *accountTestLoggingRepo) GetByID(context.Context, int64) (*Account, error) {
if r.account == nil {
return nil, errors.New("missing")
}
return r.account, nil
}
func TestAccountTestFailureLogAttribution(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, background := range []bool{false, true} {
for _, missing := range []bool{false, true} {
core, logs := observer.New(zap.WarnLevel)
ctx := logger.IntoContext(context.Background(), zap.New(core).With(zap.String("request_id", "req-test")))
repo := &accountTestLoggingRepo{}
if !missing {
repo.account = &Account{ID: 77, Platform: PlatformAnthropic, Type: AccountTypeAPIKey}
}
svc := &AccountTestService{accountRepo: repo}
if background {
result, err := svc.RunTestBackground(ctx, 77, "test-model")
require.NoError(t, err)
require.Equal(t, "failed", result.Status)
require.NotEmpty(t, result.ErrorMessage)
} else {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest("POST", "/accounts/77/test", nil).WithContext(ctx)
err := svc.TestAccountConnection(c, 77, "test-model", "private prompt", "")
require.Error(t, err)
require.Contains(t, rec.Body.String(), `"type":"error"`)
}
entries := logs.FilterMessage("account_test.failed").All()
require.Len(t, entries, 1)
fields := entries[0].ContextMap()
require.Equal(t, int64(77), fields["account_id"])
require.Equal(t, "req-test", fields["request_id"])
require.Equal(t, "test-model", fields["requested_model"])
require.NotEmpty(t, fields["test_id"])
if background {
require.Equal(t, "background", fields["test_source"])
} else {
require.Equal(t, "http", fields["test_source"])
}
if !missing {
require.Equal(t, PlatformAnthropic, fields["platform"])
}
require.NotContains(t, fields, "prompt")
}
}
}
func TestAccountTestErrorLogRedaction(t *testing.T) {
core, logs := observer.New(zap.WarnLevel)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/test", nil).WithContext(logger.IntoContext(context.Background(), zap.New(core)))
initAccountTestLogger(c, 1, "", "")
message := `API returned 403: {"api_key":"secret-key","access_token":"secret-token","message":"denied"}`
err := (&AccountTestService{}).sendErrorAndEnd(c, message)
require.EqualError(t, err, message)
entry := logs.FilterMessage("account_test.failed").All()[0]
logged, ok := entry.ContextMap()["error"].(string)
require.True(t, ok)
require.NotContains(t, logged, "secret-key")
require.NotContains(t, logged, "secret-token")
require.Contains(t, logged, "denied")
logAccountTestError(c, strings.Repeat("x", 10000))
last, ok := logs.All()[1].ContextMap()["error"].(string)
require.True(t, ok)
require.Less(t, len(last), 2100)
}
@@ -359,6 +359,7 @@ func createTestPayload(modelID string) (map[string]any, error) {
// mode is optional - "compact" routes OpenAI accounts to the /responses/compact probe path
// opts is optional media (image/audio data URLs for real generation / STT).
func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64, modelID string, prompt string, mode string, opts ...AccountTestOptions) error {
initAccountTestLogger(c, accountID, modelID, mode)
ctx := c.Request.Context()
testOpts := firstAccountTestOptions(opts)
@@ -367,6 +368,7 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
if err != nil {
return s.sendErrorAndEnd(c, "Account not found")
}
bindAccountTestPlatform(c, account)
// Synthetic UI load-test accounts exercise the real SSE parsing and modal
// interactions, but intentionally do not send their placeholder credentials
@@ -3271,7 +3273,7 @@ func (s *AccountTestService) sendEvent(c *gin.Context, event TestEvent) {
// sendErrorAndEnd sends an error event and ends the stream
func (s *AccountTestService) sendErrorAndEnd(c *gin.Context, errorMsg string) error {
log.Printf("Account test error: %s", errorMsg)
logAccountTestError(c, errorMsg)
s.sendEvent(c, TestEvent{Type: "error", Error: errorMsg})
return fmt.Errorf("%s", errorMsg)
}
@@ -3284,6 +3286,7 @@ func (s *AccountTestService) RunTestBackground(ctx context.Context, accountID in
w := httptest.NewRecorder()
ginCtx, _ := gin.CreateTestContext(w)
ginCtx.Request = (&http.Request{}).WithContext(ctx)
ginCtx.Set(accountTestBackgroundKey, true)
testErr := s.TestAccountConnection(ginCtx, accountID, modelID, "", AccountTestModeDefault)