feat(logging): introduce centralized logging with custom format and Gin integration

- Implemented a global logger with structured formatting for consistent log output.
- Added support for rotating log files using Lumberjack.
- Integrated new logging functionality with Gin HTTP server for unified log handling.
- Replaced direct `log.Info` calls with `fmt.Printf` in non-critical paths to simplify core functionality.
This commit is contained in:
Luis Pater
2025-09-26 00:54:52 +08:00
parent 72325f792c
commit cf734f7e7b
20 changed files with 209 additions and 148 deletions

View File

@@ -4,106 +4,30 @@
package main package main
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cmd" "github.com/router-for-me/CLIProxyAPI/v6/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
) )
var ( var (
Version = "dev" Version = "dev"
Commit = "none" Commit = "none"
BuildDate = "unknown" BuildDate = "unknown"
logWriter *lumberjack.Logger
ginInfoWriter *io.PipeWriter
ginErrorWriter *io.PipeWriter
) )
// LogFormatter defines a custom log format for logrus. // init initializes the shared logger setup.
// This formatter adds timestamp, log level, and source location information
// to each log entry for better debugging and monitoring.
type LogFormatter struct {
}
// Format renders a single log entry with custom formatting.
// It includes timestamp, log level, source file and line number, and the log message.
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
timestamp := entry.Time.Format("2006-01-02 15:04:05")
var newLog string
// Ensure message doesn't carry trailing newlines; formatter appends one.
msg := strings.TrimRight(entry.Message, "\r\n")
// Customize the log format to include timestamp, level, caller file/line, and message.
newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, msg)
b.WriteString(newLog)
return b.Bytes(), nil
}
// init initializes the logger configuration.
// It sets up the custom log formatter, enables caller reporting,
// and configures the log output destination.
func init() { func init() {
logDir := "logs" logging.SetupBaseLogger()
if err := os.MkdirAll(logDir, 0755); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
os.Exit(1)
}
logWriter = &lumberjack.Logger{
Filename: filepath.Join(logDir, "main.log"),
MaxSize: 10,
MaxBackups: 0,
MaxAge: 0,
Compress: false,
}
log.SetOutput(logWriter)
// Enable reporting the caller function's file and line number.
log.SetReportCaller(true)
// Set the custom log formatter.
log.SetFormatter(&LogFormatter{})
ginInfoWriter = log.StandardLogger().Writer()
gin.DefaultWriter = ginInfoWriter
ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel)
gin.DefaultErrorWriter = ginErrorWriter
gin.DebugPrintFunc = func(format string, values ...interface{}) {
// Trim trailing newlines from Gin's formatted messages to avoid blank lines.
// Gin's debug prints usually include a trailing "\n"; our formatter also appends one.
// Removing it here ensures a single newline per entry.
format = strings.TrimRight(format, "\r\n")
log.StandardLogger().Infof(format, values...)
}
log.RegisterExitHandler(func() {
if logWriter != nil {
_ = logWriter.Close()
}
if ginInfoWriter != nil {
_ = ginInfoWriter.Close()
}
if ginErrorWriter != nil {
_ = ginErrorWriter.Close()
}
})
} }
// main is the entry point of the application. // main is the entry point of the application.
@@ -111,7 +35,6 @@ func init() {
// service based on the provided flags (login, codex-login, or server mode). // service based on the provided flags (login, codex-login, or server mode).
func main() { func main() {
fmt.Printf("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s\n", Version, Commit, BuildDate) fmt.Printf("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s\n", Version, Commit, BuildDate)
log.Infof("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s", Version, Commit, BuildDate)
// Command-line flags to control the application's behavior. // Command-line flags to control the application's behavior.
var login bool var login bool
@@ -189,6 +112,12 @@ func main() {
log.Fatalf("failed to load config: %v", err) log.Fatalf("failed to load config: %v", err)
} }
if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
log.Fatalf("failed to configure log output: %v", err)
}
log.Infof("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s", Version, Commit, BuildDate)
// Set the log level based on the configuration. // Set the log level based on the configuration.
util.SetLogLevel(cfg) util.SetLogLevel(cfg)

View File

@@ -18,6 +18,9 @@ auth-dir: "~/.cli-proxy-api"
# Enable debug logging # Enable debug logging
debug: false debug: false
# When true, write application logs to rotating files instead of stdout
logging-to-file: true
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/ # Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
proxy-url: "" proxy-url: ""

View File

@@ -359,7 +359,7 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco
func (h *Handler) RequestAnthropicToken(c *gin.Context) { func (h *Handler) RequestAnthropicToken(c *gin.Context) {
ctx := context.Background() ctx := context.Background()
log.Info("Initializing Claude authentication...") fmt.Println("Initializing Claude authentication...")
// Generate PKCE codes // Generate PKCE codes
pkceCodes, err := claude.GeneratePKCECodes() pkceCodes, err := claude.GeneratePKCECodes()
@@ -407,7 +407,7 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
} }
} }
log.Info("Waiting for authentication callback...") fmt.Println("Waiting for authentication callback...")
// Wait up to 5 minutes // Wait up to 5 minutes
resultMap, errWait := waitForFile(waitFile, 5*time.Minute) resultMap, errWait := waitForFile(waitFile, 5*time.Minute)
if errWait != nil { if errWait != nil {
@@ -509,11 +509,11 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
return return
} }
log.Infof("Authentication successful! Token saved to %s", savedPath) fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
if bundle.APIKey != "" { if bundle.APIKey != "" {
log.Info("API key obtained and saved") fmt.Println("API key obtained and saved")
} }
log.Info("You can now use Claude services through this CLI") fmt.Println("You can now use Claude services through this CLI")
delete(oauthStatus, state) delete(oauthStatus, state)
}() }()
@@ -527,7 +527,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
// Optional project ID from query // Optional project ID from query
projectID := c.Query("project_id") projectID := c.Query("project_id")
log.Info("Initializing Google authentication...") fmt.Println("Initializing Google authentication...")
// OAuth2 configuration (mirrors internal/auth/gemini) // OAuth2 configuration (mirrors internal/auth/gemini)
conf := &oauth2.Config{ conf := &oauth2.Config{
@@ -549,7 +549,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
go func() { go func() {
// Wait for callback file written by server route // Wait for callback file written by server route
waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-gemini-%s.oauth", state)) waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-gemini-%s.oauth", state))
log.Info("Waiting for authentication callback...") fmt.Println("Waiting for authentication callback...")
deadline := time.Now().Add(5 * time.Minute) deadline := time.Now().Add(5 * time.Minute)
var authCode string var authCode string
for { for {
@@ -618,9 +618,9 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
email := gjson.GetBytes(bodyBytes, "email").String() email := gjson.GetBytes(bodyBytes, "email").String()
if email != "" { if email != "" {
log.Infof("Authenticated user email: %s", email) fmt.Printf("Authenticated user email: %s\n", email)
} else { } else {
log.Info("Failed to get user email from token") fmt.Println("Failed to get user email from token")
oauthStatus[state] = "Failed to get user email from token" oauthStatus[state] = "Failed to get user email from token"
} }
@@ -657,7 +657,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
oauthStatus[state] = "Failed to get authenticated client" oauthStatus[state] = "Failed to get authenticated client"
return return
} }
log.Info("Authentication successful.") fmt.Println("Authentication successful.")
record := &sdkAuth.TokenRecord{ record := &sdkAuth.TokenRecord{
Provider: "gemini", Provider: "gemini",
@@ -676,7 +676,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
} }
delete(oauthStatus, state) delete(oauthStatus, state)
log.Infof("You can now use Gemini CLI services through this CLI; token saved to %s", savedPath) fmt.Printf("You can now use Gemini CLI services through this CLI; token saved to %s\n", savedPath)
}() }()
oauthStatus[state] = "" oauthStatus[state] = ""
@@ -737,14 +737,14 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
return return
} }
log.Infof("Successfully saved Gemini Web token to: %s", savedPath) fmt.Printf("Successfully saved Gemini Web token to: %s\n", savedPath)
c.JSON(http.StatusOK, gin.H{"status": "ok", "file": filepath.Base(savedPath)}) c.JSON(http.StatusOK, gin.H{"status": "ok", "file": filepath.Base(savedPath)})
} }
func (h *Handler) RequestCodexToken(c *gin.Context) { func (h *Handler) RequestCodexToken(c *gin.Context) {
ctx := context.Background() ctx := context.Background()
log.Info("Initializing Codex authentication...") fmt.Println("Initializing Codex authentication...")
// Generate PKCE codes // Generate PKCE codes
pkceCodes, err := codex.GeneratePKCECodes() pkceCodes, err := codex.GeneratePKCECodes()
@@ -884,11 +884,11 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
log.Fatalf("Failed to save authentication tokens: %v", errSave) log.Fatalf("Failed to save authentication tokens: %v", errSave)
return return
} }
log.Infof("Authentication successful! Token saved to %s", savedPath) fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
if bundle.APIKey != "" { if bundle.APIKey != "" {
log.Info("API key obtained and saved") fmt.Println("API key obtained and saved")
} }
log.Info("You can now use Codex services through this CLI") fmt.Println("You can now use Codex services through this CLI")
delete(oauthStatus, state) delete(oauthStatus, state)
}() }()
@@ -899,7 +899,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
func (h *Handler) RequestQwenToken(c *gin.Context) { func (h *Handler) RequestQwenToken(c *gin.Context) {
ctx := context.Background() ctx := context.Background()
log.Info("Initializing Qwen authentication...") fmt.Println("Initializing Qwen authentication...")
state := fmt.Sprintf("gem-%d", time.Now().UnixNano()) state := fmt.Sprintf("gem-%d", time.Now().UnixNano())
// Initialize Qwen auth service // Initialize Qwen auth service
@@ -914,7 +914,7 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
authURL := deviceFlow.VerificationURIComplete authURL := deviceFlow.VerificationURIComplete
go func() { go func() {
log.Info("Waiting for authentication...") fmt.Println("Waiting for authentication...")
tokenData, errPollForToken := qwenAuth.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier) tokenData, errPollForToken := qwenAuth.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
if errPollForToken != nil { if errPollForToken != nil {
oauthStatus[state] = "Authentication failed" oauthStatus[state] = "Authentication failed"
@@ -939,8 +939,8 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
return return
} }
log.Infof("Authentication successful! Token saved to %s", savedPath) fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
log.Info("You can now use Qwen services through this CLI") fmt.Println("You can now use Qwen services through this CLI")
delete(oauthStatus, state) delete(oauthStatus, state)
}() }()

View File

@@ -452,6 +452,14 @@ func (s *Server) UpdateClients(cfg *config.Config) {
log.Debugf("request logging updated from %t to %t", s.cfg.RequestLog, cfg.RequestLog) log.Debugf("request logging updated from %t to %t", s.cfg.RequestLog, cfg.RequestLog)
} }
if s.cfg.LoggingToFile != cfg.LoggingToFile {
if err := logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
log.Errorf("failed to reconfigure log output: %v", err)
} else {
log.Debugf("logging_to_file updated from %t to %t", s.cfg.LoggingToFile, cfg.LoggingToFile)
}
}
// Update log level dynamically when debug flag changes // Update log level dynamically when debug flag changes
if s.cfg.Debug != cfg.Debug { if s.cfg.Debug != cfg.Debug {
util.SetLogLevel(cfg) util.SetLogLevel(cfg)
@@ -477,7 +485,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
} }
total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
log.Infof("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)", fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
total, total,
authFiles, authFiles,
glAPIKeyCount, glAPIKeyCount,

View File

@@ -107,7 +107,7 @@ func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiToken
// If no token is found in storage, initiate the web-based OAuth flow. // If no token is found in storage, initiate the web-based OAuth flow.
if ts.Token == nil { if ts.Token == nil {
log.Info("Could not load token from file, starting OAuth flow.") fmt.Printf("Could not load token from file, starting OAuth flow.\n")
token, err = g.getTokenFromWeb(ctx, conf, noBrowser...) token, err = g.getTokenFromWeb(ctx, conf, noBrowser...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get token from web: %w", err) return nil, fmt.Errorf("failed to get token from web: %w", err)
@@ -169,9 +169,9 @@ func (g *GeminiAuth) createTokenStorage(ctx context.Context, config *oauth2.Conf
emailResult := gjson.GetBytes(bodyBytes, "email") emailResult := gjson.GetBytes(bodyBytes, "email")
if emailResult.Exists() && emailResult.Type == gjson.String { if emailResult.Exists() && emailResult.Type == gjson.String {
log.Infof("Authenticated user email: %s", emailResult.String()) fmt.Printf("Authenticated user email: %s\n", emailResult.String())
} else { } else {
log.Info("Failed to get user email from token") fmt.Println("Failed to get user email from token")
} }
var ifToken map[string]any var ifToken map[string]any
@@ -246,19 +246,19 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")) authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
if len(noBrowser) == 1 && !noBrowser[0] { if len(noBrowser) == 1 && !noBrowser[0] {
log.Info("Opening browser for authentication...") fmt.Println("Opening browser for authentication...")
// Check if browser is available // Check if browser is available
if !browser.IsAvailable() { if !browser.IsAvailable() {
log.Warn("No browser available on this system") log.Warn("No browser available on this system")
util.PrintSSHTunnelInstructions(8085) util.PrintSSHTunnelInstructions(8085)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL) fmt.Printf("Please manually open this URL in your browser:\n\n%s\n", authURL)
} else { } else {
if err := browser.OpenURL(authURL); err != nil { if err := browser.OpenURL(authURL); err != nil {
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err) authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
log.Warn(codex.GetUserFriendlyMessage(authErr)) log.Warn(codex.GetUserFriendlyMessage(authErr))
util.PrintSSHTunnelInstructions(8085) util.PrintSSHTunnelInstructions(8085)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL) fmt.Printf("Please manually open this URL in your browser:\n\n%s\n", authURL)
// Log platform info for debugging // Log platform info for debugging
platformInfo := browser.GetPlatformInfo() platformInfo := browser.GetPlatformInfo()
@@ -269,10 +269,10 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
} }
} else { } else {
util.PrintSSHTunnelInstructions(8085) util.PrintSSHTunnelInstructions(8085)
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL) fmt.Printf("Please open this URL in your browser:\n\n%s\n", authURL)
} }
log.Info("Waiting for authentication callback...") fmt.Println("Waiting for authentication callback...")
// Wait for the authorization code or an error. // Wait for the authorization code or an error.
var authCode string var authCode string
@@ -296,6 +296,6 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
return nil, fmt.Errorf("failed to exchange token: %w", err) return nil, fmt.Errorf("failed to exchange token: %w", err)
} }
log.Info("Authentication successful.") fmt.Println("Authentication successful.")
return token, nil return token, nil
} }

