mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
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:
@@ -4,106 +4,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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/logging"
|
||||
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
Commit = "none"
|
||||
BuildDate = "unknown"
|
||||
logWriter *lumberjack.Logger
|
||||
ginInfoWriter *io.PipeWriter
|
||||
ginErrorWriter *io.PipeWriter
|
||||
Version = "dev"
|
||||
Commit = "none"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
// LogFormatter defines a custom log format for logrus.
|
||||
// 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.
|
||||
// init initializes the shared logger setup.
|
||||
func init() {
|
||||
logDir := "logs"
|
||||
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()
|
||||
}
|
||||
})
|
||||
logging.SetupBaseLogger()
|
||||
}
|
||||
|
||||
// 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).
|
||||
func main() {
|
||||
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.
|
||||
var login bool
|
||||
@@ -189,6 +112,12 @@ func main() {
|
||||
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.
|
||||
util.SetLogLevel(cfg)
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ auth-dir: "~/.cli-proxy-api"
|
||||
# Enable debug logging
|
||||
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: ""
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco
|
||||
func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
||||
ctx := context.Background()
|
||||
|
||||
log.Info("Initializing Claude authentication...")
|
||||
fmt.Println("Initializing Claude authentication...")
|
||||
|
||||
// Generate PKCE codes
|
||||
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
|
||||
resultMap, errWait := waitForFile(waitFile, 5*time.Minute)
|
||||
if errWait != nil {
|
||||
@@ -509,11 +509,11 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Authentication successful! Token saved to %s", savedPath)
|
||||
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||
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)
|
||||
}()
|
||||
|
||||
@@ -527,7 +527,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
||||
// Optional project ID from query
|
||||
projectID := c.Query("project_id")
|
||||
|
||||
log.Info("Initializing Google authentication...")
|
||||
fmt.Println("Initializing Google authentication...")
|
||||
|
||||
// OAuth2 configuration (mirrors internal/auth/gemini)
|
||||
conf := &oauth2.Config{
|
||||
@@ -549,7 +549,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
||||
go func() {
|
||||
// Wait for callback file written by server route
|
||||
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)
|
||||
var authCode string
|
||||
for {
|
||||
@@ -618,9 +618,9 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
||||
|
||||
email := gjson.GetBytes(bodyBytes, "email").String()
|
||||
if email != "" {
|
||||
log.Infof("Authenticated user email: %s", email)
|
||||
fmt.Printf("Authenticated user email: %s\n", email)
|
||||
} 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"
|
||||
}
|
||||
|
||||
@@ -657,7 +657,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
||||
oauthStatus[state] = "Failed to get authenticated client"
|
||||
return
|
||||
}
|
||||
log.Info("Authentication successful.")
|
||||
fmt.Println("Authentication successful.")
|
||||
|
||||
record := &sdkAuth.TokenRecord{
|
||||
Provider: "gemini",
|
||||
@@ -676,7 +676,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
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] = ""
|
||||
@@ -737,14 +737,14 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
|
||||
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)})
|
||||
}
|
||||
|
||||
func (h *Handler) RequestCodexToken(c *gin.Context) {
|
||||
ctx := context.Background()
|
||||
|
||||
log.Info("Initializing Codex authentication...")
|
||||
fmt.Println("Initializing Codex authentication...")
|
||||
|
||||
// Generate PKCE codes
|
||||
pkceCodes, err := codex.GeneratePKCECodes()
|
||||
@@ -884,11 +884,11 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
||||
log.Fatalf("Failed to save authentication tokens: %v", errSave)
|
||||
return
|
||||
}
|
||||
log.Infof("Authentication successful! Token saved to %s", savedPath)
|
||||
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||
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)
|
||||
}()
|
||||
|
||||
@@ -899,7 +899,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
||||
func (h *Handler) RequestQwenToken(c *gin.Context) {
|
||||
ctx := context.Background()
|
||||
|
||||
log.Info("Initializing Qwen authentication...")
|
||||
fmt.Println("Initializing Qwen authentication...")
|
||||
|
||||
state := fmt.Sprintf("gem-%d", time.Now().UnixNano())
|
||||
// Initialize Qwen auth service
|
||||
@@ -914,7 +914,7 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
|
||||
authURL := deviceFlow.VerificationURIComplete
|
||||
|
||||
go func() {
|
||||
log.Info("Waiting for authentication...")
|
||||
fmt.Println("Waiting for authentication...")
|
||||
tokenData, errPollForToken := qwenAuth.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
|
||||
if errPollForToken != nil {
|
||||
oauthStatus[state] = "Authentication failed"
|
||||
@@ -939,8 +939,8 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Authentication successful! Token saved to %s", savedPath)
|
||||
log.Info("You can now use Qwen services through this CLI")
|
||||
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||
fmt.Println("You can now use Qwen services through this CLI")
|
||||
delete(oauthStatus, state)
|
||||
}()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
if s.cfg.Debug != cfg.Debug {
|
||||
util.SetLogLevel(cfg)
|
||||
@@ -477,7 +485,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
||||
}
|
||||
|
||||
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,
|
||||
authFiles,
|
||||
glAPIKeyCount,
|
||||
|
||||
@@ -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 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...)
|
||||
if err != nil {
|
||||
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")
|
||||
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 {
|
||||
log.Info("Failed to get user email from token")
|
||||
fmt.Println("Failed to get user email from token")
|
||||
}
|
||||
|
||||
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"))
|
||||
|
||||
if len(noBrowser) == 1 && !noBrowser[0] {
|
||||
log.Info("Opening browser for authentication...")
|
||||
fmt.Println("Opening browser for authentication...")
|
||||
|
||||
// Check if browser is available
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available on this system")
|
||||
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 {
|
||||
if err := browser.OpenURL(authURL); err != nil {
|
||||
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
||||
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
||||
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
|
||||
platformInfo := browser.GetPlatformInfo()
|
||||
@@ -269,10 +269,10 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
log.Info("Authentication successful.")
|
||||
fmt.Println("Authentication successful.")
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
|
||||
switch errorType {
|
||||
case "authorization_pending":
|
||||
// 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)
|
||||
continue
|
||||
case "slow_down":
|
||||
@@ -269,7 +269,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
|
||||
if 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)
|
||||
continue
|
||||
case "expired_token":
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// Returns:
|
||||
// - An error if the URL cannot be opened, otherwise nil.
|
||||
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
|
||||
err := open.Run(url)
|
||||
|
||||
@@ -62,8 +62,8 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
|
||||
}
|
||||
|
||||
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!")
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ type Config struct {
|
||||
// Debug enables or disables debug-level logging and other debug features.
|
||||
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 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.
|
||||
var config Config
|
||||
// Set defaults before unmarshal so that absent keys keep defaults.
|
||||
config.LoggingToFile = true
|
||||
config.GeminiWeb.Context = true
|
||||
if err = yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
|
||||
117
internal/logging/global_logger.go
Normal file
117
internal/logging/global_logger.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package misc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -15,7 +16,7 @@ func LogSavingCredentials(path string) {
|
||||
return
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -147,7 +147,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
|
||||
if len(matches) >= 2 {
|
||||
token := matches[1]
|
||||
if verbose {
|
||||
log.Infof("Gemini access token acquired.")
|
||||
fmt.Println("Gemini access token acquired.")
|
||||
}
|
||||
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))
|
||||
if verbose {
|
||||
log.Infof("Gemini client initialized successfully.")
|
||||
fmt.Println("Gemini client initialized successfully.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
|
||||
return "", err
|
||||
}
|
||||
if verbose {
|
||||
log.Infof("Image saved as %s", dest)
|
||||
fmt.Printf("Image saved as %s\n", dest)
|
||||
}
|
||||
abspath, _ := filepath.Abs(dest)
|
||||
return abspath, nil
|
||||
|
||||
@@ -120,7 +120,7 @@ func GetIPAddress() string {
|
||||
func PrintSSHTunnelInstructions(port int) {
|
||||
ipAddress := GetIPAddress()
|
||||
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(" Run one of the following commands on your local machine (NOT the server):")
|
||||
fmt.Println()
|
||||
|
||||
@@ -380,7 +380,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
||||
log.Debugf("config file content unchanged (hash match), skipping reload")
|
||||
return
|
||||
}
|
||||
log.Infof("config file changed, reloading: %s", w.configPath)
|
||||
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
|
||||
if w.reloadConfig() {
|
||||
w.clientsMutex.Lock()
|
||||
w.lastConfigHash = newHash
|
||||
@@ -390,7 +390,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
w.addOrUpdateClient(event.Name)
|
||||
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
|
||||
@@ -80,22 +80,22 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
||||
state = returnedState
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Claude authentication")
|
||||
fmt.Println("Opening browser for Claude authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
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 {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -131,9 +131,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Claude authentication successful")
|
||||
fmt.Println("Claude authentication successful")
|
||||
if authBundle.APIKey != "" {
|
||||
log.Info("Claude API key obtained and stored")
|
||||
fmt.Println("Claude API key obtained and stored")
|
||||
}
|
||||
|
||||
return &TokenRecord{
|
||||
|
||||
@@ -79,22 +79,22 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
||||
}
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Codex authentication")
|
||||
fmt.Println("Opening browser for Codex authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
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 {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -130,9 +130,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Codex authentication successful")
|
||||
fmt.Println("Codex authentication successful")
|
||||
if authBundle.APIKey != "" {
|
||||
log.Info("Codex API key obtained and stored")
|
||||
fmt.Println("Codex API key obtained and stored")
|
||||
}
|
||||
|
||||
return &TokenRecord{
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||
// legacy client removed
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
log.Info("Gemini authentication successful")
|
||||
fmt.Println("Gemini authentication successful")
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
|
||||
@@ -51,19 +51,19 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
||||
authURL := deviceFlow.VerificationURIComplete
|
||||
|
||||
if !opts.NoBrowser {
|
||||
log.Info("Opening browser for Qwen authentication")
|
||||
fmt.Println("Opening browser for Qwen authentication")
|
||||
if !browser.IsAvailable() {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -101,7 +101,7 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
||||
"email": tokenStorage.Email,
|
||||
}
|
||||
|
||||
log.Info("Qwen authentication successful")
|
||||
fmt.Println("Qwen authentication successful")
|
||||
|
||||
return &TokenRecord{
|
||||
Provider: a.Provider(),
|
||||
|
||||
@@ -331,7 +331,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
log.Info("API server started successfully")
|
||||
fmt.Println("API server started successfully")
|
||||
|
||||
if s.hooks.OnAfterStart != nil {
|
||||
s.hooks.OnAfterStart(s)
|
||||
|
||||
Reference in New Issue
Block a user