Files
logging/logging.go

287 lines
6.4 KiB
Go
Raw Normal View History

2025-12-24 19:09:07 +08:00
package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/term"
)
const (
COLOR_RESET = "\033[0m"
COLOR_RED = "\033[91m"
COLOR_YELLOW = "\033[93m"
COLOR_GREEN = "\033[92m"
COLOR_BLUE = "\033[94m"
COLOR_CYAN = "\033[96m"
COLOR_WHITE = "\033[97m"
COLOR_GRAY = "\033[90m"
COLOR_GREY = COLOR_GRAY
)
type LogLevel int
const (
Debug LogLevel = iota
Info
Warn
Error
Critical
)
func (l LogLevel) String() string {
switch l {
case Debug:
return "DEBUG"
case Info:
return "INFO"
case Warn:
return "WARN"
case Error:
return "ERROR"
case Critical:
return "CRITICAL"
default:
return "UNKNOWN"
}
}
var LevelColorMap = map[LogLevel]string{
Debug: COLOR_GRAY,
Info: COLOR_WHITE,
Warn: COLOR_YELLOW,
Error: COLOR_RED,
Critical: COLOR_RED,
}
type Formatter interface {
Format(level LogLevel, timestamp time.Time, message string) string
}
type ConsoleFormatter struct {
TimestampFormatter string
UseColor bool
LevelColors map[LogLevel]string
mu sync.Mutex
}
func NewConsoleFormatter(timestampFormat string, useColor bool, levelColors map[LogLevel]string) *ConsoleFormatter {
if timestampFormat == "" {
timestampFormat = "[02/01/06 15:04:05]"
}
if levelColors == nil {
levelColors = LevelColorMap
}
return &ConsoleFormatter{
TimestampFormatter: timestampFormat,
UseColor: useColor,
LevelColors: levelColors,
}
}
func (f *ConsoleFormatter) Format(level LogLevel, timestamp time.Time, message string) string {
f.mu.Lock()
defer f.mu.Unlock()
timestampStr := timestamp.Format(f.TimestampFormatter)
levelStr := level.String()
color, ok := f.LevelColors[level]
if !ok {
color = COLOR_WHITE
}
if f.UseColor && isTTY() {
return fmt.Sprintf("%s %s[%s]%s %s", timestampStr, color, levelStr, COLOR_RESET, message)
}
return fmt.Sprintf("%s [%s] %s", timestampStr, levelStr, message)
}
type FileFormatter struct {
TimestampFormat string
LoggerName string
}
func NewFileFormatter(timestampFormat, loggerName string) *FileFormatter {
if timestampFormat == "" {
timestampFormat = "2006-01-02 15:04:05"
}
return &FileFormatter{
TimestampFormat: timestampFormat,
LoggerName: loggerName,
}
}
func (f *FileFormatter) Format(level LogLevel, timestamp time.Time, message string) string {
return fmt.Sprintf("[ %s | %s | %s ] %s", timestamp.Format(f.TimestampFormat), f.LoggerName, level.String(), message)
}
type Logger struct {
name string
level LogLevel
consoleColor bool
logDir string
logFilenamePattern string
formatter Formatter
consoleWriter io.Writer
fileWriter io.WriteCloser
fileLogger *log.Logger
fileFormatter *FileFormatter
mu sync.Mutex
initialized bool
}
var (
globalLogger *Logger
once sync.Once
)
func NewLogger(
loggerName string,
logLevel LogLevel,
consoleColor bool,
logDir string,
logFilenamePattern string,
) *Logger {
if loggerName == "" {
loggerName = "main"
}
if logDir == "" {
logDir = "logs"
}
if logFilenamePattern == "" {
logFilenamePattern = "2006-01-02"
}
return &Logger{
name: loggerName,
level: logLevel,
consoleColor: consoleColor,
logDir: logDir,
logFilenamePattern: logFilenamePattern,
consoleWriter: os.Stdout,
}
}
func (l *Logger) init() {
l.mu.Lock()
defer l.mu.Unlock()
if l.initialized {
return
}
if err := os.MkdirAll(l.logDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating log directory %s: %v\n", l.logDir, err)
} else {
logFilePath := l.genUniqueLogFilename()
file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening log file %s: %v\n", logFilePath, err)
} else {
l.fileWriter = file
if l.fileWriter != nil {
l.fileFormatter = NewFileFormatter("", l.name)
}
l.fileLogger = log.New(l.fileWriter, "", 0)
fmt.Printf("Log file will be saved to %s\n", logFilePath)
}
}
l.formatter = NewConsoleFormatter("", l.consoleColor, nil)
l.initialized = true
}
func (l *Logger) genUniqueLogFilename() string {
now := time.Now()
baseFilename := now.Format(l.logFilenamePattern)
logFilePath := filepath.Join(l.logDir, fmt.Sprintf("%s.log", baseFilename))
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
return logFilePath
}
counter := 1
for {
suffixedFilePath := filepath.Join(l.logDir, fmt.Sprintf("%s_%d.log", baseFilename, counter))
if _, err := os.Stat(suffixedFilePath); os.IsNotExist(err) {
return suffixedFilePath
}
counter += 1
}
}
func (l *Logger) Log(level LogLevel, message string) {
if !l.initialized {
l.init()
}
if level < l.level {
return
}
now := time.Now()
if l.formatter != nil {
formattedMsg := l.formatter.Format(level, now, message)
fmt.Fprintln(l.consoleWriter, formattedMsg)
} else {
fmt.Fprintf(l.consoleWriter, "%s [%s] %s\n", now.Format("[15:04:05]"), level.String(), message)
}
if l.fileLogger != nil && l.fileFormatter != nil {
formattedFileMsg := l.fileFormatter.Format(level, now, message)
l.fileLogger.Println(formattedFileMsg)
}
}
func (l *Logger) Debug(message string, v ...any) {
l.Log(Debug, fmt.Sprintf(message, v...))
}
func (l *Logger) Warn(message string, v ...any) {
l.Log(Warn, fmt.Sprintf(message, v...))
}
func (l *Logger) Info(message string, v ...any) {
l.Log(Info, fmt.Sprintf(message, v...))
}
func (l *Logger) Error(message string, v ...any) {
l.Log(Error, fmt.Sprintf(message, v...))
}
func (l *Logger) Critical(message string, v ...any) {
l.Log(Critical, fmt.Sprintf(message, v...))
os.Exit(1)
}
func (l *Logger) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.fileWriter != nil {
err := l.fileWriter.Close()
l.fileWriter = nil
l.fileLogger = nil
return err
}
return nil
}
func GetLogger() *Logger {
once.Do(func() {
globalLogger = NewLogger("shizuku", Debug, true, "logs", "2006-01-02")
})
return globalLogger
}
func isTTY() bool {
return os.Getenv("TERM") != "" && term.IsTerminal(int(os.Stdout.Fd()))
}