Files
agents_and_robots/dev/issues/completed/007-logs-mejorados.md
T
egutierrez f561f686c4 refactor: migrar tasks/ a dev/issues/ con estructura de desarrollo
Se mueve la documentación de issues/tasks de .claude/tasks/ a dev/issues/
para separar la planificación de desarrollo de la configuración de Claude.
Se añade dev/README.md como índice de la carpeta de desarrollo. Los issues
completados se mueven a dev/issues/completed/. Esto permite que dev/ sea
el punto central de documentación interna del proyecto.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:41:16 +00:00

9.8 KiB

Tarea: Implementar Sistema de Logging Estructurado para Agentes

Contexto del Proyecto

Estamos construyendo un sistema multi-agente en Go con las siguientes características arquitectónicas:

  • Separación pure core / impure shell: el core retorna decisiones como datos, el shell las ejecuta e interactúa con el mundo exterior.
  • Monorepo en Go con módulos separados.
  • Comunicación inter-agente via Matrix (mautrix-go) como bus de mensajes.
  • Múltiples agentes con identidades independientes (cada uno con su propio contexto Git, etc.).
  • Integración con múltiples LLM providers (Anthropic, OpenAI-compatible, Ollama) via abstracción unificada.

El logging vive en el impure shell — nunca en el core.

Objetivo

Crear un paquete pkg/logger (o internal/logger) que provea logging estructurado en formato JSONL, optimizado para ser consumido tanto por humanos como por agentes LLM. Los logs deben ser fácilmente parseables, consultables por fecha/agente, y auto-gestionados (rotación, limpieza).

Requisitos Funcionales

1. Formato de Salida: JSONL

Cada línea de log es un objeto JSON independiente con los siguientes campos obligatorios:

{
  "time": "2026-03-06T10:00:00.000Z",
  "level": "INFO",
  "msg": "agent action completed",
  "agent_id": "researcher-01",
  "trace_id": "abc123",
  "component": "shell"
}

Campos opcionales según contexto:

{
  "action": "web_search",
  "duration_ms": 342,
  "tokens_used": 1500,
  "result": "success",
  "error_type": "timeout",
  "reason": "user requested summary of recent papers",
  "metadata": {}
}

El campo reason es especialmente importante: cuando otro agente lee el log, necesita saber por qué se tomó una decisión, no solo qué se hizo.

2. Segmentación de Archivos

Estructura de directorios por agente y por día:

/var/log/agents/
├── orchestrator/
│   ├── 2026-03-04.jsonl
│   ├── 2026-03-05.jsonl
│   └── 2026-03-06.jsonl
├── researcher-01/
│   ├── 2026-03-05.jsonl
│   └── 2026-03-06.jsonl
└── coder-01/
    └── 2026-03-06.jsonl

Reglas:

  • Un archivo JSONL por agente por día.
  • Si un archivo excede un tamaño máximo configurable (default: 50MB), se rota añadiendo un sufijo incremental: 2026-03-06.jsonl2026-03-06.1.jsonl.
  • Nombres de archivo siempre en formato YYYY-MM-DD.jsonl.

3. Rotación y Limpieza

  • Retención configurable (default: 7 días).
  • Goroutine de limpieza que corre periódicamente (default: cada 24h) y elimina archivos que excedan la retención.
  • Compresión opcional de archivos rotados (gzip).
  • La limpieza debe ser segura para ejecución concurrente.

4. API del Logger

// Config para crear un logger de agente
type LoggerConfig struct {
    BaseDir      string        // directorio raíz de logs (default: "/var/log/agents")
    AgentID      string        // identificador único del agente
    MaxSizeMB    int64         // tamaño máximo por archivo (default: 50)
    MaxAgeDays   int           // días de retención (default: 7)
    Compress     bool          // comprimir archivos rotados (default: true)
    CleanupInterval time.Duration // intervalo de limpieza (default: 24h)
    Level        slog.Level    // nivel mínimo de log (default: slog.LevelInfo)
}

// Factory function
func NewAgentLogger(cfg LoggerConfig) (*slog.Logger, func(), error)
// Retorna:
//   - *slog.Logger: logger configurado con slog
//   - func(): función de cleanup para llamar en shutdown (cierra archivos, detiene goroutine de limpieza)
//   - error: si no se puede crear el directorio o el archivo inicial

// Uso esperado:
logger, cleanup, err := logger.NewAgentLogger(logger.LoggerConfig{
    AgentID: "researcher-01",
})
defer cleanup()

logger.InfoContext(ctx, "executing decision",
    "action", decision.Action,
    "reason", decision.Reason,
    "trace_id", traceIDFromCtx(ctx),
    "tokens_used", 1500,
)

5. Writer Personalizado

Implementar un io.Writer que maneje la rotación diaria con fallback por tamaño:

type DailyRotatingWriter struct {
    baseDir    string
    agentID    string
    maxSizeMB  int64
    compress   bool
    
    mu         sync.Mutex
    current    *os.File
    written    int64
    currentDay string
    suffix     int  // para rotación por tamaño dentro del mismo día
}

// Debe implementar io.Writer
func (w *DailyRotatingWriter) Write(p []byte) (n int, err error)

// Cierre limpio
func (w *DailyRotatingWriter) Close() error

