Files
agents_and_robots/shell/logger/logger.go
T
egutierrez 71079962ca feat: add structured JSONL logging package with rotation and query
Nuevo paquete shell/logger/ que implementa logging estructurado JSONL
para agentes. Incluye DailyRotatingWriter con rotación diaria y por
tamaño (50MB default), limpieza automática de archivos viejos (7 días),
compresión gzip de logs rotados, y funciones de consulta (ReadLogs,
SearchLogs, ListAgents, ListDates) para que agentes LLM puedan leer
logs de otros agentes. Basado en log/slog de stdlib, sin dependencias
externas. 18 tests unitarios cubren rotación, concurrencia y consultas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:26:56 +00:00

102 lines
2.9 KiB
Go

// Package logger provides structured JSONL logging for agents with daily
// file rotation, size-based splitting, automatic cleanup, and query helpers.
package logger
import (
"context"
"log/slog"
"os"
"time"
)
// Standard field names for structured logging across all agents.
const (
FieldAgentID = "agent_id"
FieldTraceID = "trace_id"
FieldAction = "action"
FieldReason = "reason"
FieldDurationMS = "duration_ms"
FieldTokensUsed = "tokens_used"
FieldResult = "result"
FieldErrorType = "error_type"
FieldComponent = "component"
)
// traceKey is the context key for trace IDs.
type traceKey struct{}
// WithTraceID returns a new context carrying the given trace ID.
func WithTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceKey{}, id)
}
// TraceIDFromCtx extracts the trace ID from ctx, or "" if absent.
func TraceIDFromCtx(ctx context.Context) string {
if v, ok := ctx.Value(traceKey{}).(string); ok {
return v
}
return ""
}
// LoggerConfig configures a per-agent logger.
type LoggerConfig struct {
BaseDir string // root log directory (default: "logs"); empty → stdout only
AgentID string // agent identifier (required)
MaxSizeMB int64 // max file size before rotation (default: 50)
MaxAgeDays int // retention in days (default: 7)
Compress bool // gzip rotated files (default: true)
CleanupInterval time.Duration // cleanup ticker interval (default: 24h)
Level slog.Level // minimum log level (default: INFO)
}
func (c *LoggerConfig) defaults() {
if c.BaseDir == "" {
c.BaseDir = "logs"
}
if c.MaxSizeMB <= 0 {
c.MaxSizeMB = 50
}
if c.MaxAgeDays <= 0 {
c.MaxAgeDays = 7
}
if c.CleanupInterval <= 0 {
c.CleanupInterval = 24 * time.Hour
}
}
// NewAgentLogger creates a structured JSON logger that writes to daily-rotated
// JSONL files under BaseDir/<AgentID>/. It returns:
// - a *slog.Logger pre-enriched with agent_id
// - a cleanup func to call on shutdown (closes files, stops cleanup goroutine)
// - an error if the log directory cannot be created
//
// If BaseDir is literally "stdout", the logger writes to os.Stdout with no
// file rotation or cleanup.
func NewAgentLogger(cfg LoggerConfig) (*slog.Logger, func(), error) {
if cfg.BaseDir == "stdout" {
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.Level})
l := slog.New(h).With(FieldAgentID, cfg.AgentID)
return l, func() {}, nil
}
cfg.defaults()
w, err := NewDailyRotatingWriter(cfg.BaseDir, cfg.AgentID, cfg.MaxSizeMB, cfg.Compress)
if err != nil {
return nil, nil, err
}
h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: cfg.Level})
l := slog.New(h).With(FieldAgentID, cfg.AgentID)
ctx, cancel := context.WithCancel(context.Background())
go runCleanup(ctx, cfg.BaseDir, cfg.AgentID, cfg.MaxAgeDays, cfg.CleanupInterval)
cleanup := func() {
cancel()
w.Close()
}
return l, cleanup, nil
}