ad4355df17
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
146 lines
3.4 KiB
Go
146 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"scratchbox/internal/config"
|
|
)
|
|
|
|
func newAccessLogWriter(cfg config.Config) (io.Writer, func() error, error) {
|
|
if !cfg.Server.AccessLog.Enabled {
|
|
return io.Discard, func() error { return nil }, nil
|
|
}
|
|
if cfg.Server.AccessLog.FilePath == "" {
|
|
return os.Stdout, func() error { return nil }, nil
|
|
}
|
|
|
|
writer, err := newRollingFileWriter(
|
|
cfg.Server.AccessLog.FilePath,
|
|
cfg.Server.AccessLog.MaxSizeBytes,
|
|
cfg.Server.AccessLog.MaxBackups,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return io.MultiWriter(os.Stdout, writer), writer.Close, nil
|
|
}
|
|
|
|
type rollingFileWriter struct {
|
|
mu sync.Mutex
|
|
path string
|
|
maxSize int64
|
|
maxBackups int
|
|
file *os.File
|
|
size int64
|
|
}
|
|
|
|
func newRollingFileWriter(path string, maxSize int64, maxBackups int) (*rollingFileWriter, error) {
|
|
if maxSize <= 0 {
|
|
return nil, fmt.Errorf("max size must be > 0")
|
|
}
|
|
if maxBackups <= 0 {
|
|
return nil, fmt.Errorf("max backups must be > 0")
|
|
}
|
|
|
|
if dir := filepath.Dir(path); dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create log directory: %w", err)
|
|
}
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open log file: %w", err)
|
|
}
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
_ = f.Close()
|
|
return nil, fmt.Errorf("stat log file: %w", err)
|
|
}
|
|
|
|
return &rollingFileWriter{
|
|
path: path,
|
|
maxSize: maxSize,
|
|
maxBackups: maxBackups,
|
|
file: f,
|
|
size: info.Size(),
|
|
}, nil
|
|
}
|
|
|
|
func (w *rollingFileWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.size > 0 && w.size+int64(len(p)) > w.maxSize {
|
|
if err := w.rotateLocked(); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
n, err := w.file.Write(p)
|
|
w.size += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
func (w *rollingFileWriter) 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 *rollingFileWriter) rotateLocked() error {
|
|
if err := w.file.Close(); err != nil {
|
|
return fmt.Errorf("close before rotate: %w", err)
|
|
}
|
|
|
|
for i := w.maxBackups - 1; i >= 1; i-- {
|
|
from := rotatedLogPath(w.path, i)
|
|
to := rotatedLogPath(w.path, i+1)
|
|
if _, err := os.Stat(from); err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("stat rotated file %s: %w", from, err)
|
|
}
|
|
if err := os.Remove(to); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove target backup slot %s: %w", to, err)
|
|
}
|
|
if err := os.Rename(from, to); err != nil {
|
|
return fmt.Errorf("rotate backup %s -> %s: %w", from, to, err)
|
|
}
|
|
}
|
|
|
|
firstBackup := rotatedLogPath(w.path, 1)
|
|
if err := os.Remove(firstBackup); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove first backup %s: %w", firstBackup, err)
|
|
}
|
|
if _, err := os.Stat(w.path); err == nil {
|
|
if err := os.Rename(w.path, firstBackup); err != nil {
|
|
return fmt.Errorf("rotate current log to %s: %w", firstBackup, err)
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("stat current log file: %w", err)
|
|
}
|
|
|
|
f, err := os.OpenFile(w.path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("open rotated log file: %w", err)
|
|
}
|
|
w.file = f
|
|
w.size = 0
|
|
return nil
|
|
}
|
|
|
|
func rotatedLogPath(path string, index int) string {
|
|
return fmt.Sprintf("%s.%d", path, index)
|
|
}
|