ff55d5de83
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m58s
The backend recorded 172.19.0.9 — the gateway's own docker connection address — as the client IP for all users: the account's last-login IP shown in the admin console, and it never reached the backend access log. The gateway forwarded the client IP as X-Forwarded-For only on chat/feedback calls; every other backend call (including the profile fetch that stamps last_login_ip) sent none, so the backend fell back to the peer address. Carry the client IP on the request context (WithClientIP, mirroring WithPlatform) and set it once per request in the Connect edge, so the backend client injects X-Forwarded-For on every downstream REST call. Also add the resolved client IP to the backend access log. Test: WithClientIP rides a non-chat call (Profile) as X-Forwarded-For, and is absent when no IP is set. Docs updated (ARCHITECTURE gateway↔backend + edge).
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// tracerName names the instrumentation scope for backend HTTP spans.
|
|
const tracerName = "scrabble/backend/server"
|
|
|
|
// Middleware returns gin middleware that, for every request, opens a server
|
|
// span, measures server-side latency, and emits a structured access log
|
|
// correlated with the active trace. It uses the globally-registered tracer, so
|
|
// spans are exported only when an exporter is configured, while the timing log
|
|
// is always emitted. Probe paths (/healthz, /readyz) log at debug level to keep
|
|
// the default log clean.
|
|
func Middleware(logger *zap.Logger) gin.HandlerFunc {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
tracer := otel.Tracer(tracerName)
|
|
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
route := c.FullPath()
|
|
if route == "" {
|
|
route = c.Request.URL.Path
|
|
}
|
|
|
|
ctx, span := tracer.Start(c.Request.Context(), c.Request.Method+" "+route)
|
|
c.Request = c.Request.WithContext(ctx)
|
|
|
|
c.Next()
|
|
|
|
status := c.Writer.Status()
|
|
elapsed := time.Since(start)
|
|
|
|
span.SetAttributes(
|
|
attribute.String("http.request.method", c.Request.Method),
|
|
attribute.String("http.route", route),
|
|
attribute.Int("http.response.status_code", status),
|
|
)
|
|
if status >= http.StatusInternalServerError {
|
|
span.SetStatus(codes.Error, http.StatusText(status))
|
|
}
|
|
span.End()
|
|
|
|
fields := []zap.Field{
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", route),
|
|
zap.Int("status", status),
|
|
zap.Duration("latency", elapsed),
|
|
// The gateway forwards the real caller as X-Forwarded-For (and Caddy does for /_gm), which
|
|
// gin resolves here — so the access log carries the client IP, not the gateway's connection.
|
|
zap.String("client_ip", c.ClientIP()),
|
|
}
|
|
fields = append(fields, TraceFieldsFromContext(ctx)...)
|
|
|
|
if isProbePath(c.Request.URL.Path) {
|
|
logger.Debug("http request", fields...)
|
|
return
|
|
}
|
|
logger.Info("http request", fields...)
|
|
}
|
|
}
|
|
|
|
// isProbePath reports whether path is one of the unauthenticated infrastructure
|
|
// probes, whose access logs are demoted to debug level.
|
|
func isProbePath(path string) bool {
|
|
return path == "/healthz" || path == "/readyz"
|
|
}
|