Fiber — ImmutableLog
Middleware para o Fiber v2. Usa c.Next() para capturar status e erros, c.Locals() para eventos customizados e goroutine fire-and-forget para zero overhead. Inclui tratamento correto do buffer recycling do fasthttp.
Código do middleware
O Fiber usa fasthttp internamente (não net/http). O middleware copia o body antes de c.Next() porque o Fiber recicla buffers após cada requisição.
package immutablelog
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
const fiberEventNameKey = "imtbl.eventName"
const fiberTrailKey = "imtbl.trail"
type Config struct {
APIKey string
ServiceName string
Env string
APIURL string
SkipPaths []string
// Timezone do cliente — usado pelo dashboard para exibir os timestamps.
// Client timezone — used by the dashboard to render timestamps.
ClientTZ string
ClientOffsetMinutes int
}
// sanitizeTrail normaliza o immutable_trail conforme as regras do servidor:
// trim, nao-vazio, max 256 chars, sem ':' (o servidor responde 400 se violado).
func sanitizeTrail(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
v = strings.ReplaceAll(v, ":", "-")
if len(v) > 256 {
v = v[:256]
}
return v
}
// New returns a fiber.Handler middleware that sends an audit event to ImmutableLog.
// The event is sent asynchronously via a goroutine — never blocks the response.
func New(cfg Config) fiber.Handler {
if cfg.APIURL == "" {
cfg.APIURL = "https://api.immutablelog.com"
}
if cfg.APIKey == "" {
cfg.APIKey = os.Getenv("IMTBL_API_KEY")
}
if cfg.ServiceName == "" {
cfg.ServiceName = os.Getenv("IMTBL_SERVICE_NAME")
}
if cfg.Env == "" {
cfg.Env = os.Getenv("IMTBL_ENV")
}
if cfg.ClientTZ == "" {
cfg.ClientTZ = "America/Sao_Paulo"
}
if cfg.ClientOffsetMinutes == 0 {
cfg.ClientOffsetMinutes = -180
}
skipSet := make(map[string]struct{}, len(cfg.SkipPaths))
for _, p := range cfg.SkipPaths {
skipSet[p] = struct{}{}
}
return func(c *fiber.Ctx) error {
if _, skip := skipSet[c.Path()]; skip {
return c.Next()
}
startedAt := time.Now()
// Copy body BEFORE c.Next() — Fiber recycles the buffer after the request
bodyBytes := make([]byte, len(c.Body()))
copy(bodyBytes, c.Body())
// Execute all downstream handlers
err := c.Next()
elapsed := time.Since(startedAt)
status := c.Response().StatusCode()
// Custom event name set by a handler via c.Locals(fiberEventNameKey, "...")
eventName, _ := c.Locals(fiberEventNameKey).(string)
// Optional trail set via c.Locals(fiberTrailKey, "flow-123")
trail, _ := c.Locals(fiberTrailKey).(string)
go emit(c, status, elapsed, bodyBytes, eventName, trail, err, cfg)
return err
}
}
type eventPayload struct {
Method string `json:"method"`
Path string `json:"path"`
Status int `json:"status"`
LatencyMs int64 `json:"latency_ms"`
ClientIP string `json:"client_ip"`
UserAgent string `json:"user_agent,omitempty"`
RequestBodyHash string `json:"request_body_hash,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// Todos os valores de meta precisam ser strings (omitempty para opcionais).
// Every meta value must be a string (omitempty for optional fields).
type eventMeta struct {
Type string `json:"type"`
EventName string `json:"event_name"`
Service string `json:"service"`
Env string `json:"env"`
RequestID string `json:"request_id"`
ImmutableTrail string `json:"immutable_trail,omitempty"`
}
type eventBody struct {
Payload string `json:"payload"`
Meta eventMeta `json:"meta"`
}
func emit(c *fiber.Ctx, status int, elapsed time.Duration,
body []byte, customName string, customTrail string, handlerErr error, cfg Config) {
requestID := c.Get("X-Request-Id")
if requestID == "" {
requestID = fmt.Sprintf("%d", time.Now().UnixNano())
}
eventName := customName
if eventName == "" {
eventName = strings.ToLower(c.Method()) + "." +
strings.ReplaceAll(strings.TrimPrefix(c.Path(), "/"), "/", ".")
}
// Trilha imutavel: c.Locals(fiberTrailKey, ...) -> header X-Imtbl-Trail.
trail := sanitizeTrail(customTrail)
if trail == "" {
trail = sanitizeTrail(c.Get("X-Imtbl-Trail"))
}
kind := "success"
if status >= 400 || handlerErr != nil {
kind = "error"
} else if status >= 300 {
kind = "info"
}
p := eventPayload{
Method: c.Method(),
Path: c.Path(),
Status: status,
LatencyMs: elapsed.Milliseconds(),
ClientIP: c.IP(),
UserAgent: string(c.Request().Header.UserAgent()),
}
if len(body) > 0 {
p.RequestBodyHash = sha256Hex(body)
}
if handlerErr != nil {
p.ErrorMessage = handlerErr.Error()
}
payloadJSON, _ := json.Marshal(p)
evt := eventBody{
Payload: string(payloadJSON),
Meta: eventMeta{
Type: kind,
EventName: eventName,
Service: cfg.ServiceName,
Env: cfg.Env,
RequestID: requestID,
ImmutableTrail: trail,
},
}
evtJSON, _ := json.Marshal(evt)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
cfg.APIURL+"/v1/events", bytes.NewBuffer(evtJSON))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
req.Header.Set("Idempotency-Key", eventName+"-"+requestID)
req.Header.Set("Request-Id", requestID)
req.Header.Set("X-Client-TZ", cfg.ClientTZ)
req.Header.Set("X-Client-Offset-Minutes", fmt.Sprintf("%d", cfg.ClientOffsetMinutes))
if trail != "" {
req.Header.Set("X-Imtbl-Trail", trail)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}Registro global
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/yourorg/yourapp/immutablelog"
)
func main() {
app := fiber.New()
// Register globally — applies to all routes
app.Use(immutablelog.New(immutablelog.Config{
APIKey: "iml_live_xxxx",
ServiceName: "my-api",
Env: "production",
SkipPaths: []string{"/health", "/metrics"},
}))
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true})
})
app.Post("/payments", paymentsHandler)
app.Listen(":8080")
}Como funciona
1. c.Next() + c.Response().StatusCode()
c.Next() executa toda a cadeia de handlers. Após retornar, c.Response().StatusCode() contém o status code final definido pelos handlers.
2. Captura de erro por retorno
Em Go/Fiber, erros são retornados — não lançados. O middleware captura o error retornado por c.Next() e o inclui no evento se não for nil, junto com status 500.
3. c.IP()
c.IP() do Fiber considera X-Forwarded-For automaticamente quando ProxyHeader está configurado. Configure via fiber.Config{ProxyHeader: "X-Forwarded-For"} na inicialização.
Evento customizado
Use c.Locals() para passar o nome do evento do handler para o middleware. Locals persiste durante todo o ciclo de vida da requisição.
func paymentsHandler(c *fiber.Ctx) error {
// Set custom event name via Locals — middleware reads after c.Next()
c.Locals("imtbl.eventName", "payment.created")
var req PaymentRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
// ... process payment ...
return c.Status(201).JSON(fiber.Map{"ok": true, "payment_id": "pay_123"})
}Trilha imutável (immutable_trail)
A trilha agrupa eventos relacionados em uma mesma linha do tempo auditável. Defina via c.Locals("imtbl.trail", ...) no handler ou propague entre serviços com o header X-Imtbl-Trail.
func runFlowHandler(c *fiber.Ctx) error {
c.Locals("imtbl.trail", "flow-"+c.Params("id"))
// ... business logic ...
return c.JSON(fiber.Map{"ok": true})
}A trilha não pode ser vazia, exceder 256 caracteres ou conter ":" — violações retornam 400 invalid_immutable_trail. O sanitizeTrail corrige o valor antes do envio.
Body recycling
O Fiber (fasthttp) reutiliza buffers de request/response para alta performance. É necessário copiar o body antes de c.Next() para garantir que o hash seja calculado sobre os dados corretos.
// Fiber (fasthttp) recycles request buffers for high performance.
// Always copy the body BEFORE calling c.Next():
bodyBytes := make([]byte, len(c.Body()))
copy(bodyBytes, c.Body())
err := c.Next()
// After this, c.Body() may be empty or contain data from another request.
// Use bodyBytes for hashing — it has a stable copy.ErrorHandler
O ErrorHandler global do Fiber processa erros retornados pelos handlers. O middleware de audit captura o erro ANTES do ErrorHandler — status e error_message ficam no evento de auditoria.
// Fiber's global ErrorHandler processes errors returned by handlers.
// The audit middleware captures the error returned from c.Next()
// BEFORE ErrorHandler runs — it's included in the event as error_message.
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{"error": err.Error()})
},
})
// Handler that returns an error:
app.Get("/fail", func(c *fiber.Ctx) error {
return fiber.NewError(500, "something went wrong")
// audit middleware captures: status=500, error_message="something went wrong"
})Esta documentação reflete o comportamento atual da integração. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.
