171 lines
5.0 KiB
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)
|
|
})
|
|
}
|
|
}
|