init, eod commit

This commit is contained in:
2025-11-30 22:00:47 +01:00
parent 76a288de8b
commit 675e114278
9 changed files with 340 additions and 16 deletions

191
util/logger/log.go Normal file
View File

@@ -0,0 +1,191 @@
package logger
import (
"context"
"fmt"
"homestead/homestead_gateway/util/config"
"log/slog"
"os"
"path/filepath"
"time"
slogmulti "github.com/samber/slog-multi"
)
// New creates a logger identified by id.
// Returns: *slog.Logger, closeFunc, error.
// Call closeFunc() on shutdown to close open files.
func New(id string, cfg config.LogConfig) (*slog.Logger, func() error, error) {
if cfg.Directory == "" {
cfg.Directory = "logs"
}
if cfg.Rotation <= 0 {
cfg.Rotation = 7
}
console := slog.NewTextHandler(&prefixWriter{inner: os.Stderr, prefix: []byte("[" + id + "] "), startLine: true}, &slog.HandlerOptions{AddSource: true})
router := newFileRouter(cfg.Directory, cfg.Rotation, id)
root := slogmulti.Fanout(console, router)
return slog.New(root), router.CloseFiles, nil
}
func (p *prefixWriter) Write(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
totalWritten := 0
if p.startLine {
n, err := p.inner.Write(p.prefix)
totalWritten += n
if err != nil {
return totalWritten, err
}
p.startLine = false
}
n, err := p.inner.Write(b)
totalWritten += n
if err != nil {
return totalWritten, err
}
if len(b) > 0 && b[len(b)-1] == '\n' {
p.startLine = true
}
return totalWritten, nil
}
func newFileRouter(baseDir string, rotationDays int, id string) *fileRouter {
return &fileRouter{
handlers: make(map[string]slog.Handler),
files: make(map[string]*os.File),
baseDir: baseDir,
rotationDays: rotationDays,
id: id,
dirTimeLayout: "2006-01-02", // e.g. 2025-11-30
}
}
//goland:noinspection GoUnusedParameter
func (r *fileRouter) Enabled(ctx context.Context, lvl slog.Level) bool {
// Conservatively true; actual handler will decide.
return true
}
func (r *fileRouter) Handle(ctx context.Context, rec slog.Record) error {
now := time.Now()
dirName := now.Format(r.dirTimeLayout)
dirPath := filepath.Join(r.baseDir, dirName)
filePath := filepath.Join(dirPath, r.id+".log")
h, err := r.getHandler(dirPath, filePath)
if err != nil {
return fmt.Errorf("file router get handler: %w", err)
}
return h.Handle(ctx, rec)
}
func (r *fileRouter) WithAttrs(attrs []slog.Attr) slog.Handler {
return r
}
func (r *fileRouter) WithGroup(name string) slog.Handler {
return r
}
// getHandler returns a text handler for the given file, creating dir/file and cleaning up old dirs if needed.
func (r *fileRouter) getHandler(dirPath, filePath string) (slog.Handler, error) {
r.mu.RLock()
h, ok := r.handlers[filePath]
r.mu.RUnlock()
if ok {
return h, nil
}
r.mu.Lock()
defer r.mu.Unlock()
if h, ok = r.handlers[filePath]; ok {
return h, nil
}
_, statErr := os.Stat(dirPath)
dirExisted := statErr == nil
if err := os.MkdirAll(dirPath, 0o755); err != nil {
return nil, fmt.Errorf("mkdir %s: %w", dirPath, err)
}
if !dirExisted {
if err := r.cleanupOldDirs(); err != nil {
// don't fail logging just because cleanup failed; report to stderr and continue.
_, _ = fmt.Fprintf(os.Stderr, "logger: cleanupOldDirs error: %v\n", err)
}
}
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, fmt.Errorf("open log file %s: %w", filePath, err)
}
textHandler := slog.NewTextHandler(f, &slog.HandlerOptions{
AddSource: false,
})
r.files[filePath] = f
r.handlers[filePath] = textHandler
return textHandler, nil
}
// cleanupOldDirs scans r.baseDir for directories matching the dirTimeLayout and deletes any whose
// day-start time is older than rotationDays from now.
func (r *fileRouter) cleanupOldDirs() error {
entries, err := os.ReadDir(r.baseDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("read baseDir: %w", err)
}
now := time.Now()
cutoff := now.AddDate(0, 0, -r.rotationDays)
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
t, err := time.ParseInLocation(r.dirTimeLayout, name, time.Local)
if err != nil {
continue
}
if t.Before(cutoff) {
path := filepath.Join(r.baseDir, name)
if err := os.RemoveAll(path); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "logger: failed to remove old log dir %s: %v\n", path, err)
}
}
}
return nil
}
// CloseFiles closes open files
func (r *fileRouter) CloseFiles() error {
r.mu.Lock()
defer r.mu.Unlock()
var firstErr error
for p, f := range r.files {
if err := f.Close(); err != nil && firstErr == nil {
firstErr = err
}
delete(r.files, p)
delete(r.handlers, p)
}
return firstErr
}

27
util/logger/structs.go Normal file
View File

@@ -0,0 +1,27 @@
package logger
import (
"io"
"log/slog"
"os"
"sync"
)
type fileRouter struct {
mu sync.RWMutex
handlers map[string]slog.Handler // map[filePath]handler
files map[string]*os.File // map[filePath]*os.File to close later
baseDir string
rotationDays int
id string
dirTimeLayout string // "2006-01-02" - daily dirs
}
// prefixWriter - writes a prefix at the start of each new line.
// It is safe for concurrent use.
type prefixWriter struct {
inner io.Writer
prefix []byte
mu sync.Mutex
startLine bool
}