Gin — ImmutableLog
Middleware para o framework Gin. Usa c.Next() para capturar status após todos os handlers, goroutine fire-and-forget para não bloquear a resposta, e c.Set() para eventos customizados.
Código do middleware
Crie um pacote immutablelog no seu projeto. Retorna gin.HandlerFunc e pode ser registrado com r.Use() globalmente ou em grupos de rotas.
package immutablelog
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const ginEventNameKey = "imtbl.eventName"
type Config struct {
APIKey string
ServiceName string
Env string
APIURL string
SkipPaths []string
}
// Middleware returns a gin.HandlerFunc that sends an audit event to ImmutableLog
// for every request. The event is sent asynchronously via a goroutine.
func Middleware(cfg Config) gin.HandlerFunc {
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 *gin.Context) {
if _, skip := skipSet[c.Request.URL.Path]; skip {
c.Next()
return
}
startedAt := time.Now()
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
c.Next() // execute all handlers
elapsed := time.Since(startedAt)
status := c.Writer.Status()
// Custom event name set by a handler via c.Set(ginEventNameKey, "...")
eventName, _ := c.Get(ginEventNameKey)
go emit(c.Request, status, elapsed, bodyBytes, eventName, cfg)
}
}
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"`
}
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(r *http.Request, status int, elapsed time.Duration,
body []byte, customName any, cfg Config) {
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
requestID = fmt.Sprintf("%d", time.Now().UnixNano())
}
eventName, _ := customName.(string)
if eventName == "" {
eventName = strings.ToLower(r.Method) + "." +
strings.ReplaceAll(strings.TrimPrefix(r.URL.Path, "/"), "/", ".")
}
kind := "success"
if status >= 400 {
kind = "error"
} else if status >= 300 {
kind = "info"
}
p := eventPayload{
Method: r.Method,
Path: r.URL.Path,
Status: status,
LatencyMs: elapsed.Milliseconds(),
ClientIP: clientIP(r),
UserAgent: r.UserAgent(),
}
if len(body) > 0 {
p.RequestBodyHash = sha256Hex(body)
}
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 clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.SplitN(xff, ",", 2)[0]
}
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
return r.RemoteAddr
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}Registro global
package main
import (
"github.com/gin-gonic/gin"
"github.com/yourorg/yourapp/immutablelog"
)
func main() {
r := gin.Default()
// Register globally — applies to all routes
r.Use(immutablelog.Middleware(immutablelog.Config{
APIKey: "iml_live_xxxx",
ServiceName: "my-api",
Env: "production",
SkipPaths: []string{"/health", "/metrics", "/ping"},
}))
r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
r.POST("/payments", paymentsHandler)
r.GET("/users/:id", usersHandler)
r.Run(":8080")
}Como funciona
1. c.Next()
c.Next() executa toda a cadeia de handlers. O código após c.Next() no middleware roda com a resposta já preparada — c.Writer.Status() tem o código correto.
2. go emit()
O evento é disparado em uma goroutine separada. Gin já enviou os headers ao cliente — o audit não acrescenta latência perceptível à resposta.
3. c.Get() / c.Set()
Gin armazena valores no contexto da request via c.Set(). O middleware lê imtbl.eventName após c.Next(), quando o handler já definiu o valor.
Evento customizado
Use c.Set("imtbl.eventName", "...") no handler para definir um nome semântico. O middleware lê o valor depois de c.Next().
func paymentsHandler(c *gin.Context) {
// Set custom event name — middleware reads after c.Next() returns
c.Set("imtbl.eventName", "payment.created")
var req PaymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// ... process payment ...
c.JSON(201, gin.H{"ok": true, "payment_id": "pay_123"})
}Route groups
Aplique o middleware em um grupo de rotas para auditar apenas endpoints específicos, sem afetar rotas públicas.
// Apply middleware only to a specific route group
api := r.Group("/api/v1")
api.Use(immutablelog.Middleware(immutablelog.Config{
APIKey: os.Getenv("IMTBL_API_KEY"),
ServiceName: "api-v1",
Env: os.Getenv("APP_ENV"),
}))
api.POST("/payments", paymentsHandler)
api.GET("/users", usersHandler)
// Routes outside the group are NOT audited
r.GET("/health", healthHandler)Abort e autenticação
Quando um middleware chama c.Abort(), o audit ainda captura o status. Registre o audit ANTES do auth para capturar tentativas não autorizadas.
// When a handler calls c.Abort(), the audit middleware still runs
// because c.Next() already returned with the aborted status.
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("Authorization") == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return // abort — audit still sees status 401
}
c.Next()
}
}
// Register audit BEFORE auth to capture unauthorized attempts
r.Use(immutablelog.Middleware(cfg)) // runs first, calls c.Next()
r.Use(AuthMiddleware()) // may abort — audit captures 401Esta 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.
