Files
fn_registry/dev/issues/completed/0019-structured-logging.md

7.9 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0019 Structured Logging Go completado feature
multi-app media
2026-05-17 2026-05-17

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.