mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
201 lines
5.1 KiB
Go
201 lines
5.1 KiB
Go
package logging
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
|
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, request ID, and source location to each log entry.
|
|
// Format: [2025-12-23 20:14:04] [debug] [manager.go:524] | a1b2c3d4 | Use API key sk-9...0RHO for model gpt-5.2
|
|
type LogFormatter struct{}
|
|
|
|
// logFieldOrder defines the display order for common log fields.
|
|
var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_mode", "original_value", "min", "max", "clamped_to", "error"}
|
|
|
|
// 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")
|
|
|
|
reqID := "--------"
|
|
if id, ok := entry.Data["request_id"].(string); ok && id != "" {
|
|
reqID = id
|
|
}
|
|
|
|
level := entry.Level.String()
|
|
if level == "warning" {
|
|
level = "warn"
|
|
}
|
|
levelStr := fmt.Sprintf("%-5s", level)
|
|
|
|
// Build fields string (only print fields in logFieldOrder)
|
|
var fieldsStr string
|
|
if len(entry.Data) > 0 {
|
|
var fields []string
|
|
for _, k := range logFieldOrder {
|
|
if v, ok := entry.Data[k]; ok {
|
|
fields = append(fields, fmt.Sprintf("%s=%v", k, v))
|
|
}
|
|
}
|
|
if len(fields) > 0 {
|
|
fieldsStr = " " + strings.Join(fields, " ")
|
|
}
|
|
}
|
|
|
|
var formatted string
|
|
if entry.Caller != nil {
|
|
formatted = fmt.Sprintf("[%s] [%s] [%s] [%s:%d] %s%s\n", timestamp, reqID, levelStr, filepath.Base(entry.Caller.File), entry.Caller.Line, message, fieldsStr)
|
|
} else {
|
|
formatted = fmt.Sprintf("[%s] [%s] [%s] %s%s\n", timestamp, reqID, levelStr, message, fieldsStr)
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
|
|
// isDirWritable checks if the specified directory exists and is writable by attempting to create and remove a test file.
|
|
func isDirWritable(dir string) bool {
|
|
info, err := os.Stat(dir)
|
|
if err != nil || !info.IsDir() {
|
|
return false
|
|
}
|
|
|
|
testFile := filepath.Join(dir, ".perm_test")
|
|
f, err := os.Create(testFile)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
defer func() {
|
|
_ = f.Close()
|
|
_ = os.Remove(testFile)
|
|
}()
|
|
return true
|
|
}
|
|
|
|
// ResolveLogDirectory determines the directory used for application logs.
|
|
func ResolveLogDirectory(cfg *config.Config) string {
|
|
logDir := "logs"
|
|
if base := util.WritablePath(); base != "" {
|
|
return filepath.Join(base, "logs")
|
|
}
|
|
if cfg == nil {
|
|
return logDir
|
|
}
|
|
if !isDirWritable(logDir) {
|
|
authDir := strings.TrimSpace(cfg.AuthDir)
|
|
if authDir != "" {
|
|
logDir = filepath.Join(authDir, "logs")
|
|
}
|
|
}
|
|
return logDir
|
|
}
|
|
|
|
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
|
|
// When logsMaxTotalSizeMB > 0, a background cleaner removes the oldest log files in the logs directory
|
|
// until the total size is within the limit.
|
|
func ConfigureLogOutput(cfg *config.Config) error {
|
|
SetupBaseLogger()
|
|
|
|
writerMu.Lock()
|
|
defer writerMu.Unlock()
|
|
|
|
logDir := ResolveLogDirectory(cfg)
|
|
|
|
protectedPath := ""
|
|
if cfg.LoggingToFile {
|
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
|
return fmt.Errorf("logging: failed to create log directory: %w", err)
|
|
}
|
|
if logWriter != nil {
|
|
_ = logWriter.Close()
|
|
}
|
|
protectedPath = filepath.Join(logDir, "main.log")
|
|
logWriter = &lumberjack.Logger{
|
|
Filename: protectedPath,
|
|
MaxSize: 10,
|
|
MaxBackups: 0,
|
|
MaxAge: 0,
|
|
Compress: false,
|
|
}
|
|
log.SetOutput(logWriter)
|
|
} else {
|
|
if logWriter != nil {
|
|
_ = logWriter.Close()
|
|
logWriter = nil
|
|
}
|
|
log.SetOutput(os.Stdout)
|
|
}
|
|
|
|
configureLogDirCleanerLocked(logDir, cfg.LogsMaxTotalSizeMB, protectedPath)
|
|
return nil
|
|
}
|
|
|
|
func closeLogOutputs() {
|
|
writerMu.Lock()
|
|
defer writerMu.Unlock()
|
|
|
|
stopLogDirCleanerLocked()
|
|
|
|
if logWriter != nil {
|
|
_ = logWriter.Close()
|
|
logWriter = nil
|
|
}
|
|
if ginInfoWriter != nil {
|
|
_ = ginInfoWriter.Close()
|
|
ginInfoWriter = nil
|
|
}
|
|
if ginErrorWriter != nil {
|
|
_ = ginErrorWriter.Close()
|
|
ginErrorWriter = nil
|
|
}
|
|
}
|