Skip to content
Open
28 changes: 19 additions & 9 deletions pkg/logql/log/label_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,18 +358,28 @@ type StringLabelFilter struct {
// This is the only LabelFilterer that can filter out the __error__ label.
// Unlike other LabelFilterer which apply conversion, if the label name doesn't exist it is compared with an empty value.
func NewStringLabelFilter(m *labels.Matcher) LabelFilterer {
f, err := NewLabelFilter(m.Value, m.Type)
if err != nil {
switch m.Type {
case labels.MatchRegexp, labels.MatchNotRegexp:
// Label regex uses Prometheus labels.Matcher, which is fully anchored.
// Keeps Loki label semantics aligned with Prometheus spec. (Ref: loki#14433)

if m.Type == labels.MatchRegexp && m.Value == ".*" {
return &NoopLabelFilter{}
}

// Fallback to Prometheus fully-anchored matcher
return &StringLabelFilter{Matcher: m}
}

if f == TrueFilter {
return &NoopLabelFilter{m}
}
default:
// Non-regex matchers: try the specialized fast path.
if f, err := NewLabelFilter(m.Value, m.Type); err == nil {
if f == TrueFilter {
return &NoopLabelFilter{}
}
return &LineFilterLabelFilter{Matcher: m, Filter: f}
}

return &LineFilterLabelFilter{
Matcher: m,
Filter: f,
return &StringLabelFilter{Matcher: m}
}
}

Expand Down
96 changes: 96 additions & 0 deletions pkg/logql/log/label_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,102 @@ func TestStringLabelFilter(t *testing.T) {
}
}

func TestStringLabelFilter_FullyAnchored_Regexp(t *testing.T) {
// NOTE: https://github.com/grafana/loki/issues/14433
lbs := labels.FromStrings("foo", "bar", "test", "1234")

cases := []struct {
name string
pat string
shouldMatch bool
}{
{"exact", `1234`, true},
{"substring_prefix", `23.*`, false},
{"substring_suffix", `.*23`, false},
{"explicit_anchors", `^1234$`, true},
{"wildcard_around", `.*234.*`, true},
{"special_char_dot", `1234[.]`, false},
{"dotstar_all", `.*`, true},
{"half_anchor_caret_only", `^12`, false},
{"half_anchor_dollar_only", `34$`, false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := labels.MustNewMatcher(labels.MatchRegexp, "test", tc.pat)
f := NewStringLabelFilter(m)

b := NewBaseLabelsBuilder().ForLabels(lbs, labels.StableHash(lbs))
b.Reset()
_, ok := f.Process(0, []byte("line"), b)
assert.Equal(t, tc.shouldMatch, ok)
})
}
}

func TestStringLabelFilter_FullyAnchored_NotRegexp(t *testing.T) {
lbs := labels.FromStrings("foo", "bar", "test", "1234")

cases := []struct {
name string
pat string
shouldMatch bool
}{
{"not_exact_1234", `1234`, false},
{"not_substring_prefix", `23.*`, true},
{"not_substring_suffix", `.*23`, true},
{"not_explicit_anchors", `^1234$`, false},
{"not_wildcard_around", `.*234.*`, false},
{"not_dotstar_all", `.*`, false},
{"not_half_anchor_caret_only", `^12`, true},
{"not_half_anchor_dollar_only", `34$`, true},
{"not_special_char_dot", `1234[.]`, true},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := labels.MustNewMatcher(labels.MatchNotRegexp, "test", tc.pat)
f := NewStringLabelFilter(m)

b := NewBaseLabelsBuilder().ForLabels(lbs, labels.StableHash(lbs))
b.Reset()
_, ok := f.Process(0, []byte("line"), b)
assert.Equal(t, tc.shouldMatch, ok)
})
}
}

func TestStringLabelFilter_MissingLabel_AnchoredSemantics(t *testing.T) {
lbs := labels.FromStrings("foo", "bar")

cases := []struct {
name string
mt labels.MatchType
pat string
shouldMatch bool
}{
{"re_empty_matches_empty", labels.MatchRegexp, ``, true},
{"re_dotstar_matches_empty", labels.MatchRegexp, `.*`, true},
{"re_literal_1234_not_empty", labels.MatchRegexp, `1234`, false},
{"not_re_empty_on_empty", labels.MatchNotRegexp, ``, false},
{"not_re_dotstar_on_empty", labels.MatchNotRegexp, `.*`, false},
{"re_explicit_empty_anchors", labels.MatchRegexp, `^$`, true},
{"not_re_explicit_empty_anchors", labels.MatchNotRegexp, `^$`, false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := labels.MustNewMatcher(tc.mt, "test", tc.pat)
f := NewStringLabelFilter(m)

b := NewBaseLabelsBuilder().ForLabels(lbs, labels.StableHash(lbs))
b.Reset()
_, ok := f.Process(0, []byte("line"), b)
assert.Equal(t, tc.shouldMatch, ok)
})
}
}

var result bool

func BenchmarkLineLabelFilters(b *testing.B) {
Expand Down