View File

@@ -260,7 +260,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
switch errorType { switch errorType {
case "authorization_pending": case "authorization_pending":
// User has not yet approved the authorization request. Continue polling. // User has not yet approved the authorization request. Continue polling.
log.Infof("Polling attempt %d/%d...\n", attempt+1, maxAttempts) fmt.Printf("Polling attempt %d/%d...\n\n", attempt+1, maxAttempts)
time.Sleep(pollInterval) time.Sleep(pollInterval)
continue continue
case "slow_down": case "slow_down":
@@ -269,7 +269,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
if pollInterval > 10*time.Second { if pollInterval > 10*time.Second {
pollInterval = 10 * time.Second pollInterval = 10 * time.Second
} }
log.Infof("Server requested to slow down, increasing poll interval to %v\n", pollInterval) fmt.Printf("Server requested to slow down, increasing poll interval to %v\n\n", pollInterval)
time.Sleep(pollInterval) time.Sleep(pollInterval)
continue continue
case "expired_token": case "expired_token":

View File

@@ -21,7 +21,7 @@ import (
// Returns: // Returns:
// - An error if the URL cannot be opened, otherwise nil. // - An error if the URL cannot be opened, otherwise nil.
func OpenURL(url string) error { func OpenURL(url string) error {
log.Infof("Attempting to open URL in browser: %s", url) fmt.Printf("Attempting to open URL in browser: %s\n", url)
// Try using the open-golang library first // Try using the open-golang library first
err := open.Run(url) err := open.Run(url)

View File

@@ -62,8 +62,8 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
} }
if savedPath != "" { if savedPath != "" {
log.Infof("Authentication saved to %s", savedPath) fmt.Printf("Authentication saved to %s\n", savedPath)
} }
log.Info("Gemini authentication successful!") fmt.Println("Gemini authentication successful!")
} }

View File

@@ -23,6 +23,9 @@ type Config struct {
// Debug enables or disables debug-level logging and other debug features. // Debug enables or disables debug-level logging and other debug features.
Debug bool `yaml:"debug" json:"debug"` Debug bool `yaml:"debug" json:"debug"`
// LoggingToFile controls whether application logs are written to rotating files or stdout.
LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"`
// ProxyURL is the URL of an optional proxy server to use for outbound requests. // ProxyURL is the URL of an optional proxy server to use for outbound requests.
ProxyURL string `yaml:"proxy-url" json:"proxy-url"` ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
@@ -202,6 +205,7 @@ func LoadConfig(configFile string) (*Config, error) {
// Unmarshal the YAML data into the Config struct. // Unmarshal the YAML data into the Config struct.
var config Config var config Config
// Set defaults before unmarshal so that absent keys keep defaults. // Set defaults before unmarshal so that absent keys keep defaults.
config.LoggingToFile = true
config.GeminiWeb.Context = true config.GeminiWeb.Context = true
if err = yaml.Unmarshal(data, &config); err != nil { if err = yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err) return nil, fmt.Errorf("failed to parse config file: %w", err)

View File

@@ -0,0 +1,117 @@
package logging
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
setupOnce sync.Once
writerMu sync.Mutex
logWriter *lumberjack.Logger
ginInfoWriter *io.PipeWriter
ginErrorWriter *io.PipeWriter
)
// LogFormatter defines a custom log format for logrus.
// This formatter adds timestamp, level, and source location to each log entry.
type LogFormatter struct{}
// Format renders a single log entry with custom formatting.
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
var buffer *bytes.Buffer
if entry.Buffer != nil {
buffer = entry.Buffer
} else {
buffer = &bytes.Buffer{}
}
timestamp := entry.Time.Format("2006-01-02 15:04:05")
message := strings.TrimRight(entry.Message, "\r\n")
formatted := fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, message)
buffer.WriteString(formatted)
return buffer.Bytes(), nil
}
// SetupBaseLogger configures the shared logrus instance and Gin writers.
// It is safe to call multiple times; initialization happens only once.
func SetupBaseLogger() {
setupOnce.Do(func() {
log.SetOutput(os.Stdout)
log.SetReportCaller(true)
log.SetFormatter(&LogFormatter{})
ginInfoWriter = log.StandardLogger().Writer()
gin.DefaultWriter = ginInfoWriter
ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel)
gin.DefaultErrorWriter = ginErrorWriter
gin.DebugPrintFunc = func(format string, values ...interface{}) {
format = strings.TrimRight(format, "\r\n")
log.StandardLogger().Infof(format, values...)
}
log.RegisterExitHandler(closeLogOutputs)
})
}
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
func ConfigureLogOutput(loggingToFile bool) error {
SetupBaseLogger()
writerMu.Lock()
defer writerMu.Unlock()
if loggingToFile {
const logDir = "logs"
if err := os.MkdirAll(logDir, 0o755); err != nil {
return fmt.Errorf("logging: failed to create log directory: %w", err)
}
if logWriter != nil {
_ = logWriter.Close()
}
logWriter = &lumberjack.Logger{
Filename: filepath.Join(logDir, "main.log"),
MaxSize: 10,
MaxBackups: 0,
MaxAge: 0,
Compress: false,
}
log.SetOutput(logWriter)
return nil
}
if logWriter != nil {
_ = logWriter.Close()
logWriter = nil
}
log.SetOutput(os.Stdout)
return nil
}
func closeLogOutputs() {
writerMu.Lock()
defer writerMu.Unlock()
if logWriter != nil {
_ = logWriter.Close()
logWriter = nil
}
if ginInfoWriter != nil {
_ = ginInfoWriter.Close()
ginInfoWriter = nil
}
if ginErrorWriter != nil {
_ = ginErrorWriter.Close()
ginErrorWriter = nil
}
}

