refactor logger to zerolog

This commit is contained in:
Cytown 2026-03-08 18:28:44 +08:00
parent 81dfdf5f45
commit 3e754dab0e
3 changed files with 211 additions and 81 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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("<none> | %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)
}
}