diff --git a/internal/api/server.go b/internal/api/server.go index a7aef0aa..7f44d085 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -59,10 +59,8 @@ type ServerOption func(*serverOptionConfig) func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger { configDir := filepath.Dir(configPath) - if base := util.WritablePath(); base != "" { - return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir, cfg.ErrorLogsMaxFiles) - } - return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir, cfg.ErrorLogsMaxFiles) + logsDir := logging.ResolveLogDirectory(cfg) + return logging.NewFileRequestLogger(cfg.RequestLog, logsDir, configDir, cfg.ErrorLogsMaxFiles) } // WithMiddleware appends additional Gin middleware during server construction. diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 06653210..f5c18aa1 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -7,9 +7,11 @@ import ( "path/filepath" "strings" "testing" + "time" gin "github.com/gin-gonic/gin" proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" @@ -109,3 +111,100 @@ func TestAmpProviderModelRoutes(t *testing.T) { }) } } + +func TestDefaultRequestLoggerFactory_UsesResolvedLogDirectory(t *testing.T) { + t.Setenv("WRITABLE_PATH", "") + t.Setenv("writable_path", "") + + originalWD, errGetwd := os.Getwd() + if errGetwd != nil { + t.Fatalf("failed to get current working directory: %v", errGetwd) + } + + tmpDir := t.TempDir() + if errChdir := os.Chdir(tmpDir); errChdir != nil { + t.Fatalf("failed to switch working directory: %v", errChdir) + } + defer func() { + if errChdirBack := os.Chdir(originalWD); errChdirBack != nil { + t.Fatalf("failed to restore working directory: %v", errChdirBack) + } + }() + + // Force ResolveLogDirectory to fallback to auth-dir/logs by making ./logs not a writable directory. + if errWriteFile := os.WriteFile(filepath.Join(tmpDir, "logs"), []byte("not-a-directory"), 0o644); errWriteFile != nil { + t.Fatalf("failed to create blocking logs file: %v", errWriteFile) + } + + configDir := filepath.Join(tmpDir, "config") + if errMkdirConfig := os.MkdirAll(configDir, 0o755); errMkdirConfig != nil { + t.Fatalf("failed to create config dir: %v", errMkdirConfig) + } + configPath := filepath.Join(configDir, "config.yaml") + + authDir := filepath.Join(tmpDir, "auth") + if errMkdirAuth := os.MkdirAll(authDir, 0o700); errMkdirAuth != nil { + t.Fatalf("failed to create auth dir: %v", errMkdirAuth) + } + + cfg := &proxyconfig.Config{ + SDKConfig: proxyconfig.SDKConfig{ + RequestLog: false, + }, + AuthDir: authDir, + ErrorLogsMaxFiles: 10, + } + + logger := defaultRequestLoggerFactory(cfg, configPath) + fileLogger, ok := logger.(*internallogging.FileRequestLogger) + if !ok { + t.Fatalf("expected *FileRequestLogger, got %T", logger) + } + + errLog := fileLogger.LogRequestWithOptions( + "/v1/chat/completions", + http.MethodPost, + map[string][]string{"Content-Type": []string{"application/json"}}, + []byte(`{"input":"hello"}`), + http.StatusBadGateway, + map[string][]string{"Content-Type": []string{"application/json"}}, + []byte(`{"error":"upstream failure"}`), + nil, + nil, + nil, + true, + "issue-1711", + time.Now(), + time.Now(), + ) + if errLog != nil { + t.Fatalf("failed to write forced error request log: %v", errLog) + } + + authLogsDir := filepath.Join(authDir, "logs") + authEntries, errReadAuthDir := os.ReadDir(authLogsDir) + if errReadAuthDir != nil { + t.Fatalf("failed to read auth logs dir %s: %v", authLogsDir, errReadAuthDir) + } + foundErrorLogInAuthDir := false + for _, entry := range authEntries { + if strings.HasPrefix(entry.Name(), "error-") && strings.HasSuffix(entry.Name(), ".log") { + foundErrorLogInAuthDir = true + break + } + } + if !foundErrorLogInAuthDir { + t.Fatalf("expected forced error log in auth fallback dir %s, got entries: %+v", authLogsDir, authEntries) + } + + configLogsDir := filepath.Join(configDir, "logs") + configEntries, errReadConfigDir := os.ReadDir(configLogsDir) + if errReadConfigDir != nil && !os.IsNotExist(errReadConfigDir) { + t.Fatalf("failed to inspect config logs dir %s: %v", configLogsDir, errReadConfigDir) + } + for _, entry := range configEntries { + if strings.HasPrefix(entry.Name(), "error-") && strings.HasSuffix(entry.Name(), ".log") { + t.Fatalf("unexpected forced error log in config dir %s", configLogsDir) + } + } +}