fad4006f60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
207 lines
7.9 KiB
Markdown
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.
|