View File

@@ -1,6 +1,7 @@
package misc package misc
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -15,7 +16,7 @@ func LogSavingCredentials(path string) {
return return
} }
// Use filepath.Clean so logs remain stable even if callers pass redundant separators. // Use filepath.Clean so logs remain stable even if callers pass redundant separators.
log.Infof("Saving credentials to %s", filepath.Clean(path)) fmt.Printf("Saving credentials to %s\n", filepath.Clean(path))
} }
// LogCredentialSeparator adds a visual separator to group auth/key processing logs. // LogCredentialSeparator adds a visual separator to group auth/key processing logs.

View File

@@ -147,7 +147,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
if len(matches) >= 2 { if len(matches) >= 2 {
token := matches[1] token := matches[1]
if verbose { if verbose {
log.Infof("Gemini access token acquired.") fmt.Println("Gemini access token acquired.")
} }
return token, mergedCookies, nil return token, mergedCookies, nil
} }
@@ -280,7 +280,7 @@ func (c *GeminiClient) Init(timeoutSec float64, verbose bool) error {
c.Timeout = time.Duration(timeoutSec * float64(time.Second)) c.Timeout = time.Duration(timeoutSec * float64(time.Second))
if verbose { if verbose {
log.Infof("Gemini client initialized successfully.") fmt.Println("Gemini client initialized successfully.")
} }
return nil return nil
} }

