- Go API server with PostgreSQL + Redis - AI floor replacement (OpenRouter Gemini) - Product database (10 brands, 3539 products) - Recommendation engine, calculator, articles - Redis async queue + worker pool - Hot product caching, brand view tracking - JWT auth, favorites, projects - Docker deployment ready Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
2.8 KiB
Go
140 lines
2.8 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Level int
|
|
|
|
const (
|
|
DEBUG Level = iota
|
|
INFO
|
|
WARN
|
|
ERROR
|
|
)
|
|
|
|
const maxLogAge = 30 * 24 * time.Hour
|
|
|
|
var levelNames = map[Level]string{DEBUG: "DEBUG", INFO: "INFO", WARN: "WARN", ERROR: "ERROR"}
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
std *log.Logger
|
|
logDir string
|
|
logLevel Level = INFO
|
|
logFile *os.File
|
|
todayDate string
|
|
)
|
|
|
|
// Init sets up logging with daily rotation to dir/app-YYYY-MM-DD.log.
|
|
func Init(dir, level string) {
|
|
logDir = dir
|
|
os.MkdirAll(dir, 0755)
|
|
|
|
switch strings.ToUpper(level) {
|
|
case "DEBUG":
|
|
logLevel = DEBUG
|
|
case "WARN":
|
|
logLevel = WARN
|
|
case "ERROR":
|
|
logLevel = ERROR
|
|
default:
|
|
logLevel = INFO
|
|
}
|
|
|
|
openToday()
|
|
go dailyCleanup()
|
|
Info("Logger initialized, level=%s, dir=%s", level, dir)
|
|
}
|
|
|
|
func openToday() {
|
|
if logFile != nil {
|
|
logFile.Close()
|
|
}
|
|
todayDate = time.Now().Format("2006-01-02")
|
|
f, err := os.OpenFile(filepath.Join(logDir, "app-"+todayDate+".log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
log.Printf("Failed to open log file: %v, logging to stdout only", err)
|
|
std = log.New(os.Stdout, "", 0)
|
|
logFile = nil
|
|
return
|
|
}
|
|
logFile = f
|
|
multi := io.MultiWriter(os.Stdout, f)
|
|
std = log.New(multi, "", 0)
|
|
}
|
|
|
|
func dailyCleanup() {
|
|
for {
|
|
time.Sleep(1 * time.Hour)
|
|
// Delete old logs
|
|
cutoff := time.Now().Add(-maxLogAge)
|
|
entries, _ := os.ReadDir(logDir)
|
|
for _, e := range entries {
|
|
if strings.HasPrefix(e.Name(), "app-") && strings.HasSuffix(e.Name(), ".log") {
|
|
dateStr := strings.TrimPrefix(strings.TrimSuffix(e.Name(), ".log"), "app-")
|
|
if t, err := time.Parse("2006-01-02", dateStr); err == nil && t.Before(cutoff) {
|
|
os.Remove(filepath.Join(logDir, e.Name()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func Close() {
|
|
if logFile != nil {
|
|
logFile.Close()
|
|
}
|
|
}
|
|
|
|
func rotateIfNeeded() {
|
|
d := time.Now().Format("2006-01-02")
|
|
if d != todayDate {
|
|
mu.Lock()
|
|
if d != todayDate {
|
|
openToday()
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func output(level Level, format string, args ...any) {
|
|
if level < logLevel {
|
|
return
|
|
}
|
|
rotateIfNeeded()
|
|
|
|
msg := fmt.Sprintf(format, args...)
|
|
now := time.Now().Format("2006-01-02 15:04:05.000")
|
|
_, file, line, _ := runtime.Caller(2)
|
|
short := file[strings.LastIndex(file, "/")+1:]
|
|
|
|
lineStr := fmt.Sprintf("%s [%s] %s:%d %s", now, levelNames[level], short, line, msg)
|
|
|
|
mu.Lock()
|
|
if std != nil {
|
|
std.Println(lineStr)
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
func Debug(format string, args ...any) { output(DEBUG, format, args...) }
|
|
func Info(format string, args ...any) { output(INFO, format, args...) }
|
|
func Warn(format string, args ...any) { output(WARN, format, args...) }
|
|
func Error(format string, args ...any) { output(ERROR, format, args...) }
|
|
|
|
func StdLogger() *log.Logger {
|
|
if std != nil {
|
|
return std
|
|
}
|
|
return log.New(os.Stdout, "", 0)
|
|
}
|