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

207 lines
7.9 KiB
Markdown

---
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.