Files
fn_registry/dev/issues/completed/0019-structured-logging.md
T
egutierrez 0bfe267501 docs: cerrar issue 0019 structured logging
Funciones, tipos y tests implementados. Registry actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:15:45 +02:00

7.7 KiB

0019 — Structured Logging Go

Metadata

Campo Valor
ID 0019
Estado pendiente
Prioridad media
Tipo feature

Dependencias

  • logger_middleware depende de issue 0009 (HTTP Server Foundation) para el tipo Middleware.
  • El resto de funciones no tiene dependencias externas.

Objetivo

Funciones de structured logging en Go (dominio infra) basadas en log/slog de stdlib. Logs en JSON con niveles, campos contextuales y middleware HTTP, reemplazando el uso ad-hoc de fmt.Println y log.Printf en las apps.

Contexto

  • Python ya tiene get_logger_py_infra y setup_logger_py_infra con rotacion, dual output y niveles.
  • Bash tiene bash_log_bash_shell con niveles y colores.
  • Go tiene cero funciones de logging estructurado. Las apps (deploy_server, sqlite_api, pipeline_launcher) loguean con fmt.Println o log.Printf sin estructura, sin niveles, sin contexto.
  • Go 1.21+ incluye log/slog en stdlib: JSON handler, niveles, campos key-value, groups. No se necesita zerolog ni zap.

Arquitectura

functions/infra/
├── logger_new.go               — NEW: crea logger con nivel, output y formato
├── logger_new.md               — NEW
├── logger_with.go              — NEW: retorna copia del logger con campos adicionales
├── logger_with.md              — NEW
├── logger_middleware.go         — NEW: middleware HTTP que loguea requests
├── logger_middleware.md         — NEW
├── log_debug.go                — NEW: log a nivel debug
├── log_debug.md                — NEW
├── log_info.go                 — NEW: log a nivel info
├── log_info.md                 — NEW
├── log_warn.go                 — NEW: log a nivel warn
├── log_warn.md                 — NEW
├── log_error.go                — NEW: log a nivel error
├── log_error.md                — NEW

types/infra/
├── logger.md                   — NEW: metadata del tipo Logger
├── log_level.md                — NEW: metadata del tipo LogLevel
├── log_entry.md                — NEW: metadata del tipo LogEntry

Patron pure core / impure shell

  • Pure: logger_with (copia inmutable del logger con campos adicionales, sin I/O)
  • Impure: logger_new, log_debug, log_info, log_warn, log_error, logger_middleware (escriben a un io.Writer)

Diseno

Tipos

// LogLevel representa los niveles de log soportados.
type LogLevel int

const (
    LogLevelDebug LogLevel = iota
    LogLevelInfo
    LogLevelWarn
    LogLevelError
)

// Logger wrappea slog.Logger con config del registry.
type Logger struct {
    Level  LogLevel
    Output io.Writer
    Format string // "json" | "text"
    Fields map[string]any
    inner  *slog.Logger
}

// LogEntry representa una entrada de log estructurada.
type LogEntry struct {
    Timestamp time.Time      `json:"timestamp"`
    Level     string         `json:"level"`
    Message   string         `json:"message"`
    Fields    map[string]any `json:"fields,omitempty"`
}

Funciones

Funcion Purity Firma (simplificada)
logger_new impure (level LogLevel, output io.Writer, format string) (*Logger, error)
logger_with pure (logger *Logger, fields map[string]any) *Logger
log_debug impure (logger *Logger, msg string, fields ...any)
log_info impure (logger *Logger, msg string, fields ...any)
log_warn impure (logger *Logger, msg string, fields ...any)
log_error impure (logger *Logger, msg string, fields ...any)
logger_middleware impure (logger *Logger) Middleware

Tareas

