Files
picoclaw/pkg/logger/logger.go
T
Kunal Karmakar 073ae4864f Fix spelling
2026-03-21 07:20:59 +00:00

370 lines
7.5 KiB
Go

package logger
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"github.com/rs/zerolog"
)
type LogLevel = zerolog.Level
const (
DEBUG = zerolog.DebugLevel
INFO = zerolog.InfoLevel
WARN = zerolog.WarnLevel
ERROR = zerolog.ErrorLevel
FATAL = zerolog.FatalLevel
)
var (
logLevelNames = map[LogLevel]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
FATAL: "FATAL",
}
currentLevel = INFO
logger zerolog.Logger
fileLogger zerolog.Logger
logFile *os.File
once sync.Once
mu sync.RWMutex
)
func init() {
once.Do(func() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "15:04:05", // TODO: make it configurable???
// Custom formatter to handle multiline strings and JSON objects
FormatFieldValue: formatFieldValue,
}
logger = zerolog.New(consoleWriter).With().Timestamp().Caller().Logger()
fileLogger = zerolog.Logger{}
})
}
func formatFieldValue(i any) string {
var s string
switch val := i.(type) {
case string:
s = val
case []byte:
s = string(val)
default:
return fmt.Sprintf("%v", i)
}
if unquoted, err := strconv.Unquote(s); err == nil {
s = unquoted
}
if strings.Contains(s, "\n") {
return fmt.Sprintf("\n%s", s)
}
if strings.Contains(s, " ") {
if (strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")) ||
(strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]")) {
return s
}
return fmt.Sprintf("%q", s)
}
return s
}
func SetLevel(level LogLevel) {
mu.Lock()
defer mu.Unlock()
currentLevel = level
zerolog.SetGlobalLevel(level)
}
func SetConsoleLevel(level LogLevel) {
mu.Lock()
defer mu.Unlock()
logger = logger.Level(level)
}
func GetLevel() LogLevel {
mu.RLock()
defer mu.RUnlock()
return currentLevel
}
// ParseLevel converts a case-insensitive level name to a LogLevel.
// Returns the level and true if valid, or (INFO, false) if unrecognized.
func ParseLevel(s string) (LogLevel, bool) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "debug":
return DEBUG, true
case "info":
return INFO, true
case "warn", "warning":
return WARN, true
case "error":
return ERROR, true
case "fatal":
return FATAL, true
default:
return INFO, false
}
}
// SetLevelFromString sets the log level from a string value.
// If the string is empty or not a recognized level name, the current level is kept.
func SetLevelFromString(s string) {
if s == "" {
return
}
if level, ok := ParseLevel(s); ok {
SetLevel(level)
}
}
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)
}
newFile, 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)
}
// Close old file if exists
if logFile != nil {
logFile.Close()
}
logFile = newFile
fileLogger = zerolog.New(logFile).With().Timestamp().Caller().Logger()
return nil
}
func DisableFileLogging() {
mu.Lock()
defer mu.Unlock()
if logFile != nil {
logFile.Close()
logFile = nil
}
fileLogger = zerolog.Logger{}
}
func getCallerSkip() int {
for i := 2; i < 15; i++ {
pc, file, _, 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, "/logger_3rd_party.go") ||
strings.HasSuffix(file, "/log.go") {
continue
}
funcName := fn.Name()
if strings.HasPrefix(funcName, "runtime.") {
continue
}
return i - 1
}
return 3
}
//nolint:zerologlint
func getEvent(logger zerolog.Logger, level LogLevel) *zerolog.Event {
switch level {
case zerolog.DebugLevel:
return logger.Debug()
case zerolog.InfoLevel:
return logger.Info()
case zerolog.WarnLevel:
return logger.Warn()
case zerolog.ErrorLevel:
return logger.Error()
case zerolog.FatalLevel:
return logger.Fatal()
default:
return logger.Info()
}
}
func logMessage(level LogLevel, component string, message string, fields map[string]any) {
if level < currentLevel {
return
}
skip := getCallerSkip()
event := getEvent(logger, level)
if component != "" {
event.Str("component", component)
}
appendFields(event, fields)
event.CallerSkipFrame(skip).Msg(message)
// Also log to file if enabled
if fileLogger.GetLevel() != zerolog.NoLevel {
fileEvent := getEvent(fileLogger, level)
if component != "" {
fileEvent.Str("component", component)
}
// fileEvent.Str("caller", fmt.Sprintf("%s:%d (%s)", callerFile, callerLine, callerFunc))
appendFields(fileEvent, fields)
fileEvent.CallerSkipFrame(skip).Msg(message)
}
if level == FATAL {
os.Exit(1)
}
}
func appendFields(event *zerolog.Event, fields map[string]any) {
for k, v := range fields {
// Type switch to avoid double JSON serialization of strings
switch val := v.(type) {
case string:
event.Str(k, val)
case int:
event.Int(k, val)
case int64:
event.Int64(k, val)
case float64:
event.Float64(k, val)
case bool:
event.Bool(k, val)
default:
event.Interface(k, v) // Fallback for struct, slice and maps
}
}
}
func Debug(message string) {
logMessage(DEBUG, "", message, nil)
}
func DebugC(component string, message string) {
logMessage(DEBUG, component, message, nil)
}
func Debugf(message string, ss ...any) {
logMessage(DEBUG, "", fmt.Sprintf(message, ss...), nil)
}
func DebugF(message string, fields map[string]any) {
logMessage(DEBUG, "", message, fields)
}
func DebugCF(component string, message string, fields map[string]any) {
logMessage(DEBUG, component, message, fields)
}
func Info(message string) {
logMessage(INFO, "", message, nil)
}
func InfoC(component string, message string) {
logMessage(INFO, component, message, nil)
}
func InfoF(message string, fields map[string]any) {
logMessage(INFO, "", message, fields)
}
func Infof(message string, ss ...any) {
logMessage(INFO, "", fmt.Sprintf(message, ss...), nil)
}
func InfoCF(component string, message string, fields map[string]any) {
logMessage(INFO, component, message, fields)
}
func Warn(message string) {
logMessage(WARN, "", message, nil)
}
func WarnC(component string, message string) {
logMessage(WARN, component, message, nil)
}
func WarnF(message string, fields map[string]any) {
logMessage(WARN, "", message, fields)
}
func WarnCF(component string, message string, fields map[string]any) {
logMessage(WARN, component, message, fields)
}
func Error(message string) {
logMessage(ERROR, "", message, nil)
}
func ErrorC(component string, message string) {
logMessage(ERROR, component, message, nil)
}
func Errorf(message string, ss ...any) {
logMessage(ERROR, "", fmt.Sprintf(message, ss...), nil)
}
func ErrorF(message string, fields map[string]any) {
logMessage(ERROR, "", message, fields)
}
func ErrorCF(component string, message string, fields map[string]any) {
logMessage(ERROR, component, message, fields)
}
func Fatal(message string) {
logMessage(FATAL, "", message, nil)
}
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)
}
func FatalCF(component string, message string, fields map[string]any) {
logMessage(FATAL, component, message, fields)
}