Files
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

177 lines
4.3 KiB
Go

package lh
import (
"io"
"sync"
"github.com/olekukonko/ll/lx"
)
// RotateSource defines the callbacks needed to implement log rotation.
// It abstracts the destination lifecycle: opening, sizing, and rotating.
//
// Example for file rotation:
//
// src := lh.RotateSource{
// Open: func() (io.WriteCloser, error) {
// return os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
// },
// Size: func() (int64, error) {
// if fi, err := os.Stat("app.log"); err == nil {
// return fi.Size(), nil
// }
// return 0, nil // File doesn't exist yet
// },
// Rotate: func() error {
// // Rename current log before creating new one
// return os.Rename("app.log", "app.log."+time.Now().Format("20060102-150405"))
// },
// }
type RotateSource struct {
// Open returns a fresh destination for log output.
// Called on initialization and after rotation.
Open func() (io.WriteCloser, error)
// Size returns the current size in bytes of the active destination.
// Return an error if size cannot be determined (rotation will be skipped).
Size func() (int64, error)
// Rotate performs cleanup/rotation actions before opening a new destination.
// For files: rename or move the current log. Optional for other destinations.
Rotate func() error
}
// Rotating wraps a handler to rotate its output when maxSize is exceeded.
// The wrapped handler must implement both Handler and Outputter interfaces.
// Rotation is triggered on each Handle call if the current size >= maxSize.
//
// Example:
//
// handler := lx.NewJSONHandler(os.Stdout)
// src := lh.RotateSource{...} // see RotateSource example
// rotator, err := lh.NewRotating(handler, 10*1024*1024, src) // 10 MB
// logger := lx.NewLogger(rotator)
// logger.Info("This log may trigger rotation when file reaches 10MB")
type Rotating[H interface {
lx.Handler
lx.Outputter
}] struct {
mu sync.Mutex
maxSize int64
src RotateSource
out io.WriteCloser
handler H
}
// NewRotating creates a rotating wrapper around handler.
// Handler's output will be replaced with destinations from src.Open.
// If maxSizeBytes <= 0, rotation is disabled.
// src.Rotate may be nil if no pre-open actions are needed.
//
// Example:
//
// // Create a JSON handler that rotates at 5MB
// handler := lx.NewJSONHandler(os.Stdout)
// rotator, err := lh.NewRotating(handler, 5*1024*1024, src)
// if err != nil {
// log.Fatal(err)
// }
// // Use rotator as your logger's handler
// logger := lx.NewLogger(rotator)
func NewRotating[H interface {
lx.Handler
lx.Outputter
}](handler H, maxSizeBytes int64, src RotateSource) (*Rotating[H], error) {
r := &Rotating[H]{
maxSize: maxSizeBytes,
src: src,
handler: handler,
}
if err := r.reopenLocked(); err != nil {
return nil, err
}
return r, nil
}
// Handle processes a log entry, rotating output if necessary.
// Thread-safe: can be called concurrently.
//
// Example:
//
// rotator.Handle(&lx.Entry{
// Level: lx.InfoLevel,
// Message: "Processing request",
// Namespace: "api",
// })
func (r *Rotating[H]) Handle(e *lx.Entry) error {
r.mu.Lock()
defer r.mu.Unlock()
if err := r.rotateIfNeededLocked(); err != nil {
return err
}
return r.handler.Handle(e)
}
// Close releases resources (closes the current output).
// Safe to call multiple times.
//
// Example:
//
// defer rotator.Close()
func (r *Rotating[H]) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.out != nil {
return r.out.Close()
}
return nil
}
// rotateIfNeededLocked checks current size and rotates if maxSize exceeded.
// Called with mu already held.
func (r *Rotating[H]) rotateIfNeededLocked() error {
if r.maxSize <= 0 || r.src.Size == nil || r.src.Open == nil {
return nil
}
size, err := r.src.Size()
if err != nil {
// Size unknown - skip rotation
return nil
}
if size < r.maxSize {
return nil
}
// Close current output
if r.out != nil {
_ = r.out.Close()
r.out = nil
}
// Run rotation hook (rename/move/commit)
if r.src.Rotate != nil {
if err := r.src.Rotate(); err != nil {
return err
}
}
// Open fresh output
return r.reopenLocked()
}
// reopenLocked opens a new destination and sets it on the handler.
// Called with mu already held.
func (r *Rotating[H]) reopenLocked() error {
out, err := r.src.Open()
if err != nil {
return err
}
r.out = out
r.handler.Output(out)
return nil
}