github.com/grafana/pyroscope@v1.18.0/pkg/featureflags/client_capability.go (about) 1 package featureflags 2 3 import ( 4 "context" 5 "mime" 6 "net/http" 7 "strings" 8 9 "connectrpc.com/connect" 10 "github.com/go-kit/log/level" 11 "github.com/grafana/dskit/middleware" 12 "github.com/grafana/pyroscope/pkg/util" 13 "google.golang.org/grpc" 14 "google.golang.org/grpc/metadata" 15 ) 16 17 const ( 18 // Capability names - update parseClientCapabilities below when new capabilities added 19 allowUtf8LabelNamesCapabilityName string = "allow-utf8-labelnames" 20 ) 21 22 // Define a custom context key type to avoid collisions 23 type contextKey struct{} 24 25 type ClientCapabilities struct { 26 AllowUtf8LabelNames bool 27 } 28 29 func WithClientCapabilities(ctx context.Context, clientCapabilities ClientCapabilities) context.Context { 30 return context.WithValue(ctx, contextKey{}, clientCapabilities) 31 } 32 33 func GetClientCapabilities(ctx context.Context) (ClientCapabilities, bool) { 34 value, ok := ctx.Value(contextKey{}).(ClientCapabilities) 35 return value, ok 36 } 37 38 func ClientCapabilitiesGRPCMiddleware() grpc.UnaryServerInterceptor { 39 return func( 40 ctx context.Context, 41 req interface{}, 42 info *grpc.UnaryServerInfo, 43 handler grpc.UnaryHandler, 44 ) (interface{}, error) { 45 // Extract metadata from context 46 md, ok := metadata.FromIncomingContext(ctx) 47 if !ok { 48 return handler(ctx, req) 49 } 50 51 // Convert metadata to http.Header for reuse of existing parsing logic 52 httpHeader := make(http.Header) 53 for key, values := range md { 54 // gRPC metadata keys are lowercase, HTTP headers are case-insensitive 55 httpHeader[http.CanonicalHeaderKey(key)] = values 56 } 57 58 // Reuse existing HTTP header parsing 59 clientCapabilities, err := parseClientCapabilities(httpHeader) 60 if err != nil { 61 return nil, connect.NewError(connect.CodeInvalidArgument, err) 62 } 63 64 enhancedCtx := WithClientCapabilities(ctx, clientCapabilities) 65 return handler(enhancedCtx, req) 66 } 67 } 68 69 // ClientCapabilitiesHttpMiddleware creates middleware that extracts and parses the 70 // `Accept` header for capabilities the client supports 71 func ClientCapabilitiesHttpMiddleware() middleware.Interface { 72 return middleware.Func(func(next http.Handler) http.Handler { 73 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 clientCapabilities, err := parseClientCapabilities(r.Header) 75 if err != nil { 76 http.Error(w, "Invalid header format: "+err.Error(), http.StatusBadRequest) 77 return 78 } 79 80 ctx := WithClientCapabilities(r.Context(), clientCapabilities) 81 next.ServeHTTP(w, r.WithContext(ctx)) 82 }) 83 }) 84 } 85 86 func parseClientCapabilities(header http.Header) (ClientCapabilities, error) { 87 acceptHeaderValues := header.Values("Accept") 88 89 var capabilities ClientCapabilities 90 91 for _, acceptHeaderValue := range acceptHeaderValues { 92 if acceptHeaderValue != "" { 93 accepts := strings.Split(acceptHeaderValue, ",") 94 95 for _, accept := range accepts { 96 if _, params, err := mime.ParseMediaType(accept); err != nil { 97 return capabilities, err 98 } else { 99 for k, v := range params { 100 switch k { 101 case allowUtf8LabelNamesCapabilityName: 102 if v == "true" { 103 capabilities.AllowUtf8LabelNames = true 104 } 105 default: 106 level.Debug(util.Logger).Log( 107 "msg", "unknown capability parsed from Accept header", 108 "acceptHeaderKey", k, 109 "acceptHeaderValue", v) 110 } 111 } 112 } 113 } 114 } 115 } 116 return capabilities, nil 117 }