71079962ca
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>
102 lines
2.9 KiB
Go
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
|
|
}
|