fix(fn-run): propagar stdout/stderr de bash functions library-style #1
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user