@@ -174,6 +174,11 @@ type Config struct {
174174 // instead of being used for postings selection.
175175 EnableMatcherOptimization bool `yaml:"enable_matcher_optimization"`
176176
177+ // Enable regex matcher limits and metrics collection for unoptimized regex queries.
178+ // When enabled, the ingester will track pattern length, label cardinality, and total value length
179+ // for unoptimized regex matchers, and enforce per-tenant limits if configured.
180+ EnableRegexMatcherLimits bool `yaml:"enable_regex_matcher_limits"`
181+
177182 QueryProtection configs.QueryProtection `yaml:"query_protection"`
178183}
179184
@@ -205,7 +210,7 @@ func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
205210 f .IntVar (& cfg .MatchersCacheMaxItems , "ingester.matchers-cache-max-items" , 0 , "Maximum number of entries in the regex matchers cache. 0 to disable." )
206211 f .BoolVar (& cfg .SkipMetadataLimits , "ingester.skip-metadata-limits" , true , "If enabled, the metadata API returns all metadata regardless of the limits." )
207212 f .BoolVar (& cfg .EnableMatcherOptimization , "ingester.enable-matcher-optimization" , false , "Enable optimization of label matchers when query chunks. When enabled, matchers with low selectivity such as =~.+ are applied lazily during series scanning instead of being used for postings matching." )
208-
213+ f . BoolVar ( & cfg . EnableRegexMatcherLimits , "ingester.enable-regex-matcher-limits" , false , "Enable regex matcher limits and metrics collection for unoptimized regex queries. When enabled, the ingester will track pattern length, label cardinality, and total value length for unoptimized regex matchers." )
209214 cfg .DefaultLimits .RegisterFlagsWithPrefix (f , "ingester." )
210215 cfg .QueryProtection .RegisterFlagsWithPrefix (f , "ingester." )
211216}
@@ -813,7 +818,8 @@ func New(cfg Config, limits *validation.Overrides, registerer prometheus.Registe
813818 i .ingestionRate ,
814819 & i .maxInflightPushRequests ,
815820 & i .maxInflightQueryRequests ,
816- cfg .BlocksStorageConfig .TSDB .PostingsCache .Blocks .Enabled || cfg .BlocksStorageConfig .TSDB .PostingsCache .Head .Enabled )
821+ cfg .BlocksStorageConfig .TSDB .PostingsCache .Blocks .Enabled || cfg .BlocksStorageConfig .TSDB .PostingsCache .Head .Enabled ,
822+ cfg .EnableRegexMatcherLimits )
817823 i .validateMetrics = validation .NewValidateMetrics (registerer )
818824
819825 // Replace specific metrics which we can't directly track but we need to read
@@ -911,6 +917,7 @@ func NewForFlusher(cfg Config, limits *validation.Overrides, registerer promethe
911917 & i .maxInflightPushRequests ,
912918 & i .maxInflightQueryRequests ,
913919 cfg .BlocksStorageConfig .TSDB .PostingsCache .Blocks .Enabled || cfg .BlocksStorageConfig .TSDB .PostingsCache .Head .Enabled ,
920+ cfg .EnableRegexMatcherLimits ,
914921 )
915922
916923 i .TSDBState .shipperIngesterID = "flusher"
@@ -2428,7 +2435,7 @@ func (i *Ingester) queryStream(ctx context.Context, userID string, req *client.Q
24282435 numSeries := 0
24292436 totalDataBytes := 0
24302437 numChunks := 0
2431- numSeries , numSamples , totalDataBytes , numChunks , err = i .queryStreamChunks (ctx , db , int64 (from ), int64 (through ), matchers , shardMatcher , stream )
2438+ numSeries , numSamples , totalDataBytes , numChunks , err = i .queryStreamChunks (ctx , userID , db , int64 (from ), int64 (through ), matchers , shardMatcher , stream )
24322439
24332440 if err != nil {
24342441 return err
@@ -2467,14 +2474,116 @@ func (i *Ingester) trackInflightQueryRequest() (func(), error) {
24672474 }, nil
24682475}
24692476
2477+ func isRegexUnOptimized (matcher * labels.Matcher ) bool {
2478+ if matcher .Type != labels .MatchRegexp {
2479+ return false
2480+ }
2481+ // PostingsForMatchers will optimize .* and .+ matchers, so we don't need to check them.
2482+ if matcher .Value == ".*" || matcher .Value == ".+" {
2483+ return false
2484+ }
2485+ return ! matcher .IsRegexOptimized ()
2486+ }
2487+
2488+ // checkRegexMatcherLimits validates regex matchers against configured limits to prevent expensive queries.
2489+ func (i * Ingester ) checkRegexMatcherLimits (ctx context.Context , userID string , db * userTSDB , matchers []* labels.Matcher , from , through int64 ) error {
2490+ // Collect all unoptimized regex matchers upfront
2491+ var unoptimizedMatchers []* labels.Matcher
2492+ for _ , matcher := range matchers {
2493+ if isRegexUnOptimized (matcher ) {
2494+ unoptimizedMatchers = append (unoptimizedMatchers , matcher )
2495+ // Record pattern length metric
2496+ if i .metrics .unoptimizedRegexPatternLength != nil {
2497+ i .metrics .unoptimizedRegexPatternLength .Observe (float64 (len (matcher .Value )))
2498+ }
2499+ }
2500+ }
2501+
2502+ if len (unoptimizedMatchers ) == 0 {
2503+ return nil
2504+ }
2505+
2506+ // Check pattern length limit if configured
2507+ maxPatternLength := i .limits .MaxRegexPatternLength (userID )
2508+ if maxPatternLength > 0 {
2509+ for _ , matcher := range unoptimizedMatchers {
2510+ patternLength := len (matcher .Value )
2511+ if patternLength > maxPatternLength {
2512+ return validation .LimitError (fmt .Sprintf (
2513+ "regex pattern length %d exceeds limit %d for unoptimized regex matcher %q. Consider using a more specific pattern." ,
2514+ patternLength , maxPatternLength , matcher .String (),
2515+ ))
2516+ }
2517+ }
2518+ }
2519+
2520+ // Query TSDB to collect cardinality and total value length metrics and check limits.
2521+ labelQuerier , err := db .Querier (from , through )
2522+ if err != nil {
2523+ return err
2524+ }
2525+ defer labelQuerier .Close ()
2526+
2527+ maxCardinality := i .limits .MaxLabelCardinalityForUnoptimizedRegex (userID )
2528+ maxTotalValueLength := i .limits .MaxTotalLabelValueLengthForUnoptimizedRegex (userID )
2529+
2530+ for _ , matcher := range unoptimizedMatchers {
2531+ labelVals , _ , err := labelQuerier .LabelValues (ctx , matcher .Name , nil )
2532+ if err != nil {
2533+ // If we can't get label values, skip this matcher and continue checking others
2534+ continue
2535+ }
2536+
2537+ cardinality := len (labelVals )
2538+
2539+ // Calculate total length of all values
2540+ var totalValueLength int
2541+ for _ , val := range labelVals {
2542+ totalValueLength += len (val )
2543+ }
2544+
2545+ // Always record metrics regardless of whether limits are configured (if metrics are enabled)
2546+ if i .metrics .unoptimizedRegexLabelCardinality != nil {
2547+ i .metrics .unoptimizedRegexLabelCardinality .Observe (float64 (cardinality ))
2548+ }
2549+ if i .metrics .unoptimizedRegexTotalValueLength != nil {
2550+ i .metrics .unoptimizedRegexTotalValueLength .Observe (float64 (totalValueLength ))
2551+ }
2552+
2553+ // Check limits only if configured
2554+ if maxCardinality > 0 && cardinality > maxCardinality {
2555+ return validation .LimitError (fmt .Sprintf (
2556+ "label %q has cardinality %d which exceeds limit %d for unoptimized regex matcher %q. Consider using a more specific matcher." ,
2557+ matcher .Name , cardinality , maxCardinality , matcher .String (),
2558+ ))
2559+ }
2560+
2561+ if maxTotalValueLength > 0 && totalValueLength > maxTotalValueLength {
2562+ return validation .LimitError (fmt .Sprintf (
2563+ "label %q has total value length %d bytes (across %d values) which exceeds limit %d for unoptimized regex matcher %q. Consider using a more specific matcher." ,
2564+ matcher .Name , totalValueLength , cardinality , maxTotalValueLength , matcher .String (),
2565+ ))
2566+ }
2567+ }
2568+
2569+ return nil
2570+ }
2571+
24702572// queryStreamChunks streams metrics from a TSDB. This implements the client.IngesterServer interface
2471- func (i * Ingester ) queryStreamChunks (ctx context.Context , db * userTSDB , from , through int64 , matchers []* labels.Matcher , sm * storepb.ShardMatcher , stream client.Ingester_QueryStreamServer ) (numSeries , numSamples , totalBatchSizeBytes , numChunks int , _ error ) {
2573+ func (i * Ingester ) queryStreamChunks (ctx context.Context , userID string , db * userTSDB , from , through int64 , matchers []* labels.Matcher , sm * storepb.ShardMatcher , stream client.Ingester_QueryStreamServer ) (numSeries , numSamples , totalBatchSizeBytes , numChunks int , _ error ) {
24722574 q , err := db .ChunkQuerier (from , through )
24732575 if err != nil {
24742576 return 0 , 0 , 0 , 0 , err
24752577 }
24762578 defer q .Close ()
24772579
2580+ // Check regex matcher limits before executing query if enabled
2581+ if i .cfg .EnableRegexMatcherLimits {
2582+ if err := i .checkRegexMatcherLimits (ctx , userID , db , matchers , from , through ); err != nil {
2583+ return 0 , 0 , 0 , 0 , err
2584+ }
2585+ }
2586+
24782587 c , err := i .trackInflightQueryRequest ()
24792588 if err != nil {
24802589 return 0 , 0 , 0 , 0 , err
0 commit comments