Lógica de Write:

  1. Adquirir lock.
  2. Verificar si el día cambió (time.Now().Format("2006-01-02") vs w.currentDay).
  3. Si cambió el día: cerrar archivo actual, comprimir si compress=true, abrir nuevo archivo del día, resetear written y suffix.
  4. Si written > maxSizeMB * 1024 * 1024: incrementar suffix, abrir nuevo archivo (2026-03-06.1.jsonl), resetear written.
  5. Escribir p al archivo actual.
  6. Incrementar written.

6. Helpers para Consulta por LLMs

Proveer funciones utilitarias para que los agentes puedan consultar logs:

// Leer logs de un agente en un rango de fechas
func ReadLogs(baseDir, agentID string, from, to time.Time) ([]json.RawMessage, error)

// Leer logs de un agente para un día específico
func ReadDayLogs(baseDir, agentID string, date time.Time) ([]json.RawMessage, error)

// Buscar logs que contengan un campo con un valor específico
func SearchLogs(baseDir, agentID string, field, value string, from, to time.Time) ([]json.RawMessage, error)

// Listar agentes disponibles (subdirectorios)
func ListAgents(baseDir string) ([]string, error)

// Listar fechas disponibles para un agente
func ListDates(baseDir, agentID string) ([]time.Time, error)

Estas funciones permiten que un agente LLM solicite logs con interfaces simples. El agente orquestador puede usar SearchLogs para buscar errores, o ReadDayLogs para obtener contexto de lo que hizo otro agente ayer.

Requisitos No Funcionales

  • Stdlib primero: usar log/slog como base. No dependencias externas excepto lo estrictamente necesario (si lumberjack simplifica, se puede usar, pero la implementación custom del DailyRotatingWriter es preferida).
  • Thread-safe: múltiples goroutines escribirán al mismo logger.
  • Mínimo overhead: el logging no debe impactar significativamente el rendimiento del agente. Escribir en buffer si es necesario.
  • Consistencia de campos: usar los mismos nombres de campo siempre. Definir constantes para campos estándar:
const (
    FieldAgentID    = "agent_id"
    FieldTraceID    = "trace_id"
    FieldAction     = "action"
    FieldReason     = "reason"
    FieldDurationMS = "duration_ms"
    FieldTokensUsed = "tokens_used"
    FieldResult     = "result"
    FieldErrorType  = "error_type"
    FieldComponent  = "component"
)
  • Testeable: incluir tests unitarios para:
    • Rotación por día.
    • Rotación por tamaño dentro del mismo día.
    • Limpieza de archivos viejos.
    • Formato de salida JSONL correcto.
    • Concurrencia (múltiples writers simultáneos).
    • Funciones de consulta (ReadLogs, SearchLogs).

Estructura de Archivos Esperada

pkg/logger/
├── logger.go          // NewAgentLogger, LoggerConfig, constantes de campos
├── writer.go          // DailyRotatingWriter implementation
├── cleanup.go         // Goroutine de limpieza y compresión
├── query.go           // ReadLogs, SearchLogs, ListAgents, ListDates
├── logger_test.go     // Tests del logger y formato
├── writer_test.go     // Tests de rotación
├── cleanup_test.go    // Tests de limpieza
└── query_test.go      // Tests de consulta

Restricciones

  • Go 1.21+ (para log/slog nativo).
  • Sin CGO.
  • Sin dependencias externas (stdlib pura). Si consideras que alguna dependencia aporta valor significativo, justifícala explícitamente.
  • El logger debe poder funcionar tanto escribiendo a archivos como a stdout (para desarrollo/debugging), configurable via LoggerConfig.
  • Todos los timestamps en UTC.

Ejemplo de Integración

Así se vería el uso del logger dentro del shell de un agente:

package main

import (
    "context"
    "log/slog"
    "myproject/pkg/logger"
)

func main() {
    log, cleanup, err := logger.NewAgentLogger(logger.LoggerConfig{
        AgentID:  "researcher-01",
        BaseDir:  "/var/log/agents",
        Level:    slog.LevelInfo,
        Compress: true,
    })
    if err != nil {
        panic(err)
    }
    defer cleanup()

    ctx := context.Background()
    ctx = logger.WithTraceID(ctx, "trace-abc-123")

    // El core retorna una decisión pura
    decision := core.Decide(input)

    // El shell loguea y ejecuta
    log.InfoContext(ctx, "executing decision",
        logger.FieldAction, decision.Action,
        logger.FieldReason, decision.Reason,
        logger.FieldComponent, "shell",
    )

    result, err := shell.Execute(ctx, decision)
    if err != nil {
        log.ErrorContext(ctx, "decision execution failed",
            logger.FieldAction, decision.Action,
            logger.FieldErrorType, categorizeError(err),
            "error", err.Error(),
        )
        return
    }

    log.InfoContext(ctx, "decision executed successfully",
        logger.FieldAction, decision.Action,
        logger.FieldResult, "success",
        logger.FieldDurationMS, result.DurationMS,
        logger.FieldTokensUsed, result.TokensUsed,
    )
}

Notas Adicionales

  • El trace_id permite correlacionar un flujo completo a través de múltiples agentes. Si el orchestrator inicia una tarea y delega al researcher, ambos usan el mismo trace_id.
  • Considerar un helper WithTraceID(ctx, id) / TraceIDFromCtx(ctx) usando context.Value.
  • El campo reason captura la intención detrás de la acción. Un LLM que lee "reason: user requested summary of recent AI papers" entiende el contexto sin necesidad de reconstruirlo desde mensajes anteriores.