241 lines
5.5 KiB
Go
241 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type dailyWriter struct {
|
|
dir string
|
|
prefix string
|
|
mu sync.Mutex
|
|
day string
|
|
part int
|
|
file *os.File
|
|
fileSize int64
|
|
retention int
|
|
maxFileBytes int64
|
|
maxTotalBytes int64
|
|
lastCleanup time.Time
|
|
bytesSinceGC int64
|
|
}
|
|
|
|
func newDailyWriter(dir, prefix string, retention int, maxFileBytes, maxTotalBytes int64) (*dailyWriter, error) {
|
|
dir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
w := &dailyWriter{
|
|
dir: dir,
|
|
prefix: prefix,
|
|
retention: retention,
|
|
maxFileBytes: maxFileBytes,
|
|
maxTotalBytes: maxTotalBytes,
|
|
}
|
|
if err = w.openForDayLocked(time.Now()); err != nil {
|
|
return nil, err
|
|
}
|
|
w.cleanupLocked(time.Now())
|
|
return w, nil
|
|
}
|
|
|
|
func (w *dailyWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
now := time.Now()
|
|
if w.file == nil || w.day != now.Format("2006-01-02") {
|
|
if err := w.openForDayLocked(now); err != nil {
|
|
return 0, err
|
|
}
|
|
w.cleanupLocked(now)
|
|
}
|
|
if w.maxFileBytes > 0 && w.fileSize > 0 && w.fileSize+int64(len(p)) > w.maxFileBytes {
|
|
if err := w.openNextPartLocked(now); err != nil {
|
|
return 0, err
|
|
}
|
|
w.cleanupLocked(now)
|
|
}
|
|
n, err := w.file.Write(p)
|
|
w.fileSize += int64(n)
|
|
w.bytesSinceGC += int64(n)
|
|
if err == nil && (w.bytesSinceGC >= 1<<20 || now.Sub(w.lastCleanup) >= time.Minute || (w.maxFileBytes > 0 && w.fileSize >= w.maxFileBytes)) {
|
|
w.cleanupLocked(now)
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (w *dailyWriter) Close() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.file == nil {
|
|
return nil
|
|
}
|
|
err := w.file.Close()
|
|
w.file = nil
|
|
return err
|
|
}
|
|
|
|
func (w *dailyWriter) openForDayLocked(now time.Time) error {
|
|
if w.file != nil {
|
|
_ = w.file.Close()
|
|
w.file = nil
|
|
}
|
|
w.day = now.Format("2006-01-02")
|
|
w.part = 0
|
|
for {
|
|
path := w.partPathLocked()
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return w.openPathLocked(path, 0)
|
|
}
|
|
return err
|
|
}
|
|
if w.maxFileBytes <= 0 || info.Size() < w.maxFileBytes {
|
|
return w.openPathLocked(path, info.Size())
|
|
}
|
|
w.part++
|
|
}
|
|
}
|
|
|
|
func (w *dailyWriter) openNextPartLocked(now time.Time) error {
|
|
if w.file != nil {
|
|
_ = w.file.Close()
|
|
w.file = nil
|
|
}
|
|
if w.day != now.Format("2006-01-02") {
|
|
return w.openForDayLocked(now)
|
|
}
|
|
w.part++
|
|
for {
|
|
path := w.partPathLocked()
|
|
info, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
return w.openPathLocked(path, 0)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if w.maxFileBytes <= 0 || info.Size() < w.maxFileBytes {
|
|
return w.openPathLocked(path, info.Size())
|
|
}
|
|
w.part++
|
|
}
|
|
}
|
|
|
|
func (w *dailyWriter) partPathLocked() string {
|
|
if w.part == 0 {
|
|
return filepath.Join(w.dir, fmt.Sprintf("%s-%s.log", w.prefix, w.day))
|
|
}
|
|
return filepath.Join(w.dir, fmt.Sprintf("%s-%s-%03d.log", w.prefix, w.day, w.part))
|
|
}
|
|
|
|
func (w *dailyWriter) openPathLocked(path string, size int64) error {
|
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.file = file
|
|
w.fileSize = size
|
|
return nil
|
|
}
|
|
|
|
type logFileInfo struct {
|
|
path string
|
|
size int64
|
|
modTime time.Time
|
|
}
|
|
|
|
func (w *dailyWriter) cleanupLocked(now time.Time) {
|
|
w.lastCleanup = now
|
|
w.bytesSinceGC = 0
|
|
entries, err := os.ReadDir(w.dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cutoff := now.AddDate(0, 0, -w.retention)
|
|
files := make([]logFileInfo, 0, len(entries))
|
|
var total int64
|
|
currentPath := ""
|
|
if w.file != nil {
|
|
currentPath, _ = filepath.Abs(w.file.Name())
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !isManagedLogName(entry.Name()) {
|
|
continue
|
|
}
|
|
path := filepath.Join(w.dir, entry.Name())
|
|
info, statErr := entry.Info()
|
|
if statErr != nil {
|
|
continue
|
|
}
|
|
if w.retention > 0 && info.ModTime().Before(cutoff) && !samePath(path, currentPath) {
|
|
if os.Remove(path) == nil {
|
|
continue
|
|
}
|
|
}
|
|
files = append(files, logFileInfo{path: path, size: info.Size(), modTime: info.ModTime()})
|
|
total += info.Size()
|
|
}
|
|
if w.maxTotalBytes <= 0 || total <= w.maxTotalBytes {
|
|
return
|
|
}
|
|
sort.Slice(files, func(i, j int) bool { return files[i].modTime.Before(files[j].modTime) })
|
|
for _, file := range files {
|
|
if total <= w.maxTotalBytes {
|
|
break
|
|
}
|
|
if samePath(file.path, currentPath) {
|
|
continue
|
|
}
|
|
if os.Remove(file.path) == nil {
|
|
total -= file.size
|
|
}
|
|
}
|
|
}
|
|
|
|
func isManagedLogName(name string) bool {
|
|
return strings.HasSuffix(name, ".log") && (strings.HasPrefix(name, "service-") || strings.HasPrefix(name, "crash-"))
|
|
}
|
|
|
|
func samePath(left, right string) bool {
|
|
return right != "" && strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
|
|
}
|
|
|
|
type appLogger struct {
|
|
*slog.Logger
|
|
writer *dailyWriter
|
|
}
|
|
|
|
func newAppLogger(cfg Config, component string) (*appLogger, error) {
|
|
maxFileBytes := int64(cfg.LogMaxFileMB) * 1024 * 1024
|
|
maxTotalBytes := int64(cfg.LogMaxTotalMB) * 1024 * 1024
|
|
w, err := newDailyWriter(cfg.LogDir, "service-"+component, cfg.LogRetentionDays, maxFileBytes, maxTotalBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handler := slog.NewJSONHandler(io.MultiWriter(os.Stdout, w), &slog.HandlerOptions{Level: slog.LevelInfo})
|
|
return &appLogger{Logger: slog.New(handler).With("component", component, "pid", os.Getpid()), writer: w}, nil
|
|
}
|
|
|
|
func (l *appLogger) Close() error {
|
|
if l == nil || l.writer == nil {
|
|
return nil
|
|
}
|
|
return l.writer.Close()
|
|
}
|
|
|
|
func discardLogger() *appLogger {
|
|
return &appLogger{Logger: slog.New(slog.NewJSONHandler(io.Discard, nil))}
|
|
}
|