diff --git a/cmd/server/main.go b/cmd/server/main.go index eb82122b..6d6c84cd 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -7,21 +7,27 @@ 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/translator" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" log "github.com/sirupsen/logrus" + "gopkg.in/natefinch/lumberjack.v2" ) var ( - Version = "dev" - Commit = "none" - BuildDate = "unknown" + Version = "dev" + Commit = "none" + BuildDate = "unknown" + logWriter *lumberjack.Logger + ginInfoWriter *io.PipeWriter + ginErrorWriter *io.PipeWriter ) // LogFormatter defines a custom log format for logrus. @@ -53,18 +59,51 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) { // It sets up the custom log formatter, enables caller reporting, // and configures the log output destination. func init() { - // Set logger output to standard output. - log.SetOutput(os.Stdout) + 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{}) { + 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. // It parses command-line flags, loads configuration, and starts the appropriate // 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. diff --git a/go.mod b/go.mod index dcb078ca..63b9d137 100644 --- a/go.mod +++ b/go.mod @@ -44,4 +44,5 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 8349259d..a4d6fbcd 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/server.go b/internal/api/server.go index 7be359e8..3067ecad 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -128,8 +128,8 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk } // Add middleware - engine.Use(gin.Logger()) - engine.Use(gin.Recovery()) + engine.Use(logging.GinLogrusLogger()) + engine.Use(logging.GinLogrusRecovery()) for _, mw := range optionState.extraMiddleware { engine.Use(mw) } diff --git a/internal/logging/gin_logger.go b/internal/logging/gin_logger.go new file mode 100644 index 00000000..14685b6c --- /dev/null +++ b/internal/logging/gin_logger.go @@ -0,0 +1,65 @@ +package logging + +import ( + "fmt" + "net/http" + "runtime/debug" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +// GinLogrusLogger writes Gin-style access logs through logrus. +func GinLogrusLogger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + c.Next() + + if raw != "" { + path = path + "?" + raw + } + + latency := time.Since(start) + if latency > time.Minute { + latency = latency.Truncate(time.Second) + } else { + latency = latency.Truncate(time.Millisecond) + } + + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + method := c.Request.Method + errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() + timestamp := time.Now().Format("2006/01/02 - 15:04:05") + logLine := fmt.Sprintf("[GIN] %s | %3d | %13v | %15s | %-7s \"%s\"", timestamp, statusCode, latency, clientIP, method, path) + if errorMessage != "" { + logLine = logLine + " | " + errorMessage + } + + switch { + case statusCode >= http.StatusInternalServerError: + log.Error(logLine) + case statusCode >= http.StatusBadRequest: + log.Warn(logLine) + default: + log.Info(logLine) + } + } +} + +// GinLogrusRecovery returns a Gin middleware that recovers from panics and logs them via logrus. +func GinLogrusRecovery() gin.HandlerFunc { + return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + log.WithFields(log.Fields{ + "panic": recovered, + "stack": string(debug.Stack()), + "path": c.Request.URL.Path, + }).Error("recovered from panic") + + c.AbortWithStatus(http.StatusInternalServerError) + }) +}