Pular para o conteúdo principal
ImmutableLog logo
Voltar
GoGin

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.

go
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

go
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().

go
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.

go
// 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.

go
// 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 401

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.