Skip to content

Instantly share code, notes, and snippets.

@brbarmex
Created September 23, 2025 12:48
Show Gist options
  • Select an option

  • Save brbarmex/342868a67528ccb3d83b444272c4e039 to your computer and use it in GitHub Desktop.

Select an option

Save brbarmex/342868a67528ccb3d83b444272c4e039 to your computer and use it in GitHub Desktop.
datadog metric custom middleware
package main
import (
"context"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/DataDog/datadog-go/v5/statsd"
"github.com/gin-gonic/gin"
gindd "gopkg.in/DataDog/dd-trace-go.v2/contrib/gin-gonic/gin"
"gopkg.in/DataDog/dd-trace-go.v2/ddtrace/tracer"
)
var (
sd *statsd.Client
inFlight int64 // gauge de saturação
)
// bodyLogWriter conta bytes de resposta
type bodyLogWriter struct {
gin.ResponseWriter
bytesWritten int64
}
func (w *bodyLogWriter) Write(b []byte) (int, error) {
n, err := w.ResponseWriter.Write(b)
atomic.AddInt64(&w.bytesWritten, int64(n))
return n, err
}
func (w *bodyLogWriter) WriteString(s string) (int, error) {
n, err := w.ResponseWriter.WriteString(s)
atomic.AddInt64(&w.bytesWritten, int64(n))
return n, err
}
// GoldenSignalsMiddleware emite métricas core via DogStatsD
func GoldenSignalsMiddleware(service string) gin.HandlerFunc {
env := getenv("DD_ENV", "dev")
version := getenv("DD_VERSION", "0.0.0")
baseTags := []string{
"service:" + service,
"env:" + env,
"version:" + version,
"runtime:go",
}
return func(c *gin.Context) {
start := time.Now()
// saturação: in-flight
cur := atomic.AddInt64(&inFlight, 1)
defer atomic.AddInt64(&inFlight, -1)
// wrap writer p/ contar bytes out
blw := &bodyLogWriter{ResponseWriter: c.Writer}
c.Writer = blw
// request info (antes do next)
method := c.Request.Method
host := c.Request.Host
// path “template” (rota) p/ reduzir cardinalidade
route := c.FullPath()
if route == "" {
// fallback quando ainda não matchou rota
route = sanitizePath(c.Request.URL.Path)
}
bytesIn := c.Request.ContentLength
if bytesIn < 0 {
bytesIn = 0
}
// opcional: medir queue_time se seu LB/proxy manda header
// formatos aceitos: "t=unix_ms" ou "unix_ms"
queueMs := parseQueueHeaderMs(c.Request.Header.Get("X-Request-Start"))
// segue o fluxo
c.Next()
// depois do handler
status := c.Writer.Status()
duration := time.Since(start)
tags := append(baseTags,
"method:"+method,
"host:"+host, // cuidado com cardinalidade se host variar muito
"route:"+route, // use template (ex: /users/:id)
"status:"+itoa(status), // "200", "500", etc.
"outcome:"+outcome(status),
)
// -------- Golden Signals --------
// Tráfego (requests):
_ = sd.Incr("http.server.requests", tags, 1)
// Latência: use Distribution p/ p50/p90/p95/p99 no Datadog
_ = sd.Distribution("http.server.request_duration_ms", float64(duration.Milliseconds()), tags, 1)
// Bytes in/out (tráfego)
if bytesIn > 0 {
_ = sd.Count("http.server.bytes_in", bytesIn, tags, 1)
}
_ = sd.Count("http.server.bytes_out", blw.bytesWritten, tags, 1)
// Erros (rate por família de status)
if status >= 500 {
_ = sd.Incr("http.server.errors", tags, 1)
}
// Saturação (gauge de in-flight)
_ = sd.Gauge("http.server.in_flight", float64(cur), tags, 1)
// Opcional: fila antes de chegar no app (ex: ALB, Nginx, Istio, sidecar)
if queueMs > 0 {
_ = sd.Distribution("http.server.queue_time_ms", float64(queueMs), tags, 1)
}
// Opcional: expor SLA (apdex) como contadores
if duration <= 100*time.Millisecond {
_ = sd.Incr("http.server.apdex_satisfying", tags, 1)
} else if duration <= 500*time.Millisecond {
_ = sd.Incr("http.server.apdex_tolerating", tags, 1)
} else {
_ = sd.Incr("http.server.apdex_frustrated", tags, 1)
}
}
}
func main() {
// -------- Tracer Datadog v2 --------
tracer.Start(
tracer.WithEnv(getenv("DD_ENV", "dev")),
tracer.WithService(getenv("DD_SERVICE", "my-gin-api")),
tracer.WithServiceVersion(getenv("DD_VERSION", "0.0.0")),
// se necessário: tracer.WithAgentAddr("host:port"),
// tracer.WithRuntimeMetrics() // útil p/ CPU/mem GC
)
defer tracer.Stop()
// -------- StatsD (DogStatsD) --------
var err error
sd, err = statsd.New(getenv("DD_AGENT_HOST", "127.0.0.1")+":8125",
statsd.WithNamespace(""),
statsd.WithMaxMessagesPerPayload(16),
statsd.WithBufferPoolSize(4096),
statsd.WithContext(context.Background()),
)
if err != nil {
log.Fatalf("statsd: %v", err)
}
defer sd.Close()
r := gin.New()
// middleware de tracing oficial do dd para Gin
r.Use(gindd.Middleware(getenv("DD_SERVICE", "my-gin-api"),
gindd.WithAnalytics(true), // opcional
))
// seu middleware de métricas (golden signals)
r.Use(GoldenSignalsMiddleware(getenv("DD_SERVICE", "my-gin-api")))
// middlewares padrão
r.Use(gin.Recovery())
// rotas exemplo
r.GET("/health", func(c *gin.Context) { c.Status(http.StatusOK) })
r.GET("/users/:id", func(c *gin.Context) {
time.Sleep(75 * time.Millisecond)
c.JSON(200, gin.H{"id": c.Param("id")})
})
addr := getenv("ADDR", ":8080")
log.Printf("listening on %s", addr)
if err := r.Run(addr); err != nil {
log.Fatal(err)
}
}
// ---------- helpers ----------
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func outcome(status int) string {
switch {
case status >= 500:
return "error"
case status >= 400:
return "client_error"
default:
return "success"
}
}
func itoa(i int) string { return strconv.Itoa(i) }
// reduz cardinalidade de paths “concretos”
func sanitizePath(p string) string {
// transforme IDs óbvios em :id pra evitar explosão de séries
parts := strings.Split(p, "/")
for i := range parts {
if isLikelyID(parts[i]) {
parts[i] = ":id"
}
}
return strings.Join(parts, "/")
}
func isLikelyID(s string) bool {
// heurística simples: números longos, UUIDs, hex grande etc.
if len(s) >= 8 && (isHex(s) || isNumeric(s)) {
return true
}
return false
}
func isNumeric(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return len(s) > 0
}
func isHex(s string) bool {
for _, r := range s {
if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') && r != '-' {
return false
}
}
return true
}
// Suporta "t=1695750000000" ou "1695750000000"
func parseQueueHeaderMs(h string) int64 {
h = strings.TrimSpace(h)
if h == "" {
return 0
}
h = strings.TrimPrefix(h, "t=")
ms, err := strconv.ParseInt(h, 10, 64)
if err != nil {
return 0
}
return ms
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment