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