diff --git a/pkg/channels/qq/qq.go b/pkg/channels/qq/qq.go index 112964143..b81839183 100644 --- a/pkg/channels/qq/qq.go +++ b/pkg/channels/qq/qq.go @@ -50,6 +50,7 @@ func (c *QQChannel) Start(ctx context.Context) error { return fmt.Errorf("QQ app_id and app_secret not configured") } + botgo.SetLogger(logger.NewLogger("botgo")) logger.InfoC("qq", "Starting QQ bot (WebSocket mode)") // create token source diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 0a36247a6..23c05272f 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -77,6 +77,7 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" { opts = append(opts, telego.WithAPIServer(baseURL)) } + opts = append(opts, telego.WithLogger(logger.NewLogger("telego"))) bot, err := telego.NewBot(telegramCfg.Token, opts...) if err != nil { diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 56dc87a53..ac445c8f0 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,24 +1,24 @@ package logger import ( - "encoding/json" "fmt" - "log" "os" + "path/filepath" "runtime" "strings" "sync" - "time" + + "github.com/rs/zerolog" ) -type LogLevel int +type LogLevel = zerolog.Level const ( - DEBUG LogLevel = iota - INFO - WARN - ERROR - FATAL + DEBUG = zerolog.DebugLevel + INFO = zerolog.InfoLevel + WARN = zerolog.WarnLevel + ERROR = zerolog.ErrorLevel + FATAL = zerolog.FatalLevel ) var ( @@ -31,27 +31,46 @@ var ( } currentLevel = INFO - logger *Logger + logger zerolog.Logger + fileLogger zerolog.Logger once sync.Once mu sync.RWMutex ) -type Logger struct { - file *os.File -} - -type LogEntry struct { - Level string `json:"level"` - Timestamp string `json:"timestamp"` - Component string `json:"component,omitempty"` - Message string `json:"message"` - Fields map[string]any `json:"fields,omitempty"` - Caller string `json:"caller,omitempty"` -} - func init() { once.Do(func() { - logger = &Logger{} + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + consoleWriter := zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "15:04:05", + NoColor: false, + } + + consoleWriter.FormatLevel = func(i interface{}) string { + level, ok := i.(string) + if !ok { + return fmt.Sprintf("| %-5s |", i) + } + + switch strings.ToUpper(level) { + case "DEBUG": + return "| \x1b[36mDBG\x1b[0m |" + case "INFO": + return "| \x1b[32mINF\x1b[0m |" + case "WARN": + return "| \x1b[33mWRN\x1b[0m |" + case "ERROR": + return "| \x1b[31mERR\x1b[0m |" + case "FATAL": + return "| \x1b[35mFTL\x1b[0m |" + default: + return fmt.Sprintf("| %-5s |", level) + } + } + + logger = zerolog.New(consoleWriter).With().Timestamp().Logger() + fileLogger = zerolog.Logger{} }) } @@ -59,6 +78,7 @@ func SetLevel(level LogLevel) { mu.Lock() defer mu.Unlock() currentLevel = level + zerolog.SetGlobalLevel(level) } func GetLevel() LogLevel { @@ -71,29 +91,53 @@ func EnableFileLogging(filePath string) error { mu.Lock() defer mu.Unlock() + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + return fmt.Errorf("failed to create log directory: %w", err) + } + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return fmt.Errorf("failed to open log file: %w", err) } - if logger.file != nil { - logger.file.Close() - } - - logger.file = file - log.Println("File logging enabled:", filePath) + fileLogger = zerolog.New(file).With().Timestamp().Caller().Logger() return nil } func DisableFileLogging() { mu.Lock() defer mu.Unlock() + fileLogger = zerolog.Logger{} +} - if logger.file != nil { - logger.file.Close() - logger.file = nil - log.Println("File logging disabled") +func getCallerInfo() (string, int, string) { + for i := 2; i < 15; i++ { + pc, file, line, ok := runtime.Caller(i) + if !ok { + continue + } + + fn := runtime.FuncForPC(pc) + if fn == nil { + continue + } + + // bypass common loggers + if strings.HasSuffix(file, "/logger.go") || + strings.HasSuffix(file, "/log.go") { + continue + } + + funcName := fn.Name() + if strings.HasPrefix(funcName, "runtime.") { + fmt.Println("===", funcName) + continue + } + + return filepath.Base(file), line, filepath.Base(funcName) } + + return "???", 0, "???" } func logMessage(level LogLevel, component string, message string, fields map[string]any) { @@ -101,65 +145,70 @@ func logMessage(level LogLevel, component string, message string, fields map[str return } - entry := LogEntry{ - Level: logLevelNames[level], - Timestamp: time.Now().UTC().Format(time.RFC3339), - Component: component, - Message: message, - Fields: fields, + callerFile, callerLine, callerFunc := getCallerInfo() + + var event *zerolog.Event + switch level { + case zerolog.DebugLevel: + event = logger.Debug() + case zerolog.InfoLevel: + event = logger.Info() + case zerolog.WarnLevel: + event = logger.Warn() + case zerolog.ErrorLevel: + event = logger.Error() + case zerolog.FatalLevel: + event = logger.Error() + default: + event = logger.Info() } - if pc, file, line, ok := runtime.Caller(2); ok { - fn := runtime.FuncForPC(pc) - if fn != nil { - entry.Caller = fmt.Sprintf("%s:%d (%s)", file, line, fn.Name()) - } - } - - if logger.file != nil { - jsonData, err := json.Marshal(entry) - if err == nil { - logger.file.Write(append(jsonData, '\n')) - } - } - - var fieldStr string - if len(fields) > 0 { - fieldStr = " " + formatFields(fields) + // Build combined field with component and caller + if component != "" { + //event.Str("component", component) + event.Str("caller", fmt.Sprintf("%-6s | %s:%d (%s)", component, callerFile, callerLine, callerFunc)) } else { - fieldStr = "" + event.Str("caller", fmt.Sprintf(" | %s:%d (%s)", callerFile, callerLine, callerFunc)) } - logLine := fmt.Sprintf("[%s] [%s]%s %s%s", - entry.Timestamp, - logLevelNames[level], - formatComponent(component), - message, - fieldStr, - ) + for k, v := range fields { + event.Interface(k, v) + } - log.Println(logLine) + event.Msg(message) + + // Also log to file if enabled + if fileLogger.GetLevel() != zerolog.NoLevel { + var fileEvent *zerolog.Event + switch level { + case zerolog.DebugLevel: + fileEvent = fileLogger.Debug() + case zerolog.InfoLevel: + fileEvent = fileLogger.Info() + case zerolog.WarnLevel: + fileEvent = fileLogger.Warn() + case zerolog.ErrorLevel: + fileEvent = fileLogger.Error() + case zerolog.FatalLevel: + fileEvent = fileLogger.Error() + default: + fileEvent = fileLogger.Info() + } + + if component != "" { + fileEvent.Str("component", component) + } + for k, v := range fields { + fileEvent.Interface(k, v) + } + fileEvent.Msg(message) + } if level == FATAL { os.Exit(1) } } -func formatComponent(component string) string { - if component == "" { - return "" - } - return fmt.Sprintf(" %s:", component) -} - -func formatFields(fields map[string]any) string { - parts := make([]string, 0, len(fields)) - for k, v := range fields { - parts = append(parts, fmt.Sprintf("%s=%v", k, v)) - } - return fmt.Sprintf("{%s}", strings.Join(parts, ", ")) -} - func Debug(message string) { logMessage(DEBUG, "", message, nil) } @@ -232,6 +281,10 @@ func FatalC(component string, message string) { logMessage(FATAL, component, message, nil) } +func Fatalf(message string, ss ...any) { + logMessage(FATAL, "", fmt.Sprintf(message, ss...), nil) +} + func FatalF(message string, fields map[string]any) { logMessage(FATAL, "", message, fields) } @@ -239,3 +292,78 @@ func FatalF(message string, fields map[string]any) { func FatalCF(component string, message string, fields map[string]any) { logMessage(FATAL, component, message, fields) } + +// Logger implements common Logger interface +type Logger struct { + component string +} + +// Debug logs debug messages +func (b *Logger) Debug(v ...interface{}) { + logMessage(DEBUG, b.component, fmt.Sprint(v...), nil) +} + +// Info logs info messages +func (b *Logger) Info(v ...interface{}) { + logMessage(INFO, b.component, fmt.Sprint(v...), nil) +} + +// Warn logs warning messages +func (b *Logger) Warn(v ...interface{}) { + logMessage(WARN, b.component, fmt.Sprint(v...), nil) +} + +// Error logs error messages +func (b *Logger) Error(v ...interface{}) { + logMessage(ERROR, b.component, fmt.Sprint(v...), nil) +} + +// Debugf logs formatted debug messages +func (b *Logger) Debugf(format string, v ...any) { + //debugCallerInfo() + logMessage(DEBUG, b.component, fmt.Sprintf(format, v...), nil) +} + +// Infof logs formatted info messages +func (b *Logger) Infof(format string, v ...any) { + //debugCallerInfo() + logMessage(INFO, b.component, fmt.Sprintf(format, v...), nil) +} + +// Warnf logs formatted warning messages +func (b *Logger) Warnf(format string, v ...any) { + logMessage(WARN, b.component, fmt.Sprintf(format, v...), nil) +} + +// Errorf logs formatted error messages +func (b *Logger) Errorf(format string, v ...any) { + //debugCallerInfo() + logMessage(ERROR, b.component, fmt.Sprintf(format, v...), nil) +} + +// Sync flushes log buffer (no-op for this implementation) +func (b *Logger) Sync() error { + return nil +} + +// NewLogger creates a new logger instance with optional component name +func NewLogger(component string) *Logger { + return &Logger{component: component} +} + +// for debugging logger only +func debugCallerInfo() { + for i := 2; i < 15; i++ { + pc, file, line, ok := runtime.Caller(i) + if !ok { + continue + } + + fn := runtime.FuncForPC(pc) + if fn == nil { + continue + } + + fmt.Println(file, line) + } +}