Fase 1: Tipos y funciones core

  • 1.1 Crear tipos Logger, LogLevel, LogEntry en functions/infra/ con .md en types/infra/
  • 1.2 logger_new — crea *Logger con slog.NewJSONHandler o slog.NewTextHandler segun format
  • 1.3 logger_with — clona el logger y anade campos al slog.Logger interno via slog.With()
  • 1.4 log_debug, log_info, log_warn, log_error — delegan al slog.Logger interno con el nivel correspondiente
  • 1.5 Tests unitarios: verificar output JSON, niveles filtrados, campos inyectados

Fase 2: Middleware HTTP (requiere 0009)

  • 2.1 logger_middleware — wrappea http.Handler, loguea method, path, status, duration_ms al completar cada request
  • 2.2 Tests con httptest.NewRecorder
  • 2.3 fn index y verificar todas las funciones en registry.db

Ejemplo de uso

package main

import (
    "context"
    "net/http"
    "os"
    "os/signal"

    "github.com/fn_registry/functions/infra"
)

func main() {
    // Crear logger JSON a stdout, nivel info
    logger, _ := infra.LoggerNew(infra.LogLevelInfo, os.Stdout, "json")

    // Logger con contexto de app
    appLog := infra.LoggerWith(logger, map[string]any{
        "app":     "sqlite_api",
        "version": "1.0.0",
    })

    infra.LogInfo(appLog, "server starting", "port", 8484)
    // {"time":"2026-04-13T...","level":"INFO","msg":"server starting","app":"sqlite_api","version":"1.0.0","port":8484}

    // Logger por request con campos adicionales
    reqLog := infra.LoggerWith(appLog, map[string]any{"request_id": "abc-123"})
    infra.LogDebug(reqLog, "parsing body")  // filtrado: nivel < info
    infra.LogError(reqLog, "db query failed", "err", "connection refused", "table", "functions")

    // Middleware HTTP (compone con las funciones de 0009)
    routes := []infra.Route{
        {Method: "GET", Path: "/health", Handler: healthHandler},
    }
    mux := infra.HttpRouter(routes)

    middleware := infra.HttpMiddlewareChain(
        infra.LoggerMiddleware(appLog),
        infra.HttpCorsMiddleware([]string{"*"}, []string{"GET"}),
    )

    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    infra.HttpServe(":8484", middleware(mux), ctx)
    // Cada request produce:
    // {"time":"...","level":"INFO","msg":"http request","app":"sqlite_api","method":"GET","path":"/health","status":200,"duration_ms":1}
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    infra.HttpJsonResponse(w, 200, map[string]string{"status": "ok"})
}

Decisiones de diseno

  • log/slog de stdlib (Go 1.21+): Zero dependencies. slog ya resuelve JSON structured logging, niveles, campos key-value y handlers extensibles. No se justifica zerolog ni zap para el scope de este registry.
  • Logger como struct, no global: Cada app/componente crea su logger con su config. Sin slog.SetDefault() ni variables de paquete. Inyeccion explicita.
  • logger_with puro: slog.Logger.With() retorna un nuevo logger sin mutar el original. Esto permite crear loggers contextuales (por request, por componente) sin side effects.
  • Funciones de nivel separadas (log_info, log_error...): En vez de un unico Log(level, msg), funciones dedicadas por nivel. Mas legibles en el call site y mas buscables en el registry.
  • Formato configurable (JSON/text): JSON para produccion y pipelines de logs, text para desarrollo local. Un solo parametro en logger_new.

Riesgos

  • Adopcion gradual: Las apps existentes usan fmt.Println/log.Printf. Mitigado porque las funciones nuevas no rompen nada — las apps migran a su ritmo.
  • Middleware depende de 0009: logger_middleware usa el tipo Middleware de 0009. Si 0009 no esta implementado, la fase 2 se pospone. La fase 1 es independiente.
  • Proliferacion de funciones de log: 4 funciones de nivel + logger_new + logger_with = 6 funciones. Aceptable: cada una es trivial y atomica, preferible a una sola funcion con parametro de nivel.