Created
September 23, 2025 12:48
-
-
Save brbarmex/342868a67528ccb3d83b444272c4e039 to your computer and use it in GitHub Desktop.
datadog metric custom middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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