From 2a5347762d9cd3d80286e0b49645f6369505723c Mon Sep 17 00:00:00 2001 From: Dimitry Linkov Date: Fri, 24 Jul 2026 09:57:40 -0700 Subject: [PATCH] querier: preserve UTF-8 (dotted) label names on read when sanitization is disabled Dotted label names (e.g. nf.app) are stored and matchable when the operator runs with validation.disable-label-sanitization=true, but they were still filtered out of LabelNames/Series label listings (and therefore Grafana typeahead) unless the request carried the per-request allow-utf8-labelnames client capability. In the v1 read path that capability never reaches the querier: it is parsed by the query-frontend HTTP middleware, but querier.NewGRPCHandler does not re-apply ClientCapabilitiesHttpMiddleware and the capability is not propagated over the frontend->querier hop. So dotted label names are unconditionally dropped from listings on v1, even though they can be queried by matcher. Preserve them additionally whenever label sanitization is disabled for the tenant. That is the same setting that keeps dotted names verbatim on the write path, so honoring it on read makes read symmetric with write and works for every client (including the Grafana datasource query editor and dashboard variables, which never send the capability). The per-request capability behavior is preserved unchanged. Adds Querier.keepUTF8LabelNames() and unit tests covering the capability, the disable-label-sanitization limit, and the no-tenant case. Co-Authored-By: Claude Opus 4.8 --- pkg/querier/querier.go | 34 +++++++++++-- pkg/querier/querier_utf8_labels_test.go | 65 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 pkg/querier/querier_utf8_labels_test.go diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index ae5eae490e..adf261b490 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -56,6 +56,7 @@ func (cfg *Config) RegisterFlags(fs *flag.FlagSet) { type Limits interface { QueryAnalysisSeriesEnabled(string) bool + DisableLabelSanitization(string) bool } type Querier struct { @@ -274,6 +275,33 @@ func (q *Querier) LabelValues(ctx context.Context, req *connect.Request[typesv1. }), nil } +// keepUTF8LabelNames reports whether non-legacy (e.g. dotted) label names such +// as "nf.app" should be preserved in label-listing responses rather than +// filtered out by filterLabelNames. +// +// Upstream only preserves them when the request carries the per-request +// allow-utf8-labelnames client capability. In the v1 read path that capability +// never reaches the querier (it is parsed by the query-frontend HTTP middleware +// but querier.NewGRPCHandler does not re-apply it, and it is not propagated over +// the frontend->querier hop), so dotted label names are always filtered out of +// typeahead/label listings even though they are stored and matchable. +// +// We additionally preserve them whenever label sanitization is disabled for the +// tenant (validation.disable-label-sanitization). That is the same setting that +// keeps dotted label names verbatim on the write path, so honoring it on read +// makes read symmetric with write and works for every client (including the +// Grafana datasource query editor and dashboard variables, which do not send +// the capability). +func (q *Querier) keepUTF8LabelNames(ctx context.Context) bool { + if capabilities, ok := featureflags.GetClientCapabilities(ctx); ok && capabilities.AllowUtf8LabelNames { + return true + } + if tenantID, err := tenant.TenantID(ctx); err == nil && q.limits.DisableLabelSanitization(tenantID) { + return true + } + return false +} + func filterLabelNames(labelNames []string) []string { filtered := make([]string, 0, len(labelNames)) // Filter out label names not passing legacy validation if utf8 label names not enabled @@ -303,7 +331,7 @@ func (q *Querier) LabelNames(ctx context.Context, req *connect.Request[typesv1.L } labelNames := uniqueSortedStrings(responses) - if capabilities, ok := featureflags.GetClientCapabilities(ctx); !ok || !capabilities.AllowUtf8LabelNames { + if !q.keepUTF8LabelNames(ctx) { level.Debug(q.logger).Log("msg", "filtering out non-valid labels") labelNames = filterLabelNames(labelNames) } @@ -357,7 +385,7 @@ func (q *Querier) LabelNames(ctx context.Context, req *connect.Request[typesv1.L } labelNames := uniqueSortedStrings(responses) - if capabilities, ok := featureflags.GetClientCapabilities(ctx); !ok || !capabilities.AllowUtf8LabelNames { + if !q.keepUTF8LabelNames(ctx) { level.Debug(q.logger).Log("msg", "filtering out non-valid labels") labelNames = filterLabelNames(labelNames) } @@ -410,7 +438,7 @@ func (q *Querier) filterLabelNames( ctx context.Context, req *connect.Request[querierv1.SeriesRequest], ) ([]string, error) { - if capabilities, ok := featureflags.GetClientCapabilities(ctx); ok && capabilities.AllowUtf8LabelNames { + if q.keepUTF8LabelNames(ctx) { return req.Msg.LabelNames, nil } diff --git a/pkg/querier/querier_utf8_labels_test.go b/pkg/querier/querier_utf8_labels_test.go new file mode 100644 index 0000000000..cca24ae77d --- /dev/null +++ b/pkg/querier/querier_utf8_labels_test.go @@ -0,0 +1,65 @@ +package querier + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/pyroscope/v2/pkg/featureflags" + "github.com/grafana/pyroscope/v2/pkg/tenant" +) + +// stubLimits implements the querier Limits interface for tests. +type stubLimits struct { + disableLabelSanitization bool +} + +func (s stubLimits) QueryAnalysisSeriesEnabled(string) bool { return false } +func (s stubLimits) DisableLabelSanitization(string) bool { return s.disableLabelSanitization } + +func TestKeepUTF8LabelNames(t *testing.T) { + withTenant := func() context.Context { + return tenant.InjectTenantID(context.Background(), "tenant") + } + withCapability := func(ctx context.Context) context.Context { + return featureflags.WithClientCapabilities(ctx, featureflags.ClientCapabilities{AllowUtf8LabelNames: true}) + } + + for _, tc := range []struct { + name string + ctx context.Context + disableLabelSanitization bool + want bool + }{ + { + name: "sanitization disabled preserves dotted names without capability", + ctx: withTenant(), + disableLabelSanitization: true, + want: true, + }, + { + name: "sanitization enabled filters when no capability", + ctx: withTenant(), + disableLabelSanitization: false, + want: false, + }, + { + name: "client capability preserves dotted names even when sanitization enabled", + ctx: withCapability(withTenant()), + disableLabelSanitization: false, + want: true, + }, + { + name: "no tenant and no capability filters", + ctx: context.Background(), + disableLabelSanitization: true, // unreachable: tenant lookup fails, so this is ignored + want: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + q := &Querier{limits: stubLimits{disableLabelSanitization: tc.disableLabelSanitization}} + require.Equal(t, tc.want, q.keepUTF8LabelNames(tc.ctx)) + }) + } +}