-
Couldn't load subscription status.
- Fork 701
feat: Implement utf8 label name client capability #4442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
72a3b80
cbb8eb2
f651740
6e8c895
a2b48a1
d28e172
8b0a87e
12c20e7
52d3ed3
cb7c9b2
705b47e
84bc1b2
b9d1ec2
b2888fa
9b0d790
f41512f
55810eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func Test_AcceptHeader(t *testing.T) { | ||
| tests := []struct { | ||
| Name string | ||
| Header http.Header | ||
| ClientCapabilities []string | ||
| Want []string | ||
| }{ | ||
| { | ||
| Name: "empty header adds capability", | ||
| Header: http.Header{}, | ||
| ClientCapabilities: []string{ | ||
| "allow-utf8-labelnames=true", | ||
| }, | ||
| Want: []string{"*/*; allow-utf8-labelnames=true"}, | ||
| }, | ||
| { | ||
| Name: "existing header appends capability", | ||
| Header: http.Header{ | ||
| "Accept": []string{"application/json"}, | ||
| }, | ||
| ClientCapabilities: []string{ | ||
| "allow-utf8-labelnames=true", | ||
| }, | ||
| Want: []string{"application/json", "*/*; allow-utf8-labelnames=true"}, | ||
simonswine marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| { | ||
| Name: "multiple existing values appends capability", | ||
| Header: http.Header{ | ||
| "Accept": []string{"application/json", "text/plain"}, | ||
| }, | ||
| ClientCapabilities: []string{ | ||
| "allow-utf8-labelnames=true", | ||
| }, | ||
| Want: []string{"application/json", "text/plain", "*/*; allow-utf8-labelnames=true"}, | ||
simonswine marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| { | ||
| Name: "existing capability is not duplicated", | ||
| Header: http.Header{ | ||
| "Accept": []string{"*/*; allow-utf8-labelnames=true"}, | ||
| }, | ||
| ClientCapabilities: []string{ | ||
| "allow-utf8-labelnames=true", | ||
| }, | ||
| Want: []string{"*/*; allow-utf8-labelnames=true"}, | ||
| }, | ||
| { | ||
| Name: "multiple client capabilities appends capability", | ||
| Header: http.Header{ | ||
| "Accept": []string{"*/*; allow-utf8-labelnames=true"}, | ||
| }, | ||
| ClientCapabilities: []string{ | ||
| "allow-utf8-labelnames=true", | ||
| "capability2=false", | ||
| }, | ||
| Want: []string{"*/*; allow-utf8-labelnames=true", "*/*; capability2=false"}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.Name, func(t *testing.T) { | ||
| t.Parallel() | ||
| req, _ := http.NewRequest("GET", "example.com", nil) | ||
| req.Header = tt.Header | ||
| clientCapabilities := tt.ClientCapabilities | ||
|
|
||
| addClientCapabilitiesHeader(req, acceptHeaderMimeType, clientCapabilities) | ||
| require.Equal(t, tt.Want, req.Header.Values("Accept")) | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package featureflags | ||
jake-kramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import ( | ||
| "context" | ||
| "mime" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "connectrpc.com/connect" | ||
| "github.com/go-kit/log/level" | ||
| "github.com/grafana/dskit/middleware" | ||
| "github.com/grafana/pyroscope/pkg/util" | ||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/metadata" | ||
| ) | ||
|
|
||
| const ( | ||
| // Capability names - update parseClientCapabilities below when new capabilities added | ||
| allowUtf8LabelNamesCapabilityName string = "allow-utf8-labelnames" | ||
| ) | ||
|
|
||
| // Define a custom context key type to avoid collisions | ||
| type contextKey struct{} | ||
|
|
||
| type ClientCapabilities struct { | ||
| AllowUtf8LabelNames bool | ||
| } | ||
|
|
||
| func WithClientCapabilities(ctx context.Context, clientCapabilities ClientCapabilities) context.Context { | ||
| return context.WithValue(ctx, contextKey{}, clientCapabilities) | ||
| } | ||
|
|
||
| func GetClientCapabilities(ctx context.Context) (ClientCapabilities, bool) { | ||
| value, ok := ctx.Value(contextKey{}).(ClientCapabilities) | ||
| return value, ok | ||
| } | ||
|
|
||
| func ClientCapabilitiesGRPCMiddleware() grpc.UnaryServerInterceptor { | ||
| return func( | ||
| ctx context.Context, | ||
| req interface{}, | ||
| info *grpc.UnaryServerInfo, | ||
| handler grpc.UnaryHandler, | ||
| ) (interface{}, error) { | ||
| // Extract metadata from context | ||
| md, ok := metadata.FromIncomingContext(ctx) | ||
| if !ok { | ||
| return handler(ctx, req) | ||
| } | ||
|
|
||
| // Convert metadata to http.Header for reuse of existing parsing logic | ||
| httpHeader := make(http.Header) | ||
| for key, values := range md { | ||
| // gRPC metadata keys are lowercase, HTTP headers are case-insensitive | ||
| httpHeader[http.CanonicalHeaderKey(key)] = values | ||
| } | ||
|
|
||
| // Reuse existing HTTP header parsing | ||
| // TODO add metrics = # requests like this and # clients [need | ||
| // labels for requests and clients/tenet and user agent(?)] | ||
|
||
| clientCapabilities, err := parseClientCapabilities(httpHeader) | ||
| if err != nil { | ||
| return nil, connect.NewError(connect.CodeInvalidArgument, err) | ||
| } | ||
|
|
||
| enhancedCtx := WithClientCapabilities(ctx, clientCapabilities) | ||
| return handler(enhancedCtx, req) | ||
| } | ||
| } | ||
|
|
||
| // ClientCapabilitiesHttpMiddleware creates middleware that extracts and parses the | ||
| // `Accept` header for capabilities the client supports | ||
| func ClientCapabilitiesHttpMiddleware() middleware.Interface { | ||
| return middleware.Func(func(next http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| clientCapabilities, err := parseClientCapabilities(r.Header) | ||
| if err != nil { | ||
| http.Error(w, "Invalid header format: "+err.Error(), http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| ctx := WithClientCapabilities(r.Context(), clientCapabilities) | ||
| next.ServeHTTP(w, r.WithContext(ctx)) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| func parseClientCapabilities(header http.Header) (ClientCapabilities, error) { | ||
| acceptHeaderValues := header.Values("Accept") | ||
|
|
||
| var capabilities ClientCapabilities | ||
|
|
||
| for _, acceptHeaderValue := range acceptHeaderValues { | ||
| if acceptHeaderValue != "" { | ||
| accepts := strings.Split(acceptHeaderValue, ",") | ||
|
|
||
| for _, accept := range accepts { | ||
| if _, params, err := mime.ParseMediaType(accept); err != nil { | ||
| return capabilities, err | ||
| } else { | ||
| for k, v := range params { | ||
| switch k { | ||
| case allowUtf8LabelNamesCapabilityName: | ||
| if v == "true" { | ||
| capabilities.AllowUtf8LabelNames = true | ||
| } | ||
| default: | ||
| level.Debug(util.Logger).Log( | ||
| "msg", "unknown capability parsed from Accept header", | ||
| "acceptHeaderKey", k, | ||
| "acceptHeaderValue", v) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return capabilities, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.