net/http — ImmutableLog
Middleware para a biblioteca padrão do Go. Compatível com qualquer router que aceite http.Handler — gorilla/mux, chi, ServeMux nativo. Zero dependências além do stdlib.
Código do middleware
Crie um arquivo immutablelog/middleware.go no seu projeto. Usa apenas a biblioteca padrão do Go — nenhuma dependência externa.
package immutablelog
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// contextKey is unexported to avoid collisions with other packages.
type contextKey string
const EventNameKey contextKey = "imtbl.eventName"
type Options struct {
APIKey string
ServiceName string
Env string
APIURL string
SkipPaths []string
}
// responseWriter wraps http.ResponseWriter to capture the status code.
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Status() int {
if rw.statusCode == 0 {
return http.StatusOK
}
return rw.statusCode
}
// Middleware wraps an http.Handler and sends an audit event to ImmutableLog
// for every request not in SkipPaths. The event is sent asynchronously —
// it never blocks the HTTP response.
func Middleware(opts Options) func(http.Handler) http.Handler {
if opts.APIURL == "" {
opts.APIURL = "https://api.immutablelog.com"
}
if opts.APIKey == "" {
opts.APIKey = os.Getenv("IMTBL_API_KEY")
}
if opts.ServiceName == "" {
opts.ServiceName = os.Getenv("IMTBL_SERVICE_NAME")
}
if opts.Env == "" {
opts.Env = os.Getenv("IMTBL_ENV")
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip health checks and monitoring paths
for _, p := range opts.SkipPaths {
if strings.HasPrefix(r.URL.Path, p) {
next.ServeHTTP(w, r)
return
}
}
startedAt := time.Now()
// Buffer the request body so downstream handlers can still read it
var bodyBytes []byte
if r.Body != nil {
bodyBytes, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
// Wrap the response writer to capture the status code
wrapped := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wrapped, r)
// Fire-and-forget — never blocks the response
go emit(r, wrapped.Status(), time.Since(startedAt), bodyBytes, opts)
})
}
}
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, opts Options) {
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
requestID = fmt.Sprintf("%d", time.Now().UnixNano())
}
// Custom event name via context (set by a handler before responding)
eventName, _ := r.Context().Value(EventNameKey).(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, err := json.Marshal(p)
if err != nil {
return
}
evt := eventBody{
Payload: string(payloadJSON),
Meta: eventMeta{
Type: kind,
EventName: eventName,
Service: opts.ServiceName,
Env: opts.Env,
RequestID: requestID,
},
}
evtJSON, err := json.Marshal(evt)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
opts.APIURL+"/v1/events", bytes.NewBuffer(evtJSON))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+opts.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 no servidor
package main
import (
"net/http"
"github.com/yourorg/yourapp/immutablelog"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/payments", paymentsHandler)
mux.HandleFunc("/health", healthHandler)
audit := immutablelog.Middleware(immutablelog.Options{
APIKey: "iml_live_xxxx", // or use env IMTBL_API_KEY
ServiceName: "my-api",
Env: "production",
SkipPaths: []string{"/health", "/metrics"},
})
http.ListenAndServe(":8080", audit(mux))
}Como funciona
1. responseWriter wrapper
http.ResponseWriter não expõe o status code após ser enviado. O wrapper intercepta WriteHeader() e armazena o código para o middleware ler depois de next.ServeHTTP().
2. io.NopCloser
O body HTTP é um stream lido apenas uma vez. O middleware lê com io.ReadAll() e restaura com io.NopCloser() para que handlers downstream possam ler normalmente.
3. go emit()
O evento é enviado em uma goroutine com go emit(). A resposta ao cliente já foi enviada. context.WithTimeout(5s) evita que a goroutine fique presa indefinidamente.
4. contextKey tipada
A chave de contexto é um tipo privado (type contextKey string) para evitar colisões com outras bibliotecas que usam context.WithValue().
Evento customizado
Use context.WithValue() no handler para definir um nome semântico de evento. O middleware lê o valor após next.ServeHTTP().
package handlers
import (
"context"
"net/http"
"github.com/yourorg/yourapp/immutablelog"
)
func PaymentHandler(w http.ResponseWriter, r *http.Request) {
// Set custom event name — middleware reads it via context after ServeHTTP
ctx := context.WithValue(r.Context(), immutablelog.EventNameKey, "payment.created")
r = r.WithContext(ctx)
// ... process payment ...
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"ok":true}`))
}Compatibilidade com routers
O middleware retorna func(http.Handler) http.Handler — o padrão mais comum no ecossistema Go. Funciona com qualquer router que siga essa interface.
// Works with any http.Handler-compatible router:
// Standard library ServeMux
http.ListenAndServe(":8080", audit(mux))
// gorilla/mux
router := mux.NewRouter()
http.ListenAndServe(":8080", audit(router))
// chi
r := chi.NewRouter()
r.Use(func(next http.Handler) http.Handler {
return audit(next)
})
http.ListenAndServe(":8080", r)Recover de panics
Adicione um middleware de recover separado. O audit captura o status code mesmo em caso de panic, porque next.ServeHTTP() já retornou antes do recover.
// Place recover AFTER audit in the chain so audit sees the panic status.
// With net/http, panics bubble up — add a recover middleware explicitly.
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Chain: recover → audit → handler
// audit fires after the response is written (even after a panic)
handler := RecoverMiddleware(audit(mux))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.
