mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 23:07:33 +08:00
354 lines
7.0 KiB
Go
354 lines
7.0 KiB
Go
package logger
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"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"`
|
|
Message string `json:"message"`
|
|
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("Failed to clean up old log files:", err)
|
|
}
|
|
log.Println("File logging enabled:", 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("File logging disabled")
|
|
}
|
|
}
|
|
|
|
func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) {
|
|
if level < currentLevel {
|
|
return
|
|
}
|
|
|
|
entry := LogEntry{
|
|
Level: logLevelNames[level],
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
Component: component,
|
|
Message: message,
|
|
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("Failed to write file log:", 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),
|
|
message,
|
|
fieldStr,
|
|
)
|
|
|
|
log.Println(logLine)
|
|
|
|
if level == FATAL {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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(message string) {
|
|
logMessage(DEBUG, "", message, nil)
|
|
}
|
|
|
|
func DebugC(component string, message string) {
|
|
logMessage(DEBUG, component, message, nil)
|
|
}
|
|
|
|
func DebugF(message string, fields map[string]interface{}) {
|
|
logMessage(DEBUG, "", message, fields)
|
|
}
|
|
|
|
func DebugCF(component string, message string, fields map[string]interface{}) {
|
|
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]interface{}) {
|
|
logMessage(INFO, "", message, fields)
|
|
}
|
|
|
|
func InfoCF(component string, message string, fields map[string]interface{}) {
|
|
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]interface{}) {
|
|
logMessage(WARN, "", message, fields)
|
|
}
|
|
|
|
func WarnCF(component string, message string, fields map[string]interface{}) {
|
|
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, fields map[string]interface{}) {
|
|
logMessage(ERROR, "", message, fields)
|
|
}
|
|
|
|
func ErrorCF(component string, message string, fields map[string]interface{}) {
|
|
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, fields map[string]interface{}) {
|
|
logMessage(FATAL, "", message, fields)
|
|
}
|
|
|
|
func FatalCF(component string, message string, fields map[string]interface{}) {
|
|
logMessage(FATAL, component, message, fields)
|
|
}
|