Files
clawgo/pkg/logger/logger.go
2026-03-03 10:36:53 +08:00

385 lines
7.5 KiB
Go

package logger
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
FATAL
)
var (
logLevelNames = map[LogLevel]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
FATAL: "FATAL",
}
currentLevel = INFO
logger *Logger
once sync.Once
mu sync.RWMutex
)
type Logger struct {
file *os.File
filePath string
maxSizeBytes int64
maxAgeDays int
fileMu sync.Mutex
}
type LogEntry struct {
Level string `json:"level"`
Timestamp string `json:"timestamp"`
Component string `json:"component,omitempty"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
Caller string `json:"caller,omitempty"`
}
func init() {
once.Do(func() {
logger = &Logger{}
})
}
func SetLevel(level LogLevel) {
mu.Lock()
defer mu.Unlock()
currentLevel = level
}
func GetLevel() LogLevel {
mu.RLock()
defer mu.RUnlock()
return currentLevel
}
func EnableFileLogging(filePath string) error {
return EnableFileLoggingWithRotation(filePath, 20, 3)
}
func EnableFileLoggingWithRotation(filePath string, maxSizeMB, maxAgeDays int) error {
mu.Lock()
defer mu.Unlock()
if maxSizeMB <= 0 {
maxSizeMB = 20
}
if maxAgeDays <= 0 {
maxAgeDays = 3
}
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); 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, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
if logger.file != nil {
logger.file.Close()
}
logger.file = file
logger.filePath = filePath
logger.maxSizeBytes = int64(maxSizeMB) * 1024 * 1024
logger.maxAgeDays = maxAgeDays
if err := logger.cleanupOldLogFiles(); err != nil {
log.Println(C0145, err)
}
log.Println(C0146, filePath)
return nil
}
func DisableFileLogging() {
mu.Lock()
defer mu.Unlock()
if logger.file != nil {
logger.file.Close()
logger.file = nil
logger.filePath = ""
logger.maxSizeBytes = 0
logger.maxAgeDays = 0
log.Println(C0147)
}
}
func logMessage(level LogLevel, component string, code CodeID, fields map[string]interface{}) {
if level < currentLevel {
return
}
if fields == nil {
fields = map[string]interface{}{}
}
entryCode := int(code)
entryMessage := extractSystemError(fields)
entry := LogEntry{
Level: logLevelNames[level],
Timestamp: time.Now().UTC().Format(time.RFC3339),
Component: component,
Code: entryCode,
Message: entryMessage,
Fields: fields,
}
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 {
if err := logger.writeLine(append(jsonData, '\n')); err != nil {
log.Println(C0148, err)
}
}
}
var fieldStr string
if len(fields) > 0 {
fieldStr = " " + formatFields(fields)
}
logLine := fmt.Sprintf("[%s] [%s]%s %s%s",
entry.Timestamp,
logLevelNames[level],
formatComponent(component),
formatCode(entry.Code),
fieldStr,
)
if entry.Message != "" {
logLine += " msg=" + entry.Message
}
log.Println(logLine)
if level == FATAL {
os.Exit(1)
}
}
func extractSystemError(fields map[string]interface{}) string {
if fields == nil {
return ""
}
v, ok := fields[FieldError]
if !ok || v == nil {
return ""
}
return strings.TrimSpace(fmt.Sprintf("%v", v))
}
func formatCode(code int) string {
if code <= 0 {
return "code=0"
}
return "code=" + strconv.Itoa(code)
}
func (l *Logger) writeLine(line []byte) error {
l.fileMu.Lock()
defer l.fileMu.Unlock()
if l.file == nil {
return nil
}
if l.maxSizeBytes > 0 {
if err := l.rotateIfNeeded(int64(len(line))); err != nil {
return err
}
}
_, err := l.file.Write(line)
return err
}
func (l *Logger) rotateIfNeeded(nextWrite int64) error {
info, err := l.file.Stat()
if err != nil {
return err
}
if info.Size()+nextWrite <= l.maxSizeBytes {
return nil
}
if err := l.file.Close(); err != nil {
return err
}
backupPath := fmt.Sprintf("%s.%s", l.filePath, time.Now().UTC().Format("20060102-150405"))
if err := os.Rename(l.filePath, backupPath); err != nil {
return err
}
file, err := os.OpenFile(l.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
l.file = file
return l.cleanupOldLogFiles()
}
func (l *Logger) cleanupOldLogFiles() error {
if l.maxAgeDays <= 0 || l.filePath == "" {
return nil
}
dir := filepath.Dir(l.filePath)
base := filepath.Base(l.filePath)
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
cutoff := time.Now().AddDate(0, 0, -l.maxAgeDays)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// Only delete rotated files like clawgo.log.20260213-120000
if !strings.HasPrefix(name, base+".") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
_ = os.Remove(filepath.Join(dir, name))
}
}
return nil
}
func formatComponent(component string) string {
if component == "" {
return ""
}
return fmt.Sprintf(" %s:", component)
}
func formatFields(fields map[string]interface{}) string {
var parts []string
for k, v := range fields {
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
}
return fmt.Sprintf("{%s}", strings.Join(parts, ", "))
}
func Debug(code CodeID) {
logMessage(DEBUG, "", code, nil)
}
func DebugC(component string, code CodeID) {
logMessage(DEBUG, component, code, nil)
}
func DebugF(code CodeID, fields map[string]interface{}) {
logMessage(DEBUG, "", code, fields)
}
func DebugCF(component string, code CodeID, fields map[string]interface{}) {
logMessage(DEBUG, component, code, fields)
}
func Info(code CodeID) {
logMessage(INFO, "", code, nil)
}
func InfoC(component string, code CodeID) {
logMessage(INFO, component, code, nil)
}
func InfoF(code CodeID, fields map[string]interface{}) {
logMessage(INFO, "", code, fields)
}
func InfoCF(component string, code CodeID, fields map[string]interface{}) {
logMessage(INFO, component, code, fields)
}
func Warn(code CodeID) {
logMessage(WARN, "", code, nil)
}
func WarnC(component string, code CodeID) {
logMessage(WARN, component, code, nil)
}
func WarnF(code CodeID, fields map[string]interface{}) {
logMessage(WARN, "", code, fields)
}
func WarnCF(component string, code CodeID, fields map[string]interface{}) {
logMessage(WARN, component, code, fields)
}
func Error(code CodeID) {
logMessage(ERROR, "", code, nil)
}
func ErrorC(component string, code CodeID) {
logMessage(ERROR, component, code, nil)
}
func ErrorF(code CodeID, fields map[string]interface{}) {
logMessage(ERROR, "", code, fields)
}
func ErrorCF(component string, code CodeID, fields map[string]interface{}) {
logMessage(ERROR, component, code, fields)
}
func Fatal(code CodeID) {
logMessage(FATAL, "", code, nil)
}
func FatalC(component string, code CodeID) {
logMessage(FATAL, component, code, nil)
}
func FatalF(code CodeID, fields map[string]interface{}) {
logMessage(FATAL, "", code, fields)
}
func FatalCF(component string, code CodeID, fields map[string]interface{}) {
logMessage(FATAL, component, code, fields)
}