Fiber — ImmutableLog
Middleware for Fiber v2. Uses c.Next() to capture status and errors, c.Locals() for custom events, and goroutine fire-and-forget for zero overhead. Includes correct handling of fasthttp buffer recycling.
Middleware code
Fiber uses fasthttp internally (not net/http). The middleware copies the body before c.Next() because Fiber recycles buffers after each request.
package immutablelog
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
const fiberEventNameKey = "imtbl.eventName"
type Config struct {
APIKey string
ServiceName string
Env string
APIURL string
SkipPaths []string
}
// 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")
}
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)
go emit(c, status, elapsed, bodyBytes, eventName, 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"`
}
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"`
}
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, 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(), "/"), "/", ".")
}
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,
},
}
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)
req.Header.Set("Idempotency-Key", requestID)
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)
}Global registration
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")
}How it works
1. c.Next() + c.Response().StatusCode()
c.Next() executes the full handler chain. After it returns, c.Response().StatusCode() contains the final status code set by the handlers.
2. Error capture via return
In Go/Fiber, errors are returned — not thrown. The middleware captures the error returned by c.Next() and includes it in the event if not nil, along with status 500.
3. c.IP()
Fiber's c.IP() considers X-Forwarded-For automatically when ProxyHeader is configured. Set it via fiber.Config{ProxyHeader: "X-Forwarded-For"} at startup.
Custom event
Use c.Locals() to pass the event name from the handler to the middleware. Locals persists for the full request lifecycle.
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"})
}Body recycling
Fiber (fasthttp) reuses request/response buffers for high performance. You must copy the body before c.Next() to ensure the hash is computed over the correct data.
// 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
Fiber's global ErrorHandler processes errors returned by handlers. The audit middleware captures the error BEFORE ErrorHandler runs — status and error_message are included in the audit event.
// 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"
})This documentation reflects the current integration behavior. For questions or advanced integrations, contact the support team.
