fix(fn-run): propagar stdout/stderr de bash functions library-style #1

Open
dataforge wants to merge 537 commits from auto/0077-fn-run-bash-mudo into master
4 changed files with 186 additions and 0 deletions
Showing only changes of commit ae22787e60 - Show all commits
+54
View File
@@ -0,0 +1,54 @@
package infra
import (
"fmt"
"io"
"log/slog"
"os"
)
// LoggerNew crea un Logger con nivel, destino y formato configurables.
// format debe ser "json" o "text". Si output es nil se usa os.Stderr.
// Retorna error si el formato no es valido.
func LoggerNew(level LogLevel, output io.Writer, format string) (*Logger, error) {
if output == nil {
output = os.Stderr
}
slogLevel := toSlogLevel(level)
opts := &slog.HandlerOptions{Level: slogLevel}
var handler slog.Handler
switch format {
case "json":
handler = slog.NewJSONHandler(output, opts)
case "text":
handler = slog.NewTextHandler(output, opts)
default:
return nil, fmt.Errorf("logger_new: formato invalido %q, usa \"json\" o \"text\"", format)
}
return &Logger{
Level: level,
Output: output,
Format: format,
Fields: map[string]any{},
inner: slog.New(handler),
}, nil
}
// toSlogLevel convierte LogLevel a slog.Level.
func toSlogLevel(level LogLevel) slog.Level {
switch level {
case LogLevelDebug:
return slog.LevelDebug
case LogLevelInfo:
return slog.LevelInfo
case LogLevelWarn:
return slog.LevelWarn
case LogLevelError:
return slog.LevelError
default:
return slog.LevelInfo
}
}
+44
View File
@@ -0,0 +1,44 @@
---
name: logger_new
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func LoggerNew(level LogLevel, output io.Writer, format string) (*Logger, error)"
description: "Crea un Logger estructurado sobre log/slog con nivel, destino y formato configurables. Formato soportado: json o text. Si output es nil cae en os.Stderr."
tags: [logging, log, slog, logger, infra]
uses_functions: []
uses_types: [Logger_go_infra, LogLevel_go_infra]
returns: [Logger_go_infra]
returns_optional: true
error_type: "error_go_core"
imports: [fmt, io, log/slog, os]
params:
- name: level
desc: "nivel minimo de log (LogLevelDebug, LogLevelInfo, LogLevelWarn o LogLevelError)"
- name: output
desc: "destino de los logs (os.Stdout, os.Stderr, un archivo, bytes.Buffer). Si es nil se usa os.Stderr"
- name: format
desc: "formato de los logs: \"json\" para maquina o \"text\" para desarrollo local"
output: "Logger listo para usar con LogInfo/LogWarn/... o error si el formato no es valido"
tested: true
tests: ["crea logger JSON valido", "crea logger text valido", "rechaza formato invalido", "output nil cae en os.Stderr sin panic", "emite JSON valido al escribir", "filtra mensajes debajo del nivel configurado"]
test_file_path: "functions/infra/logger_test.go"
file_path: "functions/infra/logger_new.go"
---
## Ejemplo
```go
logger, err := LoggerNew(LogLevelInfo, os.Stdout, "json")
if err != nil {
log.Fatal(err)
}
LogInfo(logger, "server starting", "port", 8484)
// {"time":"...","level":"INFO","msg":"server starting","port":8484}
```
## Notas
Funcion impura — internamente construye `slog.NewJSONHandler` o `slog.NewTextHandler` segun el formato y lo envuelve en `slog.New()`. El campo privado `inner` del Logger es el `*slog.Logger` real. Cada Logger es inmutable una vez creado: para anadir campos usar `LoggerWith`, que retorna una copia.
+46
View File
@@ -0,0 +1,46 @@
package infra
import "sort"
// LoggerWith retorna una copia del Logger con campos adicionales.
// No muta el logger original — los campos se apilan sobre los ya existentes.
// Funcion pura: misma entrada produce siempre la misma salida sin I/O.
func LoggerWith(logger *Logger, fields map[string]any) *Logger {
if logger == nil {
return nil
}
// Combinar fields existentes + nuevos (los nuevos tienen precedencia)
combined := make(map[string]any, len(logger.Fields)+len(fields))
for k, v := range logger.Fields {
combined[k] = v
}
for k, v := range fields {
combined[k] = v
}
// Convertir a args key-value ordenados para slog.With (orden determinista)
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
args := make([]any, 0, len(keys)*2)
for _, k := range keys {
args = append(args, k, fields[k])
}
var inner = logger.inner
if inner != nil && len(args) > 0 {
inner = inner.With(args...)
}
return &Logger{
Level: logger.Level,
Output: logger.Output,
Format: logger.Format,
Fields: combined,
inner: inner,
}
}
+42
View File
@@ -0,0 +1,42 @@
---
name: logger_with
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func LoggerWith(logger *Logger, fields map[string]any) *Logger"
description: "Retorna una copia del Logger con campos contextuales adicionales. No muta el logger original — los campos se apilan sobre los existentes y aparecen en cada entrada del nuevo logger."
tags: [logging, log, slog, logger, context, pure, infra]
uses_functions: []
uses_types: [Logger_go_infra]
returns: [Logger_go_infra]
returns_optional: false
error_type: ""
imports: [sort]
params:
- name: logger
desc: "Logger base a clonar. Si es nil retorna nil"
- name: fields
desc: "mapa de campos key-value a anadir al logger (ej: {\"app\": \"api\", \"request_id\": \"abc\"})"
output: "nuevo Logger con los fields combinados (los del parametro tienen precedencia sobre los del logger base)"
tested: true
tests: ["anade campos al logger", "no muta el logger original", "apila fields sobre un logger ya contextualizado", "retorna nil si recibe nil"]
test_file_path: "functions/infra/logger_test.go"
file_path: "functions/infra/logger_with.go"
---
## Ejemplo
```go
base, _ := LoggerNew(LogLevelInfo, os.Stdout, "json")
appLog := LoggerWith(base, map[string]any{"app": "sqlite_api", "version": "1.0.0"})
reqLog := LoggerWith(appLog, map[string]any{"request_id": "abc-123"})
LogInfo(reqLog, "evento")
// {"...","msg":"evento","app":"sqlite_api","version":"1.0.0","request_id":"abc-123"}
```
## Notas
Funcion pura — no hace I/O, no muta estado. Internamente llama a `slog.Logger.With()` que ya retorna un nuevo logger. Los campos se pasan en orden alfabetico a `With()` para que el output sea determinista (util para tests). El campo `Fields` del `*Logger` mantiene la union combinada (base + nuevos) para permitir inspeccion programatica.