Pular para o conteúdo principal
ImmutableLog logo
Voltar
Gonet/http

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.

go
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

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

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

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

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