feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DailyRotatingWriter is an io.Writer that rotates log files daily and by
|
||||
// size. Files are named <baseDir>/<agentID>/YYYY-MM-DD.jsonl with optional
|
||||
// numeric suffixes for size-based splits within the same day.
|
||||
type DailyRotatingWriter struct {
|
||||
baseDir string
|
||||
agentID string
|
||||
maxSize int64 // bytes
|
||||
compress bool
|
||||
nowFunc func() time.Time // for testing; defaults to time.Now().UTC
|
||||
dir string // resolved agent log directory
|
||||
|
||||
mu sync.Mutex
|
||||
current *os.File
|
||||
written int64
|
||||
currentDay string
|
||||
suffix int
|
||||
}
|
||||
|
||||
// NewDailyRotatingWriter creates a writer that stores logs under
|
||||
// baseDir/agentID/. It creates the directory if needed and opens the first
|
||||
// log file for today.
|
||||
func NewDailyRotatingWriter(baseDir, agentID string, maxSizeMB int64, compress bool) (*DailyRotatingWriter, error) {
|
||||
dir := filepath.Join(baseDir, agentID)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create log dir %s: %w", dir, err)
|
||||
}
|
||||
|
||||
w := &DailyRotatingWriter{
|
||||
baseDir: baseDir,
|
||||
agentID: agentID,
|
||||
maxSize: maxSizeMB * 1024 * 1024,
|
||||
compress: compress,
|
||||
nowFunc: func() time.Time { return time.Now().UTC() },
|
||||
dir: dir,
|
||||
}
|
||||
|
||||
if err := w.openFile(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write implements io.Writer with daily and size-based rotation.
|
||||
func (w *DailyRotatingWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
today := w.nowFunc().Format("2006-01-02")
|
||||
|
||||
// Day changed → rotate to new day file.
|
||||
if today != w.currentDay {
|
||||
if err := w.rotate(today, 0); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Size exceeded → split within same day.
|
||||
if w.written+int64(len(p)) > w.maxSize && w.written > 0 {
|
||||
w.suffix++
|
||||
if err := w.rotate(today, w.suffix); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
n, err := w.current.Write(p)
|
||||
w.written += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the current log file.
|
||||
func (w *DailyRotatingWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.current != nil {
|
||||
return w.current.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotate closes the current file (optionally compressing it) and opens a new one.
|
||||
func (w *DailyRotatingWriter) rotate(day string, suffix int) error {
|
||||
prev := w.current
|
||||
prevPath := ""
|
||||
if prev != nil {
|
||||
prevPath = prev.Name()
|
||||
prev.Close()
|
||||
}
|
||||
|
||||
// Compress the previous file in the background if enabled and it's from a
|
||||
// different day (we don't compress intra-day splits until day rotates).
|
||||
if w.compress && prevPath != "" && day != w.currentDay {
|
||||
go compressFile(prevPath)
|
||||
}
|
||||
|
||||
w.currentDay = day
|
||||
w.suffix = suffix
|
||||
w.written = 0
|
||||
|
||||
return w.openFile()
|
||||
}
|
||||
|
||||
// openFile opens (or creates) the log file for the current day/suffix.
|
||||
func (w *DailyRotatingWriter) openFile() error {
|
||||
w.currentDay = w.nowFunc().Format("2006-01-02")
|
||||
name := w.filename(w.currentDay, w.suffix)
|
||||
|
||||
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open log file %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Track how much has already been written (append mode).
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
w.current = f
|
||||
w.written = info.Size()
|
||||
return nil
|
||||
}
|
||||
|
||||
// filename returns the full path for a given day and suffix.
|
||||
func (w *DailyRotatingWriter) filename(day string, suffix int) string {
|
||||
if suffix == 0 {
|
||||
return filepath.Join(w.dir, day+".jsonl")
|
||||
}
|
||||
return filepath.Join(w.dir, fmt.Sprintf("%s.%d.jsonl", day, suffix))
|
||||
}
|
||||
|
||||
// compressFile gzips src to src.gz and removes the original.
|
||||
func compressFile(src string) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(src + ".gz")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gz := gzip.NewWriter(out)
|
||||
if _, err := io.Copy(gz, in); err != nil {
|
||||
gz.Close()
|
||||
out.Close()
|
||||
os.Remove(src + ".gz")
|
||||
return
|
||||
}
|
||||
gz.Close()
|
||||
out.Close()
|
||||
in.Close()
|
||||
os.Remove(src)
|
||||
}
|
||||
Reference in New Issue
Block a user