feat: integrar structured logging en todos los componentes del shell

Se propaga *slog.Logger a todos los componentes impuros del shell:
- shell/bus/ — logs de subscribe, send, reply, timeout, unsubscribe
- shell/effects/ — duración y resultado de cada action ejecutada
- shell/llm/ (anthropic, openai, factory) — request/response con tokens, duración, fallback
- shell/memory/sqlite — open, save, recall, close con detalles
- shell/ssh/ — inicio, fin, errores y duración de comandos SSH
- tools/registry — registro, ejecución y errores de herramientas

Se usa el paquete shell/logger para field names consistentes (FieldDurationMS, FieldTokensUsed, etc.).
Cada componente recibe el logger por inyección de dependencias, sin globals.
Las firmas de New/FromConfig se actualizan para aceptar *slog.Logger.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 21:53:31 +00:00
parent 6858a5f13e
commit 5697b92ab8
11 changed files with 175 additions and 30 deletions
+26 -3
View File
@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"fmt"
"log/slog"
"net"
"os"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/tools"
"github.com/enmanuel/agents/shell/logger"
)
// Result holds the output of an SSH command execution.
@@ -25,22 +27,32 @@ type Result struct {
// Executor runs SSH commands against configured targets.
type Executor struct {
cfg config.SSHCfg
cfg config.SSHCfg
logger *slog.Logger
}
// NewExecutor creates an Executor from the SSH config section.
func NewExecutor(cfg config.SSHCfg) *Executor {
return &Executor{cfg: cfg}
func NewExecutor(cfg config.SSHCfg, log *slog.Logger) *Executor {
return &Executor{cfg: cfg, logger: log.With(logger.FieldComponent, "ssh")}
}
// Execute runs the SSH command described by spec. Impure.
func (e *Executor) Execute(ctx context.Context, spec tools.SSHCommandSpec) Result {
cmdPreview := spec.Command
if len(cmdPreview) > 80 {
cmdPreview = cmdPreview[:80] + "..."
}
e.logger.Info("ssh_exec_start", "target", spec.Target, "command", cmdPreview)
start := time.Now()
target, ok := e.cfg.Targets[spec.Target]
if !ok {
e.logger.Error("ssh_exec_error", "target", spec.Target, "err", "unknown target")
return Result{Err: fmt.Errorf("unknown SSH target: %s", spec.Target)}
}
if len(target.Hosts) == 0 {
e.logger.Error("ssh_exec_error", "target", spec.Target, "err", "no hosts")
return Result{Err: fmt.Errorf("no hosts for target: %s", spec.Target)}
}
@@ -65,6 +77,8 @@ func (e *Executor) Execute(ctx context.Context, spec tools.SSHCommandSpec) Resul
signer, err := loadSigner(keyEnv)
if err != nil {
ms := time.Since(start).Milliseconds()
e.logger.Error("ssh_exec_error", "target", spec.Target, logger.FieldDurationMS, ms, "err", err)
return Result{Err: fmt.Errorf("load SSH key: %w", err)}
}
@@ -81,12 +95,16 @@ func (e *Executor) Execute(ctx context.Context, spec tools.SSHCommandSpec) Resul
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := gossh.Dial("tcp", addr, sshCfg)
if err != nil {
ms := time.Since(start).Milliseconds()
e.logger.Error("ssh_exec_error", "target", spec.Target, "host", addr, logger.FieldDurationMS, ms, "err", err)
return Result{Err: fmt.Errorf("ssh dial %s: %w", addr, err)}
}
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
ms := time.Since(start).Milliseconds()
e.logger.Error("ssh_exec_error", "target", spec.Target, logger.FieldDurationMS, ms, "err", err)
return Result{Err: fmt.Errorf("ssh session: %w", err)}
}
defer session.Close()
@@ -102,17 +120,22 @@ func (e *Executor) Execute(ctx context.Context, spec tools.SSHCommandSpec) Resul
select {
case <-ctx.Done():
session.Signal(gossh.SIGTERM)
ms := time.Since(start).Milliseconds()
e.logger.Warn("ssh_exec_cancelled", "target", spec.Target, logger.FieldDurationMS, ms)
return Result{Err: ctx.Err()}
case err := <-done:
ms := time.Since(start).Milliseconds()
code := 0
if err != nil {
var exitErr *gossh.ExitError
if ok := asExitError(err, &exitErr); ok {
code = exitErr.ExitStatus()
} else {
e.logger.Error("ssh_exec_error", "target", spec.Target, logger.FieldDurationMS, ms, "err", err)
return Result{Err: err}
}
}
e.logger.Info("ssh_exec_end", "target", spec.Target, "exit_code", code, logger.FieldDurationMS, ms)
return Result{
Stdout: stdout.String(),
Stderr: stderr.String(),