View File

@@ -136,7 +136,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
return "", err return "", err
} }
if verbose { if verbose {
log.Infof("Image saved as %s", dest) fmt.Printf("Image saved as %s\n", dest)
} }
abspath, _ := filepath.Abs(dest) abspath, _ := filepath.Abs(dest)
return abspath, nil return abspath, nil

View File

@@ -120,7 +120,7 @@ func GetIPAddress() string {
func PrintSSHTunnelInstructions(port int) { func PrintSSHTunnelInstructions(port int) {
ipAddress := GetIPAddress() ipAddress := GetIPAddress()
border := "================================================================================" border := "================================================================================"
log.Infof("To authenticate from a remote machine, an SSH tunnel may be required.") fmt.Println("To authenticate from a remote machine, an SSH tunnel may be required.")
fmt.Println(border) fmt.Println(border)
fmt.Println(" Run one of the following commands on your local machine (NOT the server):") fmt.Println(" Run one of the following commands on your local machine (NOT the server):")
fmt.Println() fmt.Println()

View File

@@ -380,7 +380,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
log.Debugf("config file content unchanged (hash match), skipping reload") log.Debugf("config file content unchanged (hash match), skipping reload")
return return
} }
log.Infof("config file changed, reloading: %s", w.configPath) fmt.Printf("config file changed, reloading: %s\n", w.configPath)
if w.reloadConfig() { if w.reloadConfig() {
w.clientsMutex.Lock() w.clientsMutex.Lock()
w.lastConfigHash = newHash w.lastConfigHash = newHash
@@ -390,7 +390,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
} }
// Handle auth directory changes incrementally (.json only) // Handle auth directory changes incrementally (.json only)
log.Infof("auth file changed (%s): %s, processing incrementally", event.Op.String(), filepath.Base(event.Name)) fmt.Printf("auth file changed (%s): %s, processing incrementally\n", event.Op.String(), filepath.Base(event.Name))
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write { if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
w.addOrUpdateClient(event.Name) w.addOrUpdateClient(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove { } else if event.Op&fsnotify.Remove == fsnotify.Remove {

View File

@@ -80,22 +80,22 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
state = returnedState state = returnedState
if !opts.NoBrowser { if !opts.NoBrowser {
log.Info("Opening browser for Claude authentication") fmt.Println("Opening browser for Claude authentication")
if !browser.IsAvailable() { if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually") log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} else if err = browser.OpenURL(authURL); err != nil { } else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err) log.Warnf("Failed to open browser automatically: %v", err)
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
} else { } else {
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
log.Info("Waiting for Claude authentication callback...") fmt.Println("Waiting for Claude authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute) result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil { if err != nil {
@@ -131,9 +131,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
log.Info("Claude authentication successful") fmt.Println("Claude authentication successful")
if authBundle.APIKey != "" { if authBundle.APIKey != "" {
log.Info("Claude API key obtained and stored") fmt.Println("Claude API key obtained and stored")
} }
return &TokenRecord{ return &TokenRecord{

View File

@@ -79,22 +79,22 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
} }
if !opts.NoBrowser { if !opts.NoBrowser {
log.Info("Opening browser for Codex authentication") fmt.Println("Opening browser for Codex authentication")
if !browser.IsAvailable() { if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually") log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} else if err = browser.OpenURL(authURL); err != nil { } else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err) log.Warnf("Failed to open browser automatically: %v", err)
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
} else { } else {
util.PrintSSHTunnelInstructions(a.CallbackPort) util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
log.Info("Waiting for Codex authentication callback...") fmt.Println("Waiting for Codex authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute) result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil { if err != nil {
@@ -130,9 +130,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
log.Info("Codex authentication successful") fmt.Println("Codex authentication successful")
if authBundle.APIKey != "" { if authBundle.APIKey != "" {
log.Info("Codex API key obtained and stored") fmt.Println("Codex API key obtained and stored")
} }
return &TokenRecord{ return &TokenRecord{

View File

@@ -8,7 +8,6 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
// legacy client removed // legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
log "github.com/sirupsen/logrus"
) )
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts. // GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
@@ -57,7 +56,7 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
"project_id": ts.ProjectID, "project_id": ts.ProjectID,
} }
log.Info("Gemini authentication successful") fmt.Println("Gemini authentication successful")
return &TokenRecord{ return &TokenRecord{
Provider: a.Provider(), Provider: a.Provider(),

View File

@@ -51,19 +51,19 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
authURL := deviceFlow.VerificationURIComplete authURL := deviceFlow.VerificationURIComplete
if !opts.NoBrowser { if !opts.NoBrowser {
log.Info("Opening browser for Qwen authentication") fmt.Println("Opening browser for Qwen authentication")
if !browser.IsAvailable() { if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually") log.Warn("No browser available; please open the URL manually")
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} else if err = browser.OpenURL(authURL); err != nil { } else if err = browser.OpenURL(authURL); err != nil {
log.Warnf("Failed to open browser automatically: %v", err) log.Warnf("Failed to open browser automatically: %v", err)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
} else { } else {
log.Infof("Visit the following URL to continue authentication:\n%s", authURL) fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
} }
log.Info("Waiting for Qwen authentication...") fmt.Println("Waiting for Qwen authentication...")
tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier) tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
if err != nil { if err != nil {
@@ -101,7 +101,7 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
"email": tokenStorage.Email, "email": tokenStorage.Email,
} }
log.Info("Qwen authentication successful") fmt.Println("Qwen authentication successful")
return &TokenRecord{ return &TokenRecord{
Provider: a.Provider(), Provider: a.Provider(),

View File

@@ -331,7 +331,7 @@ func (s *Service) Run(ctx context.Context) error {
}() }()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
log.Info("API server started successfully") fmt.Println("API server started successfully")
if s.hooks.OnAfterStart != nil { if s.hooks.OnAfterStart != nil {
s.hooks.OnAfterStart(s) s.hooks.OnAfterStart(s)