--- id: "0019" title: "Structured Logging Go" status: completado type: feature domain: [] scope: multi-app priority: media depends: [] blocks: [] related: [] created: 2026-05-17 updated: 2026-05-17 tags: [] --- # 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 ```go // 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 ```go 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.