Go
Guia completo de integração com o ImmutableLog em Go. Middleware automático para net/http, Gin e Fiber — ou cliente HTTP direto para workers, Kafka consumers e jobs agendados. Goroutines fire-and-forget garantem zero overhead na latência das respostas.
Middlewares
Integração automática via middleware — toda requisição HTTP é capturada sem modificar nenhum handler. Clique no framework para ver o código completo.
Middleware para a biblioteca padrão. Zero dependências — compatível com gorilla/mux, chi e qualquer router http.Handler.
func Middleware(opts Options) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startedAt := time.Now()
wrapped := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wrapped, r)
go emit(r, wrapped.Status(), time.Since(startedAt), opts)
})
}
}Ver documentação completa →
gin.HandlerFunc com c.Next(), c.Writer.Status() e c.Set() para eventos customizados. Suporte a route groups e abort de auth.
func Middleware(cfg Config) gin.HandlerFunc {
return func(c *gin.Context) {
startedAt := time.Now()
c.Next()
elapsed := time.Since(startedAt)
status := c.Writer.Status()
eventName, _ := c.Get("imtbl.eventName")
go emit(c.Request, status, elapsed, eventName, cfg)
}
}Ver documentação completa →
fiber.Handler com fasthttp. Copia o body antes de c.Next() para evitar buffer recycling. Captura erros Go retornados pela cadeia.
func New(cfg Config) fiber.Handler {
return func(c *fiber.Ctx) error {
body := make([]byte, len(c.Body()))
copy(body, c.Body())
startedAt := time.Now()
err := c.Next()
status := c.Response().StatusCode()
go emit(c, status, time.Since(startedAt), body, err, cfg)
return err
}
}Ver documentação completa →
Cliente HTTP direto
Use o Client para enviar eventos diretamente ao ImmutableLog sem depender de um framework web. Ideal para workers, consumidores Kafka, cron jobs e qualquer código Go que precise registrar eventos.
package immutablelog
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
type Client struct {
APIKey string
ServiceName string
Env string
APIURL string
httpClient *http.Client
}
func NewClient(apiKey, serviceName, env string) *Client {
return &Client{
APIKey: apiKey,
ServiceName: serviceName,
Env: env,
APIURL: "https://api.immutablelog.com",
httpClient: &http.Client{Timeout: 5 * time.Second},
}
}
type SendOpts struct {
EventName string
Type string // "success" | "info" | "error"
Payload any // will be marshaled to JSON string
Trail string // optional: immutable_trail (groups related events)
}
// sanitizeTrail normaliza o immutable_trail (trim, max 256, sem ':').
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
}
// Send sends a single event. Returns immediately — use SendAsync for fire-and-forget.
func (c *Client) Send(ctx context.Context, opts SendOpts) error {
payloadJSON, err := json.Marshal(opts.Payload)
if err != nil {
return fmt.Errorf("marshal payload: %w", err)
}
requestID := fmt.Sprintf("%d", time.Now().UnixNano())
trail := sanitizeTrail(opts.Trail)
// Todos os valores de meta precisam ser strings.
meta := map[string]string{
"type": opts.Type,
"event_name": opts.EventName,
"service": c.ServiceName,
"env": c.Env,
"request_id": requestID,
}
if trail != "" {
meta["immutable_trail"] = trail
}
body := map[string]any{
"payload": string(payloadJSON),
"meta": meta,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.APIURL+"/v1/events", bytes.NewBuffer(bodyJSON))
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
req.Header.Set("Idempotency-Key", opts.EventName+"-"+requestID)
req.Header.Set("Request-Id", requestID)
req.Header.Set("X-Client-TZ", "America/Sao_Paulo")
req.Header.Set("X-Client-Offset-Minutes", "-180")
if trail != "" {
req.Header.Set("X-Imtbl-Trail", trail)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("send event: %w", err)
}
defer resp.Body.Close()
return nil
}
// SendAsync sends the event in a goroutine. Never blocks the caller.
func (c *Client) SendAsync(opts SendOpts) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = c.Send(ctx, opts)
}()
}
// Usage:
// client := immutablelog.NewClient(os.Getenv("IMTBL_API_KEY"), "my-service", "production")
//
// client.SendAsync(immutablelog.SendOpts{
// EventName: "user.created",
// Type: "success",
// Payload: map[string]any{"user_id": "usr_123", "plan": "pro"},
// })SendAsync() dispara uma goroutine e retorna imediatamente. Use Send() quando precisar do erro de retorno. Ambos respeitam o context.Context para cancelamento e timeout.
Retry com backoff exponencial
Para produção de alta disponibilidade, adicione retry com backoff exponencial. O ImmutableLog retorna 202 em sucesso — nunca faça retry em 429 (limite mensal atingido).
// SendWithRetry retries on transient errors (not on 429).
func (c *Client) SendWithRetry(ctx context.Context, opts SendOpts, maxAttempts int) error {
delay := 500 * time.Millisecond
for attempt := 0; attempt < maxAttempts; attempt++ {
err := c.Send(ctx, opts)
if err == nil {
return nil
}
// Do not retry on context cancellation
if ctx.Err() != nil {
return ctx.Err()
}
if attempt+1 < maxAttempts {
time.Sleep(delay)
delay *= 2 // exponential backoff: 500ms → 1s → 2s
}
}
return fmt.Errorf("failed after %d attempts", maxAttempts)
}Envio em lote (batch)
Em workers de alta frequência, envie múltiplos eventos em paralelo com sync.WaitGroup. Cada evento é enviado individualmente — o WaitGroup aguarda todos completarem.
// SendBatch sends multiple events concurrently using goroutines.
// Uses a WaitGroup to wait for all sends to complete.
func (c *Client) SendBatch(ctx context.Context, events []SendOpts) []error {
errs := make([]error, len(events))
var wg sync.WaitGroup
for i, evt := range events {
wg.Add(1)
go func(idx int, e SendOpts) {
defer wg.Done()
errs[idx] = c.Send(ctx, e)
}(i, evt)
}
wg.Wait()
return errs
}
// Usage:
ctx := context.Background()
errs := client.SendBatch(ctx, []immutablelog.SendOpts{
{EventName: "order.created", Type: "success", Payload: map[string]any{"order_id": "ord_1"}},
{EventName: "payment.captured", Type: "success", Payload: map[string]any{"amount": 9900}},
{EventName: "email.sent", Type: "info", Payload: map[string]any{"template": "welcome"}},
})Hash de dados sensíveis
Nunca envie dados pessoais brutos (e-mail, CPF, IP) para o ImmutableLog. Use SHA-256 para gerar um digest determinístico — rastreável sem expor o dado original.
import (
"crypto/sha256"
"fmt"
)
// Hash sensitive data before sending — never log raw PII.
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}
// Usage in event payload:
payload := map[string]any{
"user_id": "usr_123",
"email_hash": sha256Hex([]byte("user@example.com")),
"ip_hash": sha256Hex([]byte("192.168.1.1")),
}Campos de meta
Além do payload, o objeto meta carrega os campos que o SIEM usa para normalizar (ECS) e enriquecer o evento. Todos os valores de meta são strings.
| Campo em meta | O que faz no SIEM |
|---|---|
event_name | O rótulo do evento (ex.: http.GET.auth-me-user, Pagamento erro) — o identificador que aparece na listagem. Vira event.action e deriva event.category (ex.: nome com login → authentication). |
client_ip | Vira source.ip com precedência máxima e proveniência client_asserted. Ver a seção abaixo. |
immutable_trail | Vira imtbl.immutable_trail — agrupa eventos relacionados para investigação (ex.: um pedido, um usuário). |
type | Severidade do evento: error / warning / info / success. Colore o badge na listagem. (event_type é aceito como sinônimo.) |
service, env, request_id | Metadados: indexados e consultáveis no painel (busca/filtros). |
client_ip — IP do usuário final
O problema que resolve
Entre o navegador e o core há proxies/ALB. O IP que o core observa na conexão é o do salto anterior (o backend do cliente ou um proxy da AWS), não o do usuário. Sem ação, source.ip seria o IP do proxy — inútil para geo, threat e detecção por IP. (Exemplo real: o XFF chegava 44.192.13.3 (AWS) enquanto o usuário era 179.110.4.205.)
A solução
O backend do cliente é o único que enxerga o IP real do navegador — então ele encaminha esse IP em meta.client_ip ao chamar POST /v1/events.
Como o SIEM trata
- Valida que é um IP (v4/v6). Um valor não-IP é ignorado — não derruba o evento.
- Escreve em
source.ipcom precedência máxima (ver ordem abaixo). - Carimba
imtbl.source_ip_origin = "client_asserted"— a UI mostra o selo “IP do usuário”. - Alimenta GeoIP, threat intel e regras de detecção por IP.
client_ipX-Forwarded-For (1º hop)X-Real-IPIP de conexãoProveniência e confiança
client_ip é asserido pelo tenant — o backend do cliente afirma o IP, e é spoofável por quem controla aquele backend. Porém é auto-contido ao próprio tenant (não cruza a fronteira de outro tenant). Um hit de threat/geo sobre um client_asserted é uma alegação, não uma observação de borda.
Como obter o IP no seu backend
Pegue o primeiro IP do X-Forwarded-For (o mais próximo do usuário); no fallback, use o IP de conexão.
func clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.TrimSpace(strings.Split(xff, ",")[0]) // primeiro hop
}
host, _, _ := net.SplitHostPort(r.RemoteAddr)
return host
}
meta := map[string]string{"event_name": "user.login", "client_ip": clientIP(r)}Exemplo de payload
{
"payload": "{\"user\":\"bob\",\"action\":\"login\"}",
"meta": {
"event_name": "user.login",
"client_ip": "179.110.4.205",
"immutable_trail": "user-bob"
}
}Nota de PII: client_ip é PII e vai selado no bloco imutável (permanente). Decida conscientemente entre evidência forense e “direito ao esquecimento” antes de enviá-lo.
Esta documentação reflete o comportamento atual da API. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.
