osaet/backend/internal/admin/logging.go

139 lines
3 KiB
Go

package admin
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
type rotatingLogWriter struct {
path string
maxBytes int64
maxBackups int
file *os.File
size int64
mu sync.Mutex
}
func ConfigureLogging(cfg Config) (io.Closer, error) {
if cfg.LogFile == "" {
return nil, nil
}
writer, err := newRotatingLogWriter(cfg.LogFile, cfg.LogMaxBytes, cfg.LogMaxBackups)
if err != nil {
return nil, err
}
multi := io.MultiWriter(os.Stdout, writer)
log.SetOutput(multi)
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.LUTC)
return writer, nil
}
func newRotatingLogWriter(path string, maxBytes int64, maxBackups int) (*rotatingLogWriter, error) {
if maxBytes <= 0 {
maxBytes = 10 * 1024 * 1024
}
if maxBackups <= 0 {
maxBackups = 5
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("create log dir: %w", err)
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return nil, fmt.Errorf("open log file: %w", err)
}
info, err := file.Stat()
if err != nil {
file.Close()
return nil, fmt.Errorf("stat log file: %w", err)
}
return &rotatingLogWriter{
path: path,
maxBytes: maxBytes,
maxBackups: maxBackups,
file: file,
size: info.Size(),
}, nil
}
func (w *rotatingLogWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.size+int64(len(p)) > w.maxBytes {
if err := w.rotateLocked(); err != nil {
return 0, err
}
}
n, err := w.file.Write(p)
w.size += int64(n)
return n, err
}
func (w *rotatingLogWriter) 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 *rotatingLogWriter) rotateLocked() error {
if w.file != nil {
if err := w.file.Close(); err != nil {
return err
}
}
stamp := time.Now().UTC().Format("20060102T150405")
rotated := fmt.Sprintf("%s.%s", w.path, stamp)
if err := os.Rename(w.path, rotated); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("rotate log: %w", err)
}
if err := w.pruneLocked(); err != nil {
return err
}
file, err := os.OpenFile(w.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("open rotated log file: %w", err)
}
w.file = file
w.size = 0
return nil
}
func (w *rotatingLogWriter) pruneLocked() error {
pattern := w.path + ".*"
matches, err := filepath.Glob(pattern)
if err != nil {
return fmt.Errorf("list rotated logs: %w", err)
}
if len(matches) <= w.maxBackups {
return nil
}
sortStrings(matches)
for _, path := range matches[:len(matches)-w.maxBackups] {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove old log %s: %w", path, err)
}
}
return nil
}
func sortStrings(values []string) {
for i := 1; i < len(values); i++ {
value := values[i]
j := i - 1
for ; j >= 0 && values[j] > value; j-- {
values[j+1] = values[j]
}
values[j+1] = value
}
}