chatdesk-ui/auth_v2.169.0/internal/observability/request-tracing.go

171 lines
5.0 KiB
Go

package observability
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"go.opentelemetry.io/otel/trace"
)
// traceChiRoutesSafely attempts to extract the Chi RouteContext. If the
// request does not have a RouteContext it will recover from the panic and
// attempt to figure out the route from the URL's path.
func traceChiRoutesSafely(r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logrus.WithField("error", rec).Error("unable to trace chi routes, traces may be off")
span := trace.SpanFromContext(r.Context())
span.SetAttributes(semconv.HTTPRouteKey.String(r.URL.Path))
}
}()
routeContext := chi.RouteContext(r.Context())
span := trace.SpanFromContext(r.Context())
span.SetAttributes(semconv.HTTPRouteKey.String(routeContext.RoutePattern()))
}
// traceChiRouteURLParamsSafely attempts to extract the Chi RouteContext
// URLParams values for the route and assign them to the tracing span. If the
// request does not have a RouteContext it will recover from the panic and not
// set any params.
func traceChiRouteURLParamsSafely(r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logrus.WithField("error", rec).Error("unable to trace route with route params, traces may be off")
}
}()
routeContext := chi.RouteContext(r.Context())
span := trace.SpanFromContext(r.Context())
var attributes []attribute.KeyValue
for i := 0; i < len(routeContext.URLParams.Keys); i += 1 {
key := routeContext.URLParams.Keys[i]
value := routeContext.URLParams.Values[i]
attributes = append(attributes, attribute.String("http.route.param."+key, value))
}
if len(attributes) > 0 {
span.SetAttributes(attributes...)
}
}
type interceptingResponseWriter struct {
writer http.ResponseWriter
statusCode int
}
func (w *interceptingResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.writer.WriteHeader(statusCode)
}
func (w *interceptingResponseWriter) Write(data []byte) (int, error) {
return w.writer.Write(data)
}
func (w *interceptingResponseWriter) Header() http.Header {
return w.writer.Header()
}
// countStatusCodesSafely counts the number of HTTP status codes per route that
// occurred while GoTrue was running. If it is not able to identify the route
// via chi.RouteContext(ctx).RoutePattern() it counts with a noroute attribute.
func countStatusCodesSafely(w *interceptingResponseWriter, r *http.Request, counter metric.Int64Counter) {
if counter == nil {
return
}
defer func() {
if rec := recover(); rec != nil {
logrus.WithField("error", rec).Error("unable to count status codes safely, metrics may be off")
counter.Add(
r.Context(),
1,
metric.WithAttributes(
attribute.Bool("noroute", true),
attribute.Int("code", w.statusCode)),
)
}
}()
ctx := r.Context()
routeContext := chi.RouteContext(ctx)
routePattern := semconv.HTTPRouteKey.String(routeContext.RoutePattern())
counter.Add(
ctx,
1,
metric.WithAttributes(attribute.Int("code", w.statusCode), routePattern),
)
}
// RequestTracing returns an HTTP handler that traces all HTTP requests coming
// in. Supports Chi routers, so this should be one of the first middlewares on
// the router.
func RequestTracing() func(http.Handler) http.Handler {
meter := otel.Meter("gotrue")
statusCodes, err := meter.Int64Counter(
"http_status_codes",
metric.WithDescription("Number of returned HTTP status codes"),
)
if err != nil {
logrus.WithError(err).Error("unable to get gotrue.http_status_codes counter metric")
}
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
writer := interceptingResponseWriter{
writer: w,
}
defer traceChiRoutesSafely(r)
defer traceChiRouteURLParamsSafely(r)
defer countStatusCodesSafely(&writer, r, statusCodes)
originalUserAgent := r.Header.Get("X-Gotrue-Original-User-Agent")
if originalUserAgent != "" {
r.Header.Set("User-Agent", originalUserAgent)
}
next.ServeHTTP(&writer, r)
if originalUserAgent != "" {
r.Header.Set("X-Gotrue-Original-User-Agent", originalUserAgent)
r.Header.Set("User-Agent", "stripped")
}
}
otelHandler := otelhttp.NewHandler(http.HandlerFunc(fn), "api")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// there is a vulnerability with otelhttp where
// User-Agent strings are kept in RAM indefinitely and
// can be used as an easy way to resource exhaustion;
// so this code strips the User-Agent header before
// it's passed to be traced by otelhttp, and then is
// returned back to the middleware
// https://github.com/supabase/gotrue/security/dependabot/11
userAgent := r.UserAgent()
if userAgent != "" {
r.Header.Set("X-Gotrue-Original-User-Agent", userAgent)
r.Header.Set("User-Agent", "stripped")
}
otelHandler.ServeHTTP(w, r)
